🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 01:33:36

This commit is contained in:
2026-05-24 01:33:36 +00:00
parent b7cd422068
commit c839653a97
5 changed files with 279 additions and 403 deletions
+218 -38
View File
@@ -9,6 +9,30 @@ import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelT
import { parseIFCtoThree } from '@/lib/convertIFC';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// WebXR imersivo
import { XR, useXR, XROrigin } from '@react-three/xr';
import { xrStore } from '@/lib/xrStore';
import { toast } from 'sonner';
import {
XRBackgroundModels,
XRActiveModel,
XRMeasurementOverlay,
XRSnapHandler,
XRGrid,
XRGridAutoFollower,
} from './XRSceneComponents';
import { XRGrabbable } from './XRGrabbable';
import { XRHitTestPlacement } from './XRHitTestPlacement';
import { XRControllerMeasure } from './XRControllerMeasure';
import { ControllerLocomotion } from './ControllerLocomotion';
import { XRHudInWorld } from './XRHudInWorld';
import { XRBroadcastMirror } from './XRBroadcastMirror';
import { XRHud } from '@/components/XRHud';
import { ShareButton } from '@/components/ShareButton';
import { useDevKit } from '@/devkit/useDevKit';
import { DevPanel } from '@/devkit/DevPanel';
import { FakeControllers } from '@/devkit/FakeControllers';
interface ModelViewerProps {
url?: string; // legacy, ignored — uses store.models
}
@@ -1006,8 +1030,8 @@ function PositionDragHandler() {
const ft = active.fineTuning;
const factor = st.scaleRatio?.factor ?? 1;
if (button === 2) {
// Right button: rotate
if (button === 0) {
// Left button (Trigger in Quest emulated mouse): rotate (unifies with AR / VR standard)
const sens = 0.4; // deg per pixel
if (shiftKey) {
useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + dx * sens });
@@ -1034,8 +1058,8 @@ function PositionDragHandler() {
} else {
useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + delta });
}
} else if (button === 0 && shiftKey) {
// Shift+left: depth (Z in camera space)
} else if (button === 2 && shiftKey) {
// Shift+Right button: depth (Z in camera space)
const worldDelta = dy * pixelsPerWorldUnit;
const camDir = new THREE.Vector3();
camera.getWorldDirection(camDir);
@@ -1046,7 +1070,7 @@ function PositionDragHandler() {
posZ: ft.posZ + move.z,
});
} else {
// Left: translate in camera plane
// Right button (Grip in Quest emulated mouse): translate in camera plane (matches AR pan-only grip)
const right = new THREE.Vector3();
const up = new THREE.Vector3();
camera.matrixWorld.extractBasis(right, up, new THREE.Vector3());
@@ -1088,10 +1112,193 @@ function PositionDragHandler() {
export function ModelViewerCanvas({ url }: ModelViewerProps) {
function SceneInsideXR() {
const isPresenting = useXR((state) => state.isPresenting);
const positionMode = useModelStore((s) => s.positionMode);
const measureMode = useModelStore((s) => s.measureMode);
const selectionMode = useModelStore((s) => s.selectionMode);
const activeId = useModelStore((s) => s.activeModelId);
const models = useModelStore((s) => s.models);
const activeModel = models.find(m => m.id === activeId);
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);
// DEVKIT: simXR forces in-XR rendering on desktop without a real WebXR session
const devkit = useDevKit();
const [simXR, setSimXR] = useState(false);
const effectiveInXR = isPresenting || simXR;
// Monitorar início e fim da sessão
useEffect(() => {
if (isPresenting) {
toast.success('Sessão AR iniciada!');
}
}, [isPresenting]);
if (!effectiveInXR) {
// Modo 2D normal (não imersivo)
return (
<>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
<directionalLight position={[-5, 5, -5]} intensity={0.3} />
<Suspense fallback={<LoadingFallback />}>
<SceneModels />
</Suspense>
<MeasurementOverlay />
<MeasureClickHandler />
<SmartSnapHandler />
<HoverDetector />
<GridLayer />
<GridAutoFollower />
<PositionDragHandler />
<SelectionHandler />
<VisibilityApplier />
<SceneRefCapture />
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.1}
minDistance={0.05}
maxDistance={50}
enabled={!positionMode}
/>
</>
);
}
// Modo AR imersivo (ou SimXR)
return (
<>
<ambientLight intensity={0.8} />
<directionalLight position={[3, 5, 3]} intensity={1.2} />
<directionalLight position={[-3, 3, -3]} intensity={0.4} />
<group ref={rigRef}>
<XROrigin />
</group>
{simXR ? (
<group position={[0, 1.0, -1.2]}>
<XRBackgroundModels />
<XRGrabbable
allowScale={allowScale}
lockedActive={isActiveLocked}
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
>
<XRActiveModel />
</XRGrabbable>
</group>
) : (
<XRHitTestPlacement
placementMode={placementMode}
snapToPlanes={snapToPlanes}
onPlace={() => {
setPlacementMode(false);
toast.success('Modelo posicionado na superfície!');
}}
>
<XRBackgroundModels />
<XRGrabbable
allowScale={allowScale}
lockedActive={isActiveLocked}
onGrabStart={() => { if (placementMode) setPlacementMode(false); }}
>
<XRActiveModel />
</XRGrabbable>
</XRHitTestPlacement>
)}
<XRMeasurementOverlay />
<XRControllerMeasure />
<ControllerLocomotion rigRef={rigRef} />
<VisibilityApplier />
<SceneRefCapture />
<XRHudInWorld
freeMove={freeMove}
onToggleFreeMove={() => setFreeMove(!freeMove)}
placementMode={placementMode}
onTogglePlacement={() => setPlacementMode(!placementMode)}
snapToPlanes={snapToPlanes}
onToggleSnap={() => setSnapToPlanes(!snapToPlanes)}
allowScale={allowScale}
onToggleAllowScale={() => setAllowScale(!allowScale)}
liveCode={liveCode}
liveViewers={liveViewers}
onStartLive={() => (window as unknown as { __trackSteelStartLive?: () => void }).__trackSteelStartLive?.()}
onStopLive={() => (window as unknown as { __trackSteelStopLive?: () => void }).__trackSteelStopLive?.()}
/>
<XRBroadcastMirror enabled />
{devkit && simXR && <FakeControllers />}
{simXR && (
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.1}
minDistance={0.05}
maxDistance={50}
target={[0, 1.0, -1.2]}
/>
)}
<XRSnapHandler />
<XRGrid />
<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) {
return (
<Canvas
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
@@ -1109,36 +1316,9 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
gl.setPixelRatio(Math.min(window.devicePixelRatio, maxRatio));
}}
>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
<directionalLight position={[-5, 5, -5]} intensity={0.3} />
<Suspense fallback={<LoadingFallback />}>
<SceneModels />
</Suspense>
<MeasurementOverlay />
<MeasureClickHandler />
<SmartSnapHandler />
<HoverDetector />
<GridLayer />
<GridAutoFollower />
<PositionDragHandler />
<SelectionHandler />
<VisibilityApplier />
<SceneRefCapture />
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.1}
minDistance={0.05}
maxDistance={50}
enabled={!positionMode}
/>
<XR store={xrStore}>
<SceneInsideXR />
</XR>
</Canvas>
);
}