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

This commit is contained in:
2026-05-24 02:10:48 +00:00
parent c8d51e0e1b
commit 6046d655df
+171 -77
View File
@@ -469,6 +469,52 @@ function ModelMeasurements({ modelId }: { modelId: string }) {
); );
} }
/** Renders snap indicator, hover tooltip, pending point and any *legacy*
* world-frame measurements that aren't attached to a specific model. */
function HoverLabelOverlay({ hoverInfo }: { hoverInfo: any }) {
const visible = !!hoverInfo;
const position = hoverInfo?.position ?? [0, 0, 0];
const text = hoverInfo
? hoverInfo.type === 'hole'
? `Ø ${hoverInfo.value.toFixed(1)}`
: `${hoverInfo.value.toFixed(1)} mm`
: '';
const variant = hoverInfo?.type === 'hole' ? 'accent' : 'primary';
const dfDynamic = useLabelDistanceFactor(position);
const borderClass = variant === 'success'
? 'border-success/60'
: variant === 'accent'
? 'border-amber-400/70'
: 'border-primary/60';
const textClass = variant === 'success'
? 'text-success'
: variant === 'accent'
? 'text-amber-400'
: 'text-primary';
return (
<Html
position={position}
center
distanceFactor={dfDynamic}
zIndexRange={[100, 0]}
style={{
pointerEvents: 'none',
display: visible ? 'block' : 'none',
opacity: visible ? 1 : 0,
}}
>
<div className={`rounded bg-card/95 border ${borderClass} px-2 py-0.5 shadow-lg backdrop-blur-sm`}>
<span className={`font-mono text-[11px] font-bold ${textClass} whitespace-nowrap`}>
{text}
</span>
</div>
</Html>
);
}
/** Renders snap indicator, hover tooltip, pending point and any *legacy* /** Renders snap indicator, hover tooltip, pending point and any *legacy*
* world-frame measurements that aren't attached to a specific model. */ * world-frame measurements that aren't attached to a specific model. */
function MeasurementOverlay() { function MeasurementOverlay() {
@@ -486,16 +532,7 @@ function MeasurementOverlay() {
)} )}
{/* Hover auto-detect tooltip (works both inside and outside measure mode) */} {/* Hover auto-detect tooltip (works both inside and outside measure mode) */}
{hoverInfo && ( <HoverLabelOverlay hoverInfo={hoverInfo} />
<MeasurementLabel
position={hoverInfo.position}
text={hoverInfo.type === 'hole'
? `Ø ${hoverInfo.value.toFixed(1)}`
: `${hoverInfo.value.toFixed(1)} mm`}
variant={hoverInfo.type === 'hole' ? 'accent' : 'primary'}
fixed
/>
)}
{/* Pending first point (always in world coords) */} {/* Pending first point (always in world coords) */}
{measurePoints.length === 1 && ( {measurePoints.length === 1 && (
@@ -1129,7 +1166,41 @@ function PositionDragHandler() {
function SceneInsideXR() { interface SceneInsideXRProps {
placementMode: boolean;
setPlacementMode: (v: boolean) => void;
snapToPlanes: boolean;
setSnapToPlanes: (v: boolean) => void;
allowScale: boolean;
setAllowScale: (v: boolean) => void;
liveCode: string | null;
setLiveCode: (v: string | null) => void;
liveViewers: number;
setLiveViewers: (v: number) => void;
freeMove: boolean;
setFreeMove: (v: boolean) => void;
simXR: boolean;
setSimXR: (v: boolean) => void;
onPresentationChange: (v: boolean) => void;
}
function SceneInsideXR({
placementMode,
setPlacementMode,
snapToPlanes,
setSnapToPlanes,
allowScale,
setAllowScale,
liveCode,
setLiveCode,
liveViewers,
setLiveViewers,
freeMove,
setFreeMove,
simXR,
setSimXR,
onPresentationChange,
}: SceneInsideXRProps) {
const isPresenting = useXR((state) => state.isPresenting); const isPresenting = useXR((state) => state.isPresenting);
const positionMode = useModelStore((s) => s.positionMode); const positionMode = useModelStore((s) => s.positionMode);
const activeId = useModelStore((s) => s.activeModelId); const activeId = useModelStore((s) => s.activeModelId);
@@ -1137,21 +1208,14 @@ function SceneInsideXR() {
const activeModel = models.find(m => m.id === activeId); const activeModel = models.find(m => m.id === activeId);
const isActiveLocked = !!activeModel?.locked; const isActiveLocked = !!activeModel?.locked;
// Estados locais para a sessão de XR
const [freeMove, setFreeMove] = useState(true);
const [placementMode, setPlacementMode] = useState(true);
const [snapToPlanes, setSnapToPlanes] = useState(true);
const [allowScale, setAllowScale] = useState(true);
const [liveCode, setLiveCode] = useState<string | null>(null);
const [liveViewers, setLiveViewers] = useState(0);
const rigRef = useRef<THREE.Group>(null); const rigRef = useRef<THREE.Group>(null);
// DEVKIT: simXR forces in-XR rendering on desktop without a real WebXR session
const devkit = useDevKit(); const devkit = useDevKit();
const [simXR, setSimXR] = useState(false);
const effectiveInXR = isPresenting || simXR; const effectiveInXR = isPresenting || simXR;
useEffect(() => {
onPresentationChange(isPresenting);
}, [isPresenting, onPresentationChange]);
// Monitorar início e fim da sessão // Monitorar início e fim da sessão
useEffect(() => { useEffect(() => {
if (isPresenting) { if (isPresenting) {
@@ -1276,66 +1340,96 @@ function SceneInsideXR() {
<XRSnapHandler /> <XRSnapHandler />
<XRGrid /> <XRGrid />
<XRGridAutoFollower /> <XRGridAutoFollower />
{/* Floating HUD overlay - visível apenas fora do headset passthrough */}
<Html fullscreen style={{ pointerEvents: 'none', display: isPresenting ? 'none' : 'contents' }}>
<div style={{ pointerEvents: 'auto' }}>
<XRHud
freeMove={freeMove}
onToggleFreeMove={() => setFreeMove(!freeMove)}
placementMode={placementMode}
onTogglePlacement={() => setPlacementMode(!placementMode)}
snapToPlanes={snapToPlanes}
onToggleSnap={() => setSnapToPlanes(!snapToPlanes)}
allowScale={allowScale}
onToggleAllowScale={() => setAllowScale(!allowScale)}
/>
</div>
</Html>
{/* Compartilhar botões ocultos para WebRTC e HUD de fora */}
<Html style={{ display: 'none' }}>
<div>
<ShareButton
onHandleChange={(h) => setLiveCode(h?.code ?? null)}
onViewerCountChange={setLiveViewers}
/>
</div>
</Html>
{/* DevPanel se devkit habilitado */}
{devkit && (
<Html fullscreen style={{ pointerEvents: 'none' }}>
<div style={{ pointerEvents: 'auto' }}>
<DevPanel simXR={simXR} onToggleSimXR={() => setSimXR((v) => !v)} />
</div>
</Html>
)}
</> </>
); );
} }
export function ModelViewerCanvas({ url }: ModelViewerProps) { export function ModelViewerCanvas({ url }: ModelViewerProps) {
const [placementMode, setPlacementMode] = useState(true);
const [snapToPlanes, setSnapToPlanes] = useState(true);
const [allowScale, setAllowScale] = useState(true);
const [liveCode, setLiveCode] = useState<string | null>(null);
const [liveViewers, setLiveViewers] = useState(0);
const [freeMove, setFreeMove] = useState(true);
const [simXR, setSimXR] = useState(false);
const [isPresenting, setIsPresenting] = useState(false);
const devkit = useDevKit();
const effectiveInXR = isPresenting || simXR;
return ( return (
<Canvas <div className="relative w-full h-full">
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }} <Canvas
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }} camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
frameloop="always" gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }}
className="!bg-background" frameloop="always"
onPointerMissed={(e) => { className="!bg-background"
if (useModelStore.getState().selectionMode) { onPointerMissed={(e) => {
(e.target as HTMLCanvasElement).dispatchEvent(new CustomEvent('tsxr-selection-miss', { detail: e })); if (useModelStore.getState().selectionMode) {
} (e.target as HTMLCanvasElement).dispatchEvent(new CustomEvent('tsxr-selection-miss', { detail: e }));
}} }
onCreated={({ gl }) => { }}
const isApple = /Mac|iPhone|iPad/.test(navigator.platform); onCreated={({ gl }) => {
const maxRatio = isApple ? 2 : 1.5; const isApple = /Mac|iPhone|iPad/.test(navigator.platform);
gl.setPixelRatio(Math.min(window.devicePixelRatio, maxRatio)); const maxRatio = isApple ? 2 : 1.5;
}} gl.setPixelRatio(Math.min(window.devicePixelRatio, maxRatio));
> }}
<XR store={xrStore}> >
<SceneInsideXR /> <XR store={xrStore}>
</XR> <SceneInsideXR
</Canvas> placementMode={placementMode}
setPlacementMode={setPlacementMode}
snapToPlanes={snapToPlanes}
setSnapToPlanes={setSnapToPlanes}
allowScale={allowScale}
setAllowScale={setAllowScale}
liveCode={liveCode}
setLiveCode={setLiveCode}
liveViewers={liveViewers}
setLiveViewers={setLiveViewers}
freeMove={freeMove}
setFreeMove={setFreeMove}
simXR={simXR}
setSimXR={setSimXR}
onPresentationChange={setIsPresenting}
/>
</XR>
</Canvas>
{/* Floating HUD overlay - visível apenas fora do headset passthrough */}
{effectiveInXR && !isPresenting && (
<div className="absolute inset-0 pointer-events-none z-30">
<div className="pointer-events-auto">
<XRHud
freeMove={freeMove}
onToggleFreeMove={() => setFreeMove(!freeMove)}
placementMode={placementMode}
onTogglePlacement={() => setPlacementMode(!placementMode)}
snapToPlanes={snapToPlanes}
onToggleSnap={() => setSnapToPlanes(!snapToPlanes)}
allowScale={allowScale}
onToggleAllowScale={() => setAllowScale(!allowScale)}
/>
</div>
</div>
)}
{/* Compartilhar botões ocultos para WebRTC e HUD de fora */}
<div style={{ display: 'none' }}>
<ShareButton
onHandleChange={(h) => setLiveCode(h?.code ?? null)}
onViewerCountChange={setLiveViewers}
/>
</div>
{/* DevPanel se devkit habilitado */}
{devkit && (
<div className="absolute inset-0 pointer-events-none z-40">
<div className="pointer-events-auto">
<DevPanel simXR={simXR} onToggleSimXR={() => setSimXR((v) => !v)} />
</div>
</div>
)}
</div>
); );
} }