diff --git a/src/components/SectionCutPanel.tsx b/src/components/SectionCutPanel.tsx new file mode 100644 index 0000000..7cf60cb --- /dev/null +++ b/src/components/SectionCutPanel.tsx @@ -0,0 +1,222 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { X, RefreshCw, Scissors } from 'lucide-react'; +import * as THREE from 'three'; +import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; +import { Switch } from '@/components/ui/switch'; +import { useModelStore } from '@/stores/useModelStore'; +import { getAllModelLocalGroups } from '@/lib/modelTransforms'; + +type Axis = 'x' | 'y' | 'z'; + +interface AxisRange { min: number; max: number; } + +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: 'text-red-400', + y: 'text-emerald-400', + z: 'text-sky-400', +}; + +const AXIS_LABEL: Record = { x: 'X', y: 'Y', z: 'Z' }; + +export function SectionCutPanel() { + const open = useModelStore((s) => s.sectionPanelOpen); + const opacity = useModelStore((s) => s.sectionPanelOpacity); + const setOpen = useModelStore((s) => s.setSectionPanelOpen); + const setOpacity = useModelStore((s) => s.setSectionPanelOpacity); + + 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); + + // Compute bounds when the panel opens or active model changes. + useEffect(() => { + if (!open) return; + // Defer one tick to let scene finish updating world matrices. + const id = setTimeout(() => setBounds(computeBoundsByAxis()), 50); + return () => clearTimeout(id); + }, [open, activeModelId]); + + // Drag-to-move logic. + const [pos, setPos] = useState<{ x: number; y: number }>({ x: 16, y: 16 }); + const dragRef = useRef<{ ox: number; oy: number; sx: number; sy: number } | null>(null); + useEffect(() => { + const onMove = (e: PointerEvent) => { + if (!dragRef.current) return; + setPos({ + x: Math.max(8, dragRef.current.sx + (e.clientX - dragRef.current.ox)), + y: Math.max(8, dragRef.current.sy + (e.clientY - dragRef.current.oy)), + }); + }; + const onUp = () => { dragRef.current = null; }; + window.addEventListener('pointermove', onMove); + window.addEventListener('pointerup', onUp); + return () => { + window.removeEventListener('pointermove', onMove); + window.removeEventListener('pointerup', onUp); + }; + }, []); + + const recompute = () => setBounds(computeBoundsByAxis()); + + if (!open) return null; + + return ( +
+ {/* Header (drag handle) */} +
{ + dragRef.current = { ox: e.clientX, oy: e.clientY, sx: pos.x, sy: pos.y }; + }} + > +
+ + + Cortes de Seção + +
+ +
+ + {/* Window opacity */} +
+
+ + Opacidade da janela + + {Math.round(opacity * 100)}% +
+ setOpacity(v / 100)} + /> +
+ + {/* Per-axis controls */} +
+ {(['x', 'y', 'z'] as Axis[]).map((axis) => { + const range = bounds?.[axis]; + const minMM = range ? range.min * 1000 : 0; + const maxMM = range ? range.max * 1000 : 100; + const valMM = level[axis] * 1000; + const disabled = !range; + return ( +
+
+
+ + {AXIS_LABEL[axis]} + + setSectionEnabled(axis, c)} + disabled={disabled} + /> +
+ +
+ +
+ + {disabled ? '— sem peça' : `${minMM.toFixed(0)} … ${maxMM.toFixed(0)} mm`} + + + {disabled ? '—' : `${valMM.toFixed(1)} mm`} + +
+ + setSectionLevel(axis, v / 1000)} + disabled={disabled || !enabled[axis]} + /> + +
+ + + +
+
+ ); + })} +
+ +
+ +
+
+ ); +}