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,
|
||||
hideSelectedElements, isolateSelectedElements, showAllElements, clearElementSelection,
|
||||
cameraMode, setCameraMode,
|
||||
gridCalibMode, setGridCalibMode, setGridAutoFollow,
|
||||
} = useModelStore();
|
||||
const activeModel = models.find(m => m.id === activeModelId);
|
||||
const hasSelection = selectedElementKeys.size > 0;
|
||||
@@ -106,6 +107,30 @@ export function ViewerControls() {
|
||||
<span className="font-mono text-xs">Grid</span>
|
||||
</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 */}
|
||||
<ScaleSelector />
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Loader2 } from 'lucide-react';
|
||||
import * as THREE from 'three';
|
||||
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
||||
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 { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
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
|
||||
* piece always looks like it is sitting on the grid — including after
|
||||
* calibration, fine-tuning, repositioning or scale changes. */
|
||||
/** Continuously aligns the grid Y to the bottom of all registered model
|
||||
* groups (not the entire scene), so the piece always sits on the grid —
|
||||
* including after calibration, fine-tuning or repositioning. */
|
||||
function GridAutoFollower() {
|
||||
const { scene } = useThree();
|
||||
const lastY = useRef<number>(Number.NaN);
|
||||
const acc = useRef(0);
|
||||
useFrame((_, dt) => {
|
||||
const auto = useModelStore.getState().gridAutoFollow;
|
||||
if (!auto) return;
|
||||
// Throttle to ~10 Hz to keep this cheap.
|
||||
acc.current += dt;
|
||||
if (acc.current < 0.1) return;
|
||||
acc.current = 0;
|
||||
const groups = getAllModelLocalGroups();
|
||||
if (groups.length === 0) return;
|
||||
const box = new THREE.Box3();
|
||||
let has = false;
|
||||
scene.traverse((obj) => {
|
||||
if (obj instanceof THREE.Mesh && obj.geometry) {
|
||||
const tmp = new THREE.Box3();
|
||||
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.RingGeometry) return;
|
||||
if (obj.userData.__edgeLine) return;
|
||||
const b = new THREE.Box3().setFromObject(obj);
|
||||
if (isFinite(b.min.y)) {
|
||||
if (!has) { box.copy(b); has = true; }
|
||||
else box.union(b);
|
||||
}
|
||||
if (isFinite(b.min.y)) tmp.union(b);
|
||||
});
|
||||
if (!tmp.isEmpty() && isFinite(tmp.min.y)) {
|
||||
if (!has) { box.copy(tmp); has = true; }
|
||||
else box.union(tmp);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!has) return;
|
||||
const target = box.min.y - 0.005;
|
||||
if (!Number.isFinite(lastY.current) || Math.abs(target - lastY.current) > 1e-4) {
|
||||
@@ -1073,6 +1078,43 @@ function GridAutoFollower() {
|
||||
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. */
|
||||
function PositionDragHandler() {
|
||||
const { camera, gl, scene } = useThree();
|
||||
@@ -1259,6 +1301,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
|
||||
<GridLayer />
|
||||
<GridAutoFollower />
|
||||
<GridCalibrationHandler />
|
||||
|
||||
<PositionDragHandler />
|
||||
<SelectionHandler />
|
||||
|
||||
@@ -22,6 +22,10 @@ export function getModelLocalGroup(modelId: string | null | undefined): THREE.Ob
|
||||
return localGroups.get(modelId);
|
||||
}
|
||||
|
||||
export function getAllModelLocalGroups(): THREE.Object3D[] {
|
||||
return Array.from(localGroups.values());
|
||||
}
|
||||
|
||||
export function worldToModelLocal(
|
||||
modelId: string | null | undefined,
|
||||
p: { x: number; y: number; z: number },
|
||||
|
||||
@@ -257,6 +257,9 @@ interface ModelStore {
|
||||
setGridAutoFollow: (b: boolean) => void;
|
||||
/** Nudge grid by delta meters and disable auto-follow. */
|
||||
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;
|
||||
setWireframeColor: (color: string) => void;
|
||||
@@ -535,6 +538,8 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
||||
gridAutoFollow: true,
|
||||
setGridAutoFollow: (gridAutoFollow) => set({ gridAutoFollow }),
|
||||
nudgeGridY: (delta) => set((s) => ({ gridY: s.gridY + delta, gridAutoFollow: false })),
|
||||
gridCalibMode: false,
|
||||
setGridCalibMode: (gridCalibMode) => set({ gridCalibMode }),
|
||||
|
||||
wireframeColor: '#00f3ff',
|
||||
setWireframeColor: (wireframeColor) => set({ wireframeColor }),
|
||||
|
||||
Reference in New Issue
Block a user