diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index 9366008..8f18227 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, forwardRef } from 'react'; +import { useRef, useState, useEffect, forwardRef } from 'react'; import { useFrame, useThree } from '@react-three/fiber'; import { useXRInputSourceState, useXR } from '@react-three/xr'; import { useNavigate } from 'react-router-dom'; @@ -11,8 +11,9 @@ import { useCanvasRecorder, startRecording, pauseRecording, resumeRecording, stopRecording, captureScreenshot, formatElapsed, } from '@/hooks/useCanvasRecorder'; +import { getAllModelLocalGroups } from '@/lib/modelTransforms'; -type Tab = 'scene' | 'tools' | 'inspection' | 'share' | 'capture' | 'webxr'; +type Tab = 'scene' | 'tools' | 'cuts' | 'inspection' | 'share' | 'capture' | 'webxr'; interface XRHudInWorldProps { freeMove: boolean; @@ -224,12 +225,13 @@ function FloatingPanel({ const tabs: { id: Tab; label: string; icon: string }[] = [ { id: 'scene', label: 'Cena', icon: '🧩' }, { id: 'tools', label: 'Ferram.', icon: '🛠' }, + { id: 'cuts', label: 'Cortes', icon: '✂' }, { id: 'inspection', label: 'Inspeção', icon: '✓' }, { id: 'capture', label: 'Captura', icon: '🎥' }, { id: 'share', label: 'Compart.', icon: '📡' }, { id: 'webxr', label: 'WebXR', icon: '⚙' }, ]; - const W = 0.5, H = 0.36; + const W = 0.54, H = 0.38; return ( {tabs.map((t, i) => ( setTab(t.id)} fontSize={0.0072} /> + onClick={() => setTab(t.id)} fontSize={0.0068} /> ))} - + {tab === 'scene' && } {tab === 'tools' && } + {tab === 'cuts' && } {tab === 'inspection' && } {tab === 'capture' && } {tab === 'share' && } @@ -273,6 +276,164 @@ function FloatingPanel({ ); } +interface AxisRange { min: number; max: number; } +type Axis = 'x' | 'y' | 'z'; + +function computeBoundsByAxis(): Record | null { + const groups = getAllModelLocalGroups(); + if (groups.length === 0) return null; + const box = new THREE.Box3(); + let has = false; + for (const g of groups) { + g.updateWorldMatrix(true, true); + const b = new THREE.Box3().setFromObject(g); + if (Number.isFinite(b.min.x) && Number.isFinite(b.max.x)) { + if (!has) { box.copy(b); has = true; } else box.union(b); + } + } + if (!has) return null; + return { + x: { min: box.min.x, max: box.max.x }, + y: { min: box.min.y, max: box.max.y }, + z: { min: box.min.z, max: box.max.z }, + }; +} + +const AXIS_COLOR: Record = { + x: '#ef4444', + y: '#10b981', + z: '#0ea5e9', +}; + +const AXIS_LABEL: Record = { x: 'X', y: 'Y', z: 'Z' }; + +function CutsTab() { + const enabled = useModelStore((s) => s.sectionEnabled); + const invert = useModelStore((s) => s.sectionInvert); + const level = useModelStore((s) => s.sectionLevel); + const setSectionEnabled = useModelStore((s) => s.setSectionEnabled); + const setSectionInvert = useModelStore((s) => s.setSectionInvert); + const setSectionLevel = useModelStore((s) => s.setSectionLevel); + const activeModelId = useModelStore((s) => s.activeModelId); + + const [bounds, setBounds] = useState | null>(null); + const [step, setStep] = useState(0.01); // 10mm por padrão + + useEffect(() => { + setBounds(computeBoundsByAxis()); + }, [activeModelId]); + + const handleStepChange = (sVal: number) => { + setStep(sVal); + }; + + const handleAdjust = (axis: Axis, dir: 1 | -1) => { + const range = bounds?.[axis]; + if (!range) return; + const currentVal = level[axis]; + const newVal = Math.max(range.min, Math.min(range.max, currentVal + dir * step)); + setSectionLevel(axis, newVal); + }; + + const handleSetPreset = (axis: Axis, type: 'min' | 'center' | 'max') => { + const range = bounds?.[axis]; + if (!range) return; + if (type === 'min') { + setSectionLevel(axis, range.min); + } else if (type === 'center') { + setSectionLevel(axis, (range.min + range.max) / 2); + } else if (type === 'max') { + setSectionLevel(axis, range.max); + } + }; + + const handleRecalculate = () => { + setBounds(computeBoundsByAxis()); + }; + + return ( + + {/* Cabeçalho / Passo */} + + Passo do Ajuste: + + handleStepChange(0.001)} fontSize={0.007} /> + handleStepChange(0.01)} fontSize={0.007} /> + handleStepChange(0.05)} fontSize={0.007} /> + + + + {/* Controles dos Eixos */} + {(['x', 'y', 'z'] as Axis[]).map((axis, idx) => { + const yOffset = 0.055 - idx * 0.055; + const range = bounds?.[axis]; + const minMM = range ? range.min * 1000 : 0; + const maxMM = range ? range.max * 1000 : 100; + const valMM = level[axis] * 1000; + const isAxisEnabled = enabled[axis]; + const isAxisInverted = invert[axis]; + const isAxisDisabled = !range; + + return ( + + {/* Eixo label */} + + {AXIS_LABEL[axis]} + + + {/* Toggle Cortar */} + setSectionEnabled(axis, !isAxisEnabled)} + fontSize={0.0075} /> + + {/* Toggle Inverter */} + setSectionInvert(axis, !isAxisInverted)} + fontSize={0.007} /> + + {/* Valor */} + + {isAxisDisabled ? "sem peça" : `${valMM.toFixed(1)} mm`} + + + {/* Menos/Mais */} + handleAdjust(axis, -1)} + fontSize={0.01} /> + handleAdjust(axis, 1)} + fontSize={0.01} /> + + {/* Presets */} + handleSetPreset(axis, 'min')} + fontSize={0.007} /> + handleSetPreset(axis, 'center')} + fontSize={0.007} /> + handleSetPreset(axis, 'max')} + fontSize={0.007} /> + + ); + })} + + ); +} + function SceneTab() { const models = useModelStore((s) => s.models); const activeId = useModelStore((s) => s.activeModelId);