Fine tuning controls added

Implemented the Fine Tuning system with X/Y/Z position steps of 1mm and Y rotation step of 0.1°, integrated into the 3D viewer. Added FineTuningControls component, wired to model store, updated ModelViewer to apply offsets and rotation, and inserted controls into the Viewer sidebar.

X-Lovable-Edit-ID: edt-9f467b59-61f9-45df-9093-6790b21f6d85
This commit is contained in:
gpt-engineer-app[bot]
2026-02-27 01:00:36 +00:00
3 changed files with 133 additions and 1 deletions
+116
View File
@@ -0,0 +1,116 @@
import { Move, RotateCw, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useModelStore } from '@/stores/useModelStore';
const POSITION_STEP = 0.001; // 1mm
const ROTATION_STEP = 0.1; // 0.1°
function StepButton({ label, onClick }: { label: string; onClick: () => void }) {
return (
<Button
variant="outline"
size="icon"
className="h-7 w-7 font-mono text-xs"
onClick={onClick}
>
{label}
</Button>
);
}
function AxisControl({
axis,
value,
unit,
onIncrement,
onDecrement,
displayValue,
}: {
axis: string;
value: number;
unit: string;
onIncrement: () => void;
onDecrement: () => void;
displayValue: string;
}) {
return (
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-xs text-muted-foreground w-6">{axis}</span>
<StepButton label="" onClick={onDecrement} />
<span className="font-mono text-xs text-foreground w-20 text-center">{displayValue}{unit}</span>
<StepButton label="+" onClick={onIncrement} />
</div>
);
}
export function FineTuningControls() {
const { fineTuning, setFineTuning, resetFineTuning } = useModelStore();
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-mono text-xs font-semibold uppercase tracking-widest text-muted-foreground">
Ajuste Fino
</h3>
<Button variant="ghost" size="sm" className="h-6 text-[10px] gap-1" onClick={resetFineTuning}>
<RefreshCw className="h-3 w-3" />
Resetar
</Button>
</div>
{/* Position */}
<div className="rounded-lg border bg-muted/30 p-3 space-y-2">
<div className="flex items-center gap-1.5 mb-2">
<Move className="h-3.5 w-3.5 text-primary" />
<span className="font-mono text-[10px] text-muted-foreground">Posição (±1mm)</span>
</div>
<AxisControl
axis="X"
value={fineTuning.posX}
unit="mm"
displayValue={(fineTuning.posX * 1000).toFixed(1)}
onIncrement={() => setFineTuning({ posX: fineTuning.posX + POSITION_STEP })}
onDecrement={() => setFineTuning({ posX: fineTuning.posX - POSITION_STEP })}
/>
<AxisControl
axis="Y"
value={fineTuning.posY}
unit="mm"
displayValue={(fineTuning.posY * 1000).toFixed(1)}
onIncrement={() => setFineTuning({ posY: fineTuning.posY + POSITION_STEP })}
onDecrement={() => setFineTuning({ posY: fineTuning.posY - POSITION_STEP })}
/>
<AxisControl
axis="Z"
value={fineTuning.posZ}
unit="mm"
displayValue={(fineTuning.posZ * 1000).toFixed(1)}
onIncrement={() => setFineTuning({ posZ: fineTuning.posZ + POSITION_STEP })}
onDecrement={() => setFineTuning({ posZ: fineTuning.posZ - POSITION_STEP })}
/>
</div>
{/* Rotation */}
<div className="rounded-lg border bg-muted/30 p-3 space-y-2">
<div className="flex items-center gap-1.5 mb-2">
<RotateCw className="h-3.5 w-3.5 text-primary" />
<span className="font-mono text-[10px] text-muted-foreground">Rotação Y (±0.1°)</span>
</div>
<AxisControl
axis="Y°"
value={fineTuning.rotY}
unit="°"
displayValue={fineTuning.rotY.toFixed(1)}
onIncrement={() => setFineTuning({ rotY: fineTuning.rotY + ROTATION_STEP })}
onDecrement={() => setFineTuning({ rotY: fineTuning.rotY - ROTATION_STEP })}
/>
</div>
<div className="rounded-lg border bg-muted/50 p-2">
<p className="font-mono text-[10px] text-muted-foreground">
No Quest 3: Grip + Joystick esq. = X/Y, Joystick dir. = Z/Rotação
</p>
</div>
</div>
);
}
+12 -1
View File
@@ -26,6 +26,7 @@ function GLBModel({ url }: { url: string }) {
const opacity = useModelStore((s) => s.opacity); const opacity = useModelStore((s) => s.opacity);
const renderMode = useModelStore((s) => s.renderMode); const renderMode = useModelStore((s) => s.renderMode);
const checklist = useModelStore((s) => s.checklist); const checklist = useModelStore((s) => s.checklist);
const fineTuning = useModelStore((s) => s.fineTuning);
const modelInfo = useMemo(() => { const modelInfo = useMemo(() => {
const box = new THREE.Box3().setFromObject(scene); const box = new THREE.Box3().setFromObject(scene);
@@ -71,8 +72,18 @@ function GLBModel({ url }: { url: string }) {
}); });
}, [scene, opacity, renderMode, checklist]); }, [scene, opacity, renderMode, checklist]);
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
return ( return (
<group ref={ref} position={[-modelInfo.center.x, -modelInfo.center.y, -modelInfo.center.z]}> <group
ref={ref}
position={[
-modelInfo.center.x + fineTuning.posX,
-modelInfo.center.y + fineTuning.posY,
-modelInfo.center.z + fineTuning.posZ,
]}
rotation={[0, rotYRad, 0]}
>
<primitive object={scene} /> <primitive object={scene} />
</group> </group>
); );
+5
View File
@@ -5,6 +5,7 @@ import { useModelStore } from '@/stores/useModelStore';
import { ModelViewerCanvas } from '@/components/three/ModelViewer'; import { ModelViewerCanvas } from '@/components/three/ModelViewer';
import { ViewerControls } from '@/components/ViewerControls'; import { ViewerControls } from '@/components/ViewerControls';
import { InspectionChecklist } from '@/components/InspectionChecklist'; import { InspectionChecklist } from '@/components/InspectionChecklist';
import { FineTuningControls } from '@/components/FineTuningControls';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
@@ -84,6 +85,10 @@ const Viewer = () => {
<Separator /> <Separator />
<FineTuningControls />
<Separator />
<InspectionChecklist /> <InspectionChecklist />
<div className="rounded-lg border bg-muted/50 p-3"> <div className="rounded-lg border bg-muted/50 p-3">