Adicionou Calibrar Grid
X-Lovable-Edit-ID: edt-eb99423f-57d2-4db3-95a2-c0c5ad524efa Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -36,6 +36,7 @@ export function ViewerControls() {
|
|||||||
selectedElementKeys, hiddenElementKeys, isolatedElementKeys,
|
selectedElementKeys, hiddenElementKeys, isolatedElementKeys,
|
||||||
hideSelectedElements, isolateSelectedElements, showAllElements, clearElementSelection,
|
hideSelectedElements, isolateSelectedElements, showAllElements, clearElementSelection,
|
||||||
cameraMode, setCameraMode,
|
cameraMode, setCameraMode,
|
||||||
|
gridCalibMode, setGridCalibMode, setGridAutoFollow,
|
||||||
} = useModelStore();
|
} = useModelStore();
|
||||||
const activeModel = models.find(m => m.id === activeModelId);
|
const activeModel = models.find(m => m.id === activeModelId);
|
||||||
const hasSelection = selectedElementKeys.size > 0;
|
const hasSelection = selectedElementKeys.size > 0;
|
||||||
@@ -106,6 +107,30 @@ export function ViewerControls() {
|
|||||||
<span className="font-mono text-xs">Grid</span>
|
<span className="font-mono text-xs">Grid</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Grid calibration: click a face on the piece to anchor the grid there */}
|
||||||
|
<Button
|
||||||
|
variant={gridCalibMode ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 h-9"
|
||||||
|
disabled={!activeModelId}
|
||||||
|
onClick={() => {
|
||||||
|
const next = !gridCalibMode;
|
||||||
|
setGridCalibMode(next);
|
||||||
|
if (next) {
|
||||||
|
setGridAutoFollow(false);
|
||||||
|
toast.info('Calibrar Grid: clique numa face da peça');
|
||||||
|
} else {
|
||||||
|
toast.dismiss();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Posiciona o grid na altura de uma face da peça"
|
||||||
|
>
|
||||||
|
<LayoutGrid className="h-3.5 w-3.5" />
|
||||||
|
<span className="font-mono text-xs">
|
||||||
|
{gridCalibMode ? 'Clique na face…' : 'Calibrar Grid'}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Scale presets */}
|
{/* Scale presets */}
|
||||||
<ScaleSelector />
|
<ScaleSelector />
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
||||||
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
|
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
|
||||||
import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleFactor } from '@/lib/modelTransforms';
|
import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleFactor, getAllModelLocalGroups } from '@/lib/modelTransforms';
|
||||||
import { parseIFCtoThree } from '@/lib/convertIFC';
|
import { parseIFCtoThree } from '@/lib/convertIFC';
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||||
import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus';
|
import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus';
|
||||||
@@ -1035,34 +1035,39 @@ function GridLayer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Continuously aligns the grid Y to the bottom of all visible models so the
|
/** Continuously aligns the grid Y to the bottom of all registered model
|
||||||
* piece always looks like it is sitting on the grid — including after
|
* groups (not the entire scene), so the piece always sits on the grid —
|
||||||
* calibration, fine-tuning, repositioning or scale changes. */
|
* including after calibration, fine-tuning or repositioning. */
|
||||||
function GridAutoFollower() {
|
function GridAutoFollower() {
|
||||||
const { scene } = useThree();
|
|
||||||
const lastY = useRef<number>(Number.NaN);
|
const lastY = useRef<number>(Number.NaN);
|
||||||
const acc = useRef(0);
|
const acc = useRef(0);
|
||||||
useFrame((_, dt) => {
|
useFrame((_, dt) => {
|
||||||
const auto = useModelStore.getState().gridAutoFollow;
|
const auto = useModelStore.getState().gridAutoFollow;
|
||||||
if (!auto) return;
|
if (!auto) return;
|
||||||
// Throttle to ~10 Hz to keep this cheap.
|
|
||||||
acc.current += dt;
|
acc.current += dt;
|
||||||
if (acc.current < 0.1) return;
|
if (acc.current < 0.1) return;
|
||||||
acc.current = 0;
|
acc.current = 0;
|
||||||
|
const groups = getAllModelLocalGroups();
|
||||||
|
if (groups.length === 0) return;
|
||||||
const box = new THREE.Box3();
|
const box = new THREE.Box3();
|
||||||
let has = false;
|
let has = false;
|
||||||
scene.traverse((obj) => {
|
const tmp = new THREE.Box3();
|
||||||
if (obj instanceof THREE.Mesh && obj.geometry) {
|
for (const g of groups) {
|
||||||
|
g.updateWorldMatrix(true, true);
|
||||||
|
tmp.makeEmpty();
|
||||||
|
g.traverse((obj) => {
|
||||||
|
if (!(obj instanceof THREE.Mesh) || !obj.geometry) return;
|
||||||
if (obj.geometry instanceof THREE.SphereGeometry) return;
|
if (obj.geometry instanceof THREE.SphereGeometry) return;
|
||||||
if (obj.geometry instanceof THREE.RingGeometry) return;
|
if (obj.geometry instanceof THREE.RingGeometry) return;
|
||||||
if (obj.userData.__edgeLine) return;
|
if (obj.userData.__edgeLine) return;
|
||||||
const b = new THREE.Box3().setFromObject(obj);
|
const b = new THREE.Box3().setFromObject(obj);
|
||||||
if (isFinite(b.min.y)) {
|
if (isFinite(b.min.y)) tmp.union(b);
|
||||||
if (!has) { box.copy(b); has = true; }
|
|
||||||
else box.union(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
if (!tmp.isEmpty() && isFinite(tmp.min.y)) {
|
||||||
|
if (!has) { box.copy(tmp); has = true; }
|
||||||
|
else box.union(tmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!has) return;
|
if (!has) return;
|
||||||
const target = box.min.y - 0.005;
|
const target = box.min.y - 0.005;
|
||||||
if (!Number.isFinite(lastY.current) || Math.abs(target - lastY.current) > 1e-4) {
|
if (!Number.isFinite(lastY.current) || Math.abs(target - lastY.current) > 1e-4) {
|
||||||
@@ -1073,6 +1078,43 @@ function GridAutoFollower() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** When gridCalibMode is on, the next click on the model sets the grid Y to
|
||||||
|
* the world Y of the clicked surface point and disables auto-follow. */
|
||||||
|
function GridCalibrationHandler() {
|
||||||
|
const { camera, scene, gl } = useThree();
|
||||||
|
const gridCalibMode = useModelStore((s) => s.gridCalibMode);
|
||||||
|
const raycaster = useMemo(() => new THREE.Raycaster(), []);
|
||||||
|
const mouse = useMemo(() => new THREE.Vector2(), []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!gridCalibMode) return;
|
||||||
|
const canvas = gl.domElement;
|
||||||
|
const prevCursor = canvas.style.cursor;
|
||||||
|
canvas.style.cursor = 'crosshair';
|
||||||
|
const onPointerUp = (ev: PointerEvent) => {
|
||||||
|
if (ev.button !== 0) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
raycaster.setFromCamera(mouse, camera);
|
||||||
|
const hits = raycaster.intersectObjects(scene.children, true);
|
||||||
|
const hit = hits.find((h) => isPickableModelMesh(h.object));
|
||||||
|
if (!hit) return;
|
||||||
|
useModelStore.setState({
|
||||||
|
gridY: hit.point.y - 0.001,
|
||||||
|
gridAutoFollow: false,
|
||||||
|
gridCalibMode: false,
|
||||||
|
showGrid: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
canvas.addEventListener('pointerup', onPointerUp);
|
||||||
|
return () => {
|
||||||
|
canvas.removeEventListener('pointerup', onPointerUp);
|
||||||
|
canvas.style.cursor = prevCursor;
|
||||||
|
};
|
||||||
|
}, [gridCalibMode, camera, scene, gl, raycaster, mouse]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Mouse drag handler for desktop "Posicionar" mode: translates/rotates the active model. */
|
/** Mouse drag handler for desktop "Posicionar" mode: translates/rotates the active model. */
|
||||||
function PositionDragHandler() {
|
function PositionDragHandler() {
|
||||||
const { camera, gl, scene } = useThree();
|
const { camera, gl, scene } = useThree();
|
||||||
@@ -1259,6 +1301,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
|||||||
|
|
||||||
<GridLayer />
|
<GridLayer />
|
||||||
<GridAutoFollower />
|
<GridAutoFollower />
|
||||||
|
<GridCalibrationHandler />
|
||||||
|
|
||||||
<PositionDragHandler />
|
<PositionDragHandler />
|
||||||
<SelectionHandler />
|
<SelectionHandler />
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export function getModelLocalGroup(modelId: string | null | undefined): THREE.Ob
|
|||||||
return localGroups.get(modelId);
|
return localGroups.get(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllModelLocalGroups(): THREE.Object3D[] {
|
||||||
|
return Array.from(localGroups.values());
|
||||||
|
}
|
||||||
|
|
||||||
export function worldToModelLocal(
|
export function worldToModelLocal(
|
||||||
modelId: string | null | undefined,
|
modelId: string | null | undefined,
|
||||||
p: { x: number; y: number; z: number },
|
p: { x: number; y: number; z: number },
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ interface ModelStore {
|
|||||||
setGridAutoFollow: (b: boolean) => void;
|
setGridAutoFollow: (b: boolean) => void;
|
||||||
/** Nudge grid by delta meters and disable auto-follow. */
|
/** Nudge grid by delta meters and disable auto-follow. */
|
||||||
nudgeGridY: (delta: number) => void;
|
nudgeGridY: (delta: number) => void;
|
||||||
|
/** When true, the next click on a model face sets grid Y to that point. */
|
||||||
|
gridCalibMode: boolean;
|
||||||
|
setGridCalibMode: (b: boolean) => void;
|
||||||
|
|
||||||
wireframeColor: string;
|
wireframeColor: string;
|
||||||
setWireframeColor: (color: string) => void;
|
setWireframeColor: (color: string) => void;
|
||||||
@@ -535,6 +538,8 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
gridAutoFollow: true,
|
gridAutoFollow: true,
|
||||||
setGridAutoFollow: (gridAutoFollow) => set({ gridAutoFollow }),
|
setGridAutoFollow: (gridAutoFollow) => set({ gridAutoFollow }),
|
||||||
nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })),
|
nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })),
|
||||||
|
gridCalibMode: false,
|
||||||
|
setGridCalibMode: (gridCalibMode) => set({ gridCalibMode }),
|
||||||
|
|
||||||
wireframeColor: '#00f3ff',
|
wireframeColor: '#00f3ff',
|
||||||
setWireframeColor: (wireframeColor) => set({ wireframeColor }),
|
setWireframeColor: (wireframeColor) => set({ wireframeColor }),
|
||||||
|
|||||||
Reference in New Issue
Block a user