🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 23:31:10

This commit is contained in:
2026-05-24 23:31:10 +00:00
parent 45d09ab7a3
commit c953db0b26
+167 -6
View File
@@ -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 (
<XR3DPanel size={[W, H]}>
<Text position={[-W / 2 + 0.012, H / 2 - 0.018, 0.002]} fontSize={0.011} color="#3b82f6"
@@ -255,15 +257,16 @@ function FloatingPanel({
<group position={[0, H / 2 - 0.045, 0.001]}>
{tabs.map((t, i) => (
<XR3DButton key={t.id}
position={[-W / 2 + 0.04 + i * 0.078, 0, 0]} size={[0.072, 0.022]}
position={[-W / 2 + 0.038 + i * 0.072, 0, 0]} size={[0.066, 0.022]}
label={t.label} icon={t.icon} active={tab === t.id}
onClick={() => setTab(t.id)} fontSize={0.0072} />
onClick={() => setTab(t.id)} fontSize={0.0068} />
))}
</group>
<group position={[0, -0.012, 0.001]}>
<group position={[0, -0.018, 0.001]}>
{tab === 'scene' && <SceneTab />}
{tab === 'tools' && <ToolsTab {...p} />}
{tab === 'cuts' && <CutsTab />}
{tab === 'inspection' && <InspectionTab />}
{tab === 'capture' && <CaptureTab />}
{tab === 'share' && <ShareTab {...p} />}
@@ -273,6 +276,164 @@ function FloatingPanel({
);
}
interface AxisRange { min: number; max: number; }
type Axis = 'x' | 'y' | 'z';
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: '#ef4444',
y: '#10b981',
z: '#0ea5e9',
};
const AXIS_LABEL: Record<Axis, string> = { 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<Record<Axis, AxisRange> | null>(null);
const [step, setStep] = useState<number>(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 (
<group>
{/* Cabeçalho / Passo */}
<Text position={[-0.24, 0.115, 0]} fontSize={0.0085} color="#94a3b8" anchorX="left" anchorY="middle">
Passo do Ajuste:
</Text>
<XR3DButton position={[-0.09, 0.115, 0]} size={[0.048, 0.02]}
label="1 mm" active={step === 0.001} onClick={() => handleStepChange(0.001)} fontSize={0.007} />
<XR3DButton position={[-0.038, 0.115, 0]} size={[0.048, 0.02]}
label="10 mm" active={step === 0.01} onClick={() => handleStepChange(0.01)} fontSize={0.007} />
<XR3DButton position={[0.014, 0.115, 0]} size={[0.048, 0.02]}
label="50 mm" active={step === 0.05} onClick={() => handleStepChange(0.05)} fontSize={0.007} />
<XR3DButton position={[0.165, 0.115, 0]} size={[0.12, 0.02]}
label="↺ Recalcular Limites" active={false} onClick={handleRecalculate} 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 (
<group key={axis} position={[0, yOffset, 0]}>
{/* Eixo label */}
<Text position={[-0.24, 0, 0]} fontSize={0.014} color={AXIS_COLOR[axis]} anchorX="left" anchorY="middle" fontStyle="bold">
{AXIS_LABEL[axis]}
</Text>
{/* Toggle Cortar */}
<XR3DButton position={[-0.19, 0, 0]} size={[0.045, 0.022]}
label={isAxisEnabled ? "ON" : "OFF"} active={isAxisEnabled}
disabled={isAxisDisabled}
onClick={() => setSectionEnabled(axis, !isAxisEnabled)}
fontSize={0.0075} />
{/* Toggle Inverter */}
<XR3DButton position={[-0.135, 0, 0]} size={[0.052, 0.022]}
label={isAxisInverted ? "Invertido" : "Inverter"} active={isAxisInverted}
disabled={isAxisDisabled || !isAxisEnabled}
onClick={() => setSectionInvert(axis, !isAxisInverted)}
fontSize={0.007} />
{/* Valor */}
<Text position={[-0.09, 0, 0]} fontSize={0.0085} color={isAxisEnabled ? "#ffffff" : "#64748b"} anchorX="left" anchorY="middle">
{isAxisDisabled ? "sem peça" : `${valMM.toFixed(1)} mm`}
</Text>
{/* Menos/Mais */}
<XR3DButton position={[0.01, 0, 0]} size={[0.024, 0.022]}
label="" disabled={isAxisDisabled || !isAxisEnabled}
onClick={() => handleAdjust(axis, -1)}
fontSize={0.01} />
<XR3DButton position={[0.038, 0, 0]} size={[0.024, 0.022]}
label="+" disabled={isAxisDisabled || !isAxisEnabled}
onClick={() => handleAdjust(axis, 1)}
fontSize={0.01} />
{/* Presets */}
<XR3DButton position={[0.076, 0, 0]} size={[0.034, 0.022]}
label="Min" disabled={isAxisDisabled || !isAxisEnabled}
onClick={() => handleSetPreset(axis, 'min')}
fontSize={0.007} />
<XR3DButton position={[0.118, 0, 0]} size={[0.042, 0.022]}
label="Centro" disabled={isAxisDisabled || !isAxisEnabled}
onClick={() => handleSetPreset(axis, 'center')}
fontSize={0.007} />
<XR3DButton position={[0.162, 0, 0]} size={[0.036, 0.022]}
label="Max" disabled={isAxisDisabled || !isAxisEnabled}
onClick={() => handleSetPreset(axis, 'max')}
fontSize={0.007} />
</group>
);
})}
</group>
);
}
function SceneTab() {
const models = useModelStore((s) => s.models);
const activeId = useModelStore((s) => s.activeModelId);