Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-24 22:53:59 +00:00
parent 7208e2bc56
commit 1222d284e3
+222
View File
@@ -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<Axis, AxisRange> | 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<Axis, string> = {
x: 'text-red-400',
y: 'text-emerald-400',
z: 'text-sky-400',
};
const AXIS_LABEL: Record<Axis, string> = { 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<Record<Axis, AxisRange> | 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 (
<div
className="absolute z-30 w-[320px] rounded-lg border bg-card shadow-2xl backdrop-blur-md"
style={{ right: pos.x, top: pos.y, opacity }}
>
{/* Header (drag handle) */}
<div
className="flex cursor-move items-center justify-between gap-2 border-b px-3 py-2 select-none"
onPointerDown={(e) => {
dragRef.current = { ox: e.clientX, oy: e.clientY, sx: pos.x, sy: pos.y };
}}
>
<div className="flex items-center gap-2">
<Scissors className="h-3.5 w-3.5 text-primary" />
<span className="font-mono text-xs font-semibold uppercase tracking-widest text-foreground">
Cortes de Seção
</span>
</div>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setOpen(false)}>
<X className="h-3.5 w-3.5" />
</Button>
</div>
{/* Window opacity */}
<div className="border-b px-3 py-2">
<div className="mb-1 flex items-center justify-between">
<span className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">
Opacidade da janela
</span>
<span className="font-mono text-[10px] text-foreground">{Math.round(opacity * 100)}%</span>
</div>
<Slider
value={[Math.round(opacity * 100)]}
min={20}
max={100}
step={5}
onValueChange={([v]) => setOpacity(v / 100)}
/>
</div>
{/* Per-axis controls */}
<div className="space-y-3 px-3 py-3">
{(['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 (
<div key={axis} className="rounded-md border bg-muted/30 p-2.5">
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={`font-mono text-sm font-bold ${AXIS_COLOR[axis]}`}>
{AXIS_LABEL[axis]}
</span>
<Switch
checked={enabled[axis]}
onCheckedChange={(c) => setSectionEnabled(axis, c)}
disabled={disabled}
/>
</div>
<label className="flex items-center gap-1.5">
<span className="font-mono text-[10px] text-muted-foreground">Inverter</span>
<Switch
checked={invert[axis]}
onCheckedChange={(c) => setSectionInvert(axis, c)}
disabled={disabled || !enabled[axis]}
/>
</label>
</div>
<div className="mb-1 flex items-center justify-between">
<span className="font-mono text-[10px] text-muted-foreground">
{disabled ? '— sem peça' : `${minMM.toFixed(0)}${maxMM.toFixed(0)} mm`}
</span>
<span className="font-mono text-[10px] text-foreground">
{disabled ? '—' : `${valMM.toFixed(1)} mm`}
</span>
</div>
<Slider
value={[range ? Math.min(maxMM, Math.max(minMM, valMM)) : 0]}
min={minMM}
max={maxMM}
step={Math.max(0.1, (maxMM - minMM) / 1000)}
onValueChange={([v]) => setSectionLevel(axis, v / 1000)}
disabled={disabled || !enabled[axis]}
/>
<div className="mt-2 grid grid-cols-3 gap-1">
<Button
variant="ghost"
size="sm"
className="h-6 font-mono text-[10px]"
disabled={disabled || !enabled[axis]}
onClick={() => range && setSectionLevel(axis, range.min)}
>
Min
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 font-mono text-[10px]"
disabled={disabled || !enabled[axis]}
onClick={() => range && setSectionLevel(axis, (range.min + range.max) / 2)}
>
Centro
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 font-mono text-[10px]"
disabled={disabled || !enabled[axis]}
onClick={() => range && setSectionLevel(axis, range.max)}
>
Max
</Button>
</div>
</div>
);
})}
</div>
<div className="border-t px-3 py-2">
<Button variant="outline" size="sm" className="w-full gap-2 h-8" onClick={recompute}>
<RefreshCw className="h-3.5 w-3.5" />
<span className="font-mono text-[11px]">Recalcular limites</span>
</Button>
</div>
</div>
);
}