Anclou cotas ao frame local
X-Lovable-Edit-ID: edt-b7c3c060-0262-4347-980e-8a1ab2da97b9 Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
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';
|
||||
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
|
||||
@@ -209,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) */}
|
||||
<group position={[-modelInfo.center.x, -modelInfo.center.y, -modelInfo.center.z]}>
|
||||
{/* 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. */}
|
||||
<ModelLocalFrame modelId={sceneModel.id} center={modelInfo.center}>
|
||||
<primitive object={scene} />
|
||||
</group>
|
||||
<ModelMeasurements modelId={sceneModel.id} />
|
||||
</ModelLocalFrame>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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: ReactNode;
|
||||
}) {
|
||||
const ref = useRef<THREE.Group>(null);
|
||||
useEffect(() => {
|
||||
const g = ref.current;
|
||||
if (!g) return;
|
||||
registerModelLocalGroup(modelId, g);
|
||||
return () => unregisterModelLocalGroup(modelId, g);
|
||||
}, [modelId]);
|
||||
return (
|
||||
<group ref={ref} position={[-center.x, -center.y, -center.z]}>
|
||||
{children}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneModels() {
|
||||
const models = useModelStore((s) => s.models);
|
||||
const activeId = useModelStore((s) => s.activeModelId);
|
||||
@@ -231,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<THREE.Mesh>(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 (
|
||||
<mesh ref={ref} position={position}>
|
||||
@@ -316,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 (
|
||||
<group>
|
||||
<PointMarker position={a} color="#f59e0b" />
|
||||
<MeasurementLabel position={a} text={m.label ?? `Ø ${m.distanceMM.toFixed(1)}`} variant="accent" />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
if (m.kind === 'edge') {
|
||||
return (
|
||||
<group>
|
||||
<PointMarker position={a} color="#22c55e" />
|
||||
<PointMarker position={b} color="#22c55e" />
|
||||
<Line points={[a, b]} color="#22c55e" lineWidth={2} depthTest={false} />
|
||||
<MeasurementLabel position={mid} text={m.label ?? `${m.distanceMM.toFixed(1)} mm`} variant="success" />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<group>
|
||||
<PointMarker position={a} color="#22c55e" />
|
||||
<PointMarker position={b} color="#22c55e" />
|
||||
<Line points={[a, b]} color="#22c55e" lineWidth={2} depthTest={false} />
|
||||
<MeasurementLabel position={mid} text={`${m.distanceMM.toFixed(1)} mm`} variant="success" />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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) => (
|
||||
<MeasurementGraphic key={m.id} m={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 (
|
||||
<>
|
||||
@@ -345,55 +436,15 @@ function MeasurementOverlay() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pending first point */}
|
||||
{/* Pending first point (always in world coords) */}
|
||||
{measurePoints.length === 1 && (
|
||||
<PointMarker position={[measurePoints[0].x, measurePoints[0].y, measurePoints[0].z]} color="#e8a838" />
|
||||
)}
|
||||
|
||||
{/* 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 (
|
||||
<group key={m.id}>
|
||||
<PointMarker position={a} color="#f59e0b" />
|
||||
<MeasurementLabel position={a} text={m.label ?? `Ø ${m.distanceMM.toFixed(1)}`} variant="accent" />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
if (m.kind === 'edge') {
|
||||
return (
|
||||
<group key={m.id}>
|
||||
<PointMarker position={a} color="#22c55e" />
|
||||
<PointMarker position={b} color="#22c55e" />
|
||||
<Line points={[a, b]} color="#22c55e" lineWidth={2} depthTest={false} />
|
||||
<MeasurementLabel position={mid} text={m.label ?? `${m.distanceMM.toFixed(1)} mm`} variant="success" />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<group key={m.id}>
|
||||
<PointMarker position={a} color="#22c55e" />
|
||||
<PointMarker position={b} color="#22c55e" />
|
||||
<Line
|
||||
points={[a, b]}
|
||||
color="#22c55e"
|
||||
lineWidth={2}
|
||||
depthTest={false}
|
||||
/>
|
||||
<MeasurementLabel position={mid} text={`${m.distanceMM.toFixed(1)} mm`} variant="success" />
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
{/* Legacy world-frame measurements (no modelId) */}
|
||||
{measurements.filter((m) => !m.modelId).map((m) => (
|
||||
<MeasurementGraphic key={m.id} m={m} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, THREE.Object3D>();
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -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';
|
||||
@@ -142,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 {
|
||||
@@ -558,11 +562,17 @@ export const useModelStore = create<ModelStore>((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 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: a,
|
||||
pointB: b,
|
||||
pointA: attached ? aConv! : a,
|
||||
pointB: attached ? bConv! : b,
|
||||
distanceMM,
|
||||
modelId: attached ? modelId : undefined,
|
||||
};
|
||||
return { measurePoints: [], measurements: [...state.measurements, measurement] };
|
||||
}
|
||||
@@ -583,27 +593,40 @@ export const useModelStore = create<ModelStore>((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 };
|
||||
|
||||
Reference in New Issue
Block a user