Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user