From caea7f0f6f93fdcd58936830e815fc5dbaa43f03 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:48:59 +0000 Subject: [PATCH 01/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/lib/modelTransforms.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/lib/modelTransforms.ts diff --git a/src/lib/modelTransforms.ts b/src/lib/modelTransforms.ts new file mode 100644 index 0000000..58d8807 --- /dev/null +++ b/src/lib/modelTransforms.ts @@ -0,0 +1,35 @@ +import * as THREE from 'three'; + +/** + * Registry of the innermost local-frame group of each model in the scene. + * Measurements are stored in this local frame so they follow the model + * when the user repositions/rotates it via the "Posicionar" tool. + */ +const localGroups = new Map(); + +export function registerModelLocalGroup(modelId: string, obj: THREE.Object3D) { + localGroups.set(modelId, obj); +} + +export function unregisterModelLocalGroup(modelId: string, obj?: THREE.Object3D) { + // Only delete if the registered object matches (avoid race between unmount/mount of the same id) + const current = localGroups.get(modelId); + if (!obj || current === obj) localGroups.delete(modelId); +} + +export function getModelLocalGroup(modelId: string | null | undefined): THREE.Object3D | undefined { + if (!modelId) return undefined; + return localGroups.get(modelId); +} + +export function worldToModelLocal( + modelId: string | null | undefined, + p: { x: number; y: number; z: number }, +): { x: number; y: number; z: number } | null { + const g = getModelLocalGroup(modelId); + if (!g) return null; + g.updateWorldMatrix(true, false); + const v = new THREE.Vector3(p.x, p.y, p.z); + g.worldToLocal(v); + return { x: v.x, y: v.y, z: v.z }; +} From 28288fd054e05ef6dd40103842d8275049f03270 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:49:05 +0000 Subject: [PATCH 02/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 6bb5177..2927e44 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { worldToModelLocal } from '@/lib/modelTransforms'; // ── Persistência de placement (fineTuning + escala) e flags WebXR ───── const PLACEMENT_KEY = 'tsxr_placements_v1'; From 07f4b7659800bbb6915954ef04a01df380a9a9a3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:49:13 +0000 Subject: [PATCH 03/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 2927e44..7f76df5 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -143,6 +143,9 @@ export interface Measurement { kind?: 'distance' | 'hole' | 'edge'; /** Optional pre-formatted label (e.g. "Ø 12.5"). When absent, UI formats distanceMM. */ label?: string; + /** Model the measurement belongs to. When set, pointA/pointB are stored in that + * model's local frame so the measurement follows the model when repositioned. */ + modelId?: string; } export interface HoverInfo { From 2455cefa75d1cd3c31bf38729ff3b72f1b0aaf8b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:49:33 +0000 Subject: [PATCH 04/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 7f76df5..b598904 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -562,11 +562,16 @@ export const useModelStore = create((set, get) => ({ const distanceWorldMM = Math.sqrt(dx * dx + dy * dy + dz * dz); const factor = state.scaleRatio?.factor ?? 1; const distanceMM = distanceWorldMM / factor; + // Convert world points to active model's local frame so they follow the model. + const modelId = state.activeModelId ?? undefined; + const aLocal = (modelId && worldToModelLocal(modelId, a)) || a; + const bLocal = (modelId && worldToModelLocal(modelId, b)) || b; const measurement: Measurement = { id: `m_${Date.now()}`, - pointA: a, - pointB: b, + pointA: aLocal, + pointB: bLocal, distanceMM, + modelId: worldToModelLocal(modelId, a) ? modelId : undefined, }; return { measurePoints: [], measurements: [...state.measurements, measurement] }; } @@ -587,27 +592,40 @@ export const useModelStore = create((set, get) => ({ return { measurements: state.measurements.slice(0, -1) }; }), registerHoverMeasurement: (info) => set((state) => { + const modelId = state.activeModelId ?? undefined; + const toLocal = (p: MeasurePoint): { point: MeasurePoint; attached: boolean } => { + const conv = modelId ? worldToModelLocal(modelId, p) : null; + return conv ? { point: conv, attached: true } : { point: p, attached: false }; + }; let measurement: Measurement; + let attached = false; if (info.type === 'hole') { - const c: MeasurePoint = { x: info.position[0], y: info.position[1], z: info.position[2] }; + const cw: MeasurePoint = { x: info.position[0], y: info.position[1], z: info.position[2] }; + const c = toLocal(cw); + attached = c.attached; measurement = { id: `m_${Date.now()}`, - pointA: c, - pointB: c, + pointA: c.point, + pointB: c.point, distanceMM: info.value, kind: 'hole', label: `Ø ${info.value.toFixed(1)}`, + modelId: attached ? modelId : undefined, }; } else { - const a = info.endpoints?.a ?? { x: info.position[0], y: info.position[1], z: info.position[2] }; - const b = info.endpoints?.b ?? a; + const aw = info.endpoints?.a ?? { x: info.position[0], y: info.position[1], z: info.position[2] }; + const bw = info.endpoints?.b ?? aw; + const a = toLocal(aw); + const b = toLocal(bw); + attached = a.attached && b.attached; measurement = { id: `m_${Date.now()}`, - pointA: a, - pointB: b, + pointA: attached ? a.point : aw, + pointB: attached ? b.point : bw, distanceMM: info.value, kind: 'edge', label: `${info.value.toFixed(1)} mm`, + modelId: attached ? modelId : undefined, }; } return { measurements: [...state.measurements, measurement], hoverInfo: null }; From fecc7eff1ea2b60a4adbce2528439ede6997165d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:49:44 +0000 Subject: [PATCH 05/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/stores/useModelStore.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index b598904..3a15175 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -564,14 +564,15 @@ export const useModelStore = create((set, get) => ({ const distanceMM = distanceWorldMM / factor; // Convert world points to active model's local frame so they follow the model. const modelId = state.activeModelId ?? undefined; - const aLocal = (modelId && worldToModelLocal(modelId, a)) || a; - const bLocal = (modelId && worldToModelLocal(modelId, b)) || b; + const aConv = modelId ? worldToModelLocal(modelId, a) : null; + const bConv = modelId ? worldToModelLocal(modelId, b) : null; + const attached = !!(aConv && bConv); const measurement: Measurement = { id: `m_${Date.now()}`, - pointA: aLocal, - pointB: bLocal, + pointA: attached ? aConv! : a, + pointB: attached ? bConv! : b, distanceMM, - modelId: worldToModelLocal(modelId, a) ? modelId : undefined, + modelId: attached ? modelId : undefined, }; return { measurePoints: [], measurements: [...state.measurements, measurement] }; } From 3472a44a2f72701c04b0a6c5ee642d9873c7daf4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:49:58 +0000 Subject: [PATCH 06/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 62cc5e0..c507ce0 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -5,6 +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 } from '@/lib/modelTransforms'; interface ModelViewerProps { url?: string; // legacy, ignored — uses store.models From 1139337dbb796c0cba09eb3462ddb629c5854f93 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:50:13 +0000 Subject: [PATCH 07/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 34 +++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index c507ce0..2e4bde1 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -210,16 +210,44 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive: rotation={[rotXRad, rotYRad, rotZRad]} scale={[s, s, s]} > - {/* Shift geometry so its center sits at the parent's origin (pivot = center) */} - + {/* Shift geometry so its center sits at the parent's origin (pivot = center). + This is the local frame where measurement points are stored, so measurements + follow the model when posX/Y/Z or rotX/Y/Z change in "Posicionar" mode. */} + - + + ); } +/** Innermost local frame of a model. Registers itself in the global model + * registry so the store can convert world points to this group's local frame. */ +function ModelLocalFrame({ + modelId, + center, + children, +}: { + modelId: string; + center: THREE.Vector3; + children: React.ReactNode; +}) { + const ref = useRef(null); + useEffect(() => { + const g = ref.current; + if (!g) return; + registerModelLocalGroup(modelId, g); + return () => unregisterModelLocalGroup(modelId, g); + }, [modelId]); + return ( + + {children} + + ); +} + function SceneModels() { const models = useModelStore((s) => s.models); const activeId = useModelStore((s) => s.activeModelId); From e0377d49faa480cdebe94405f088335fe0670bff Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:50:28 +0000 Subject: [PATCH 08/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 2e4bde1..a46f032 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -260,19 +260,26 @@ function SceneModels() { ); } -/** Sphere marker at a 3D point — scaled per-frame to keep constant screen size */ +/** Sphere marker at a 3D point — scaled per-frame to keep constant screen size. + * Compensates for any cumulative parent world scale so it stays the same screen + * size whether mounted in world space or inside a transformed model group. */ function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) { const ref = useRef(null); const { camera, size } = useThree(); + const tmpVec = useMemo(() => new THREE.Vector3(), []); + const worldPos = useMemo(() => new THREE.Vector3(), []); useFrame(() => { if (!ref.current) return; - const dist = camera.position.distanceTo(ref.current.position); + ref.current.getWorldPosition(worldPos); + const dist = camera.position.distanceTo(worldPos); const perspCam = camera as THREE.PerspectiveCamera; const fov = (perspCam.fov ?? 50) * (Math.PI / 180); const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / size.height; - // Target: ~6 px radius (radius = 1 in base geometry → scale = 6*wpp) - const s = Math.max(0.0005, 6 * worldPerPixel); - ref.current.scale.setScalar(s); + // Target: ~6 px radius (radius = 1 in base geometry → scale = 6*wpp). + const targetWorld = Math.max(0.0005, 6 * worldPerPixel); + // Divide by parent world scale so the final world size matches `targetWorld`. + const parentScale = ref.current.parent ? ref.current.parent.getWorldScale(tmpVec).x || 1 : 1; + ref.current.scale.setScalar(targetWorld / parentScale); }); return ( From 4c9e838f934dffec7fdcab86360cc59b80e6cf87 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:51:01 +0000 Subject: [PATCH 09/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 111 +++++++++++++++------------ 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index a46f032..47f0dbe 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -352,15 +352,70 @@ function MeasurementLabel({ position, text, variant, fixed = false }: { position ); } -/** Renders all measurements, snap point, and hover info */ +/** Renders a single measurement's geometry + label. + * Coordinates are interpreted in whichever frame the component is mounted in. */ +function MeasurementGraphic({ m }: { m: import('@/stores/useModelStore').Measurement }) { + const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z]; + const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z]; + const mid: [number, number, number] = [ + (a[0] + b[0]) / 2, + (a[1] + b[1]) / 2, + (a[2] + b[2]) / 2, + ]; + + if (m.kind === 'hole') { + return ( + + + + + ); + } + + if (m.kind === 'edge') { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} + +/** Renders measurements attached to a given model, in that model's local frame. + * Mounted inside GLBModel so they follow the model when it is repositioned. */ +function ModelMeasurements({ modelId }: { modelId: string }) { + const measurements = useModelStore((s) => s.measurements); + const attached = measurements.filter((m) => m.modelId === modelId); + if (attached.length === 0) return null; + return ( + <> + {attached.map((m) => ( + + ))} + + ); +} + +/** Renders snap indicator, hover tooltip, pending point and any *legacy* + * world-frame measurements that aren't attached to a specific model. */ function MeasurementOverlay() { const measurements = useModelStore((s) => s.measurements); const measurePoints = useModelStore((s) => s.measurePoints); const snapPoint = useModelStore((s) => s.snapPoint); const hoverInfo = useModelStore((s) => s.hoverInfo); const measureMode = useModelStore((s) => s.measureMode); - const scaleRatio = useModelStore((s) => s.scaleRatio); - const factor = scaleRatio?.factor ?? 1; return ( <> @@ -381,55 +436,15 @@ function MeasurementOverlay() { /> )} - {/* Pending first point */} + {/* Pending first point (always in world coords) */} {measurePoints.length === 1 && ( )} - {/* Completed measurements */} - {measurements.map((m) => { - const a: [number, number, number] = [m.pointA.x, m.pointA.y, m.pointA.z]; - const b: [number, number, number] = [m.pointB.x, m.pointB.y, m.pointB.z]; - const mid: [number, number, number] = [ - (a[0] + b[0]) / 2, - (a[1] + b[1]) / 2, - (a[2] + b[2]) / 2, - ]; - - if (m.kind === 'hole') { - return ( - - - - - ); - } - - if (m.kind === 'edge') { - return ( - - - - - - - ); - } - - return ( - - - - - - - ); - })} + {/* Legacy world-frame measurements (no modelId) */} + {measurements.filter((m) => !m.modelId).map((m) => ( + + ))} ); } From 88cf36965465f121400114c6c747adf1e94d877e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:51:14 +0000 Subject: [PATCH 10/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 47f0dbe..b3feaed 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -1,4 +1,4 @@ -import { Suspense, useRef, useMemo, useEffect } from 'react'; +import { Suspense, useRef, useMemo, useEffect, type ReactNode } from 'react'; import { Canvas, useThree, useFrame } from '@react-three/fiber'; import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei'; import { Loader2 } from 'lucide-react'; From 1e6ef9a1b5d7be57842cc238aacb1df48db0d2f1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 20:51:19 +0000 Subject: [PATCH 11/11] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index b3feaed..494e899 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -232,7 +232,7 @@ function ModelLocalFrame({ }: { modelId: string; center: THREE.Vector3; - children: React.ReactNode; + children: ReactNode; }) { const ref = useRef(null); useEffect(() => {