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:
gpt-engineer-app[bot]
2026-05-24 20:51:36 +00:00
4 changed files with 90 additions and 13 deletions
+25
View File
@@ -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 />
+56 -13
View File
@@ -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 />
+4
View File
@@ -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 },
+5
View File
@@ -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 }),