Changes
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
||||||
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
||||||
ClipboardCheck, X, Crosshair,
|
ClipboardCheck, X, Crosshair, Magnet,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
@@ -19,9 +19,11 @@ interface XRHudProps {
|
|||||||
onToggleFreeMove: () => void;
|
onToggleFreeMove: () => void;
|
||||||
placementMode?: boolean;
|
placementMode?: boolean;
|
||||||
onTogglePlacement?: () => void;
|
onTogglePlacement?: () => void;
|
||||||
|
snapToPlanes?: boolean;
|
||||||
|
onToggleSnap?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTogglePlacement }: XRHudProps) {
|
export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTogglePlacement, snapToPlanes = true, onToggleSnap }: XRHudProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [checklistOpen, setChecklistOpen] = useState(false);
|
const [checklistOpen, setChecklistOpen] = useState(false);
|
||||||
|
|
||||||
@@ -161,6 +163,19 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Snap to planes toggle */}
|
||||||
|
{onToggleSnap && (
|
||||||
|
<Button
|
||||||
|
variant={snapToPlanes ? 'default' : 'outline'}
|
||||||
|
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||||
|
onClick={onToggleSnap}
|
||||||
|
title="Snap automático em planos detectados"
|
||||||
|
>
|
||||||
|
<Magnet className="h-3.5 w-3.5" />
|
||||||
|
{snapToPlanes ? 'Snap ON' : 'Snap'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={freeMove ? 'default' : 'outline'}
|
variant={freeMove ? 'default' : 'outline'}
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useRef, useState, useCallback, useEffect } from 'react';
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import { useXRHitTest, useXRRequestHitTest } from '@react-three/xr';
|
import { useXRHitTest, useXRRequestHitTest, useXRPlanes } from '@react-three/xr';
|
||||||
|
import { XRSpace } from '@react-three/xr';
|
||||||
|
import { XRPlaneModel } from '@react-three/xr';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
const matrixHelper = new THREE.Matrix4();
|
const matrixHelper = new THREE.Matrix4();
|
||||||
@@ -11,34 +13,58 @@ interface XRHitTestPlacementProps {
|
|||||||
placementMode: boolean;
|
placementMode: boolean;
|
||||||
onPlace: (position: THREE.Vector3, quaternion: THREE.Quaternion) => void;
|
onPlace: (position: THREE.Vector3, quaternion: THREE.Quaternion) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
snapToPlanes?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses @react-three/xr useXRHitTest to show a reticle on real-world surfaces.
|
* Uses @react-three/xr useXRHitTest to show a reticle on real-world surfaces.
|
||||||
|
* When snapToPlanes is true, snaps to detected planes (floor, table, wall).
|
||||||
* Tap (select) to place the model at the reticle position.
|
* Tap (select) to place the model at the reticle position.
|
||||||
*/
|
*/
|
||||||
export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTestPlacementProps) {
|
export function XRHitTestPlacement({ placementMode, onPlace, children, snapToPlanes = true }: XRHitTestPlacementProps) {
|
||||||
const reticleRef = useRef<THREE.Mesh>(null);
|
const reticleRef = useRef<THREE.Mesh>(null);
|
||||||
const [reticlePos, setReticlePos] = useState<THREE.Vector3 | null>(null);
|
const [reticlePos, setReticlePos] = useState<THREE.Vector3 | null>(null);
|
||||||
const [reticleQuat, setReticleQuat] = useState<THREE.Quaternion | null>(null);
|
const [reticleQuat, setReticleQuat] = useState<THREE.Quaternion | null>(null);
|
||||||
const [placed, setPlaced] = useState(false);
|
const [placed, setPlaced] = useState(false);
|
||||||
|
const [snappedPlaneLabel, setSnappedPlaneLabel] = useState<string | null>(null);
|
||||||
const prevPlacementMode = useRef(placementMode);
|
const prevPlacementMode = useRef(placementMode);
|
||||||
const placedPosition = useRef(new THREE.Vector3(0, 0, -1.5));
|
const placedPosition = useRef(new THREE.Vector3(0, 0, -1.5));
|
||||||
const placedQuaternion = useRef(new THREE.Quaternion());
|
const placedQuaternion = useRef(new THREE.Quaternion());
|
||||||
|
|
||||||
|
// Get detected planes for snap
|
||||||
|
const allPlanes = useXRPlanes();
|
||||||
|
|
||||||
// Continuous hit-test from the viewer (camera center)
|
// Continuous hit-test from the viewer (camera center)
|
||||||
useXRHitTest(
|
useXRHitTest(
|
||||||
placementMode && !placed
|
placementMode && !placed
|
||||||
? (results, getWorldMatrix) => {
|
? (results, getWorldMatrix) => {
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
setReticlePos(null);
|
setReticlePos(null);
|
||||||
|
setSnappedPlaneLabel(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getWorldMatrix(matrixHelper, results[0]);
|
getWorldMatrix(matrixHelper, results[0]);
|
||||||
positionHelper.setFromMatrixPosition(matrixHelper);
|
positionHelper.setFromMatrixPosition(matrixHelper);
|
||||||
quaternionHelper.setFromRotationMatrix(matrixHelper);
|
quaternionHelper.setFromRotationMatrix(matrixHelper);
|
||||||
|
|
||||||
|
// Snap to nearest detected plane if enabled
|
||||||
|
if (snapToPlanes && allPlanes.length > 0) {
|
||||||
|
const snapResult = findSnapPlane(allPlanes, positionHelper, 0.3);
|
||||||
|
if (snapResult) {
|
||||||
|
// Project hit point onto detected plane
|
||||||
|
const projected = projectOntoPlane(positionHelper, snapResult);
|
||||||
|
setReticlePos(projected);
|
||||||
|
// Align quaternion with plane normal
|
||||||
|
const alignedQuat = getPlaneAlignedQuaternion(snapResult);
|
||||||
|
setReticleQuat(alignedQuat);
|
||||||
|
setSnappedPlaneLabel(snapResult.semanticLabel ?? snapResult.orientation ?? 'surface');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setReticlePos(positionHelper.clone());
|
setReticlePos(positionHelper.clone());
|
||||||
setReticleQuat(quaternionHelper.clone());
|
setReticleQuat(quaternionHelper.clone());
|
||||||
|
setSnappedPlaneLabel(null);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
'viewer'
|
'viewer'
|
||||||
@@ -50,13 +76,13 @@ export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTe
|
|||||||
const handleSelect = useCallback(async () => {
|
const handleSelect = useCallback(async () => {
|
||||||
if (!placementMode) return;
|
if (!placementMode) return;
|
||||||
|
|
||||||
// Use current reticle position if available
|
|
||||||
if (reticlePos) {
|
if (reticlePos) {
|
||||||
placedPosition.current.copy(reticlePos);
|
placedPosition.current.copy(reticlePos);
|
||||||
if (reticleQuat) placedQuaternion.current.copy(reticleQuat);
|
if (reticleQuat) placedQuaternion.current.copy(reticleQuat);
|
||||||
setPlaced(true);
|
setPlaced(true);
|
||||||
onPlace(reticlePos.clone(), reticleQuat?.clone() ?? new THREE.Quaternion());
|
onPlace(reticlePos.clone(), reticleQuat?.clone() ?? new THREE.Quaternion());
|
||||||
console.log('[HitTest] 📍 Modelo posicionado via reticle:', reticlePos.toArray());
|
const label = snappedPlaneLabel ? ` (${snappedPlaneLabel})` : '';
|
||||||
|
console.log(`[HitTest] 📍 Modelo posicionado${label}:`, reticlePos.toArray());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +98,7 @@ export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTe
|
|||||||
setPlaced(true);
|
setPlaced(true);
|
||||||
onPlace(pos, quat);
|
onPlace(pos, quat);
|
||||||
console.log('[HitTest] 📍 Modelo posicionado via requestHitTest:', pos.toArray());
|
console.log('[HitTest] 📍 Modelo posicionado via requestHitTest:', pos.toArray());
|
||||||
}, [placementMode, reticlePos, reticleQuat, onPlace, requestHitTest]);
|
}, [placementMode, reticlePos, reticleQuat, onPlace, requestHitTest, snappedPlaneLabel]);
|
||||||
|
|
||||||
// Update reticle mesh position each frame
|
// Update reticle mesh position each frame
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
@@ -95,18 +121,30 @@ export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTe
|
|||||||
prevPlacementMode.current = placementMode;
|
prevPlacementMode.current = placementMode;
|
||||||
}, [placementMode]);
|
}, [placementMode]);
|
||||||
|
|
||||||
|
// Reticle color changes when snapped
|
||||||
|
const reticleColor = snappedPlaneLabel ? '#3b82f6' : '#22c55e';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Reticle on detected surface */}
|
{/* Reticle on detected surface */}
|
||||||
<mesh
|
<group ref={reticleRef as any} visible={false}>
|
||||||
ref={reticleRef}
|
<mesh onClick={handleSelect} pointerEventsType={{ deny: 'grab' as any }}>
|
||||||
visible={false}
|
<ringGeometry args={[0.08, 0.1, 32]} />
|
||||||
onClick={handleSelect}
|
<meshBasicMaterial color={reticleColor} side={THREE.DoubleSide} transparent opacity={0.8} />
|
||||||
pointerEventsType={{ deny: 'grab' as any }}
|
</mesh>
|
||||||
>
|
{/* Inner crosshair */}
|
||||||
<ringGeometry args={[0.08, 0.1, 32]} />
|
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
<meshBasicMaterial color="#22c55e" side={THREE.DoubleSide} transparent opacity={0.8} />
|
<ringGeometry args={[0.005, 0.015, 16]} />
|
||||||
</mesh>
|
<meshBasicMaterial color={reticleColor} side={THREE.DoubleSide} transparent opacity={0.6} />
|
||||||
|
</mesh>
|
||||||
|
{/* Snap indicator text - shows plane type */}
|
||||||
|
{snappedPlaneLabel && placementMode && !placed && (
|
||||||
|
<mesh position={[0, 0.02, 0]}>
|
||||||
|
<planeGeometry args={[0.12, 0.03]} />
|
||||||
|
<meshBasicMaterial color={reticleColor} transparent opacity={0.6} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
|
||||||
{/* Invisible tap plane to catch select events */}
|
{/* Invisible tap plane to catch select events */}
|
||||||
{placementMode && !placed && (
|
{placementMode && !placed && (
|
||||||
@@ -121,6 +159,34 @@ export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTe
|
|||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Show detected planes as semi-transparent overlays when in placement mode */}
|
||||||
|
{placementMode && !placed && snapToPlanes && allPlanes.map((plane, i) => {
|
||||||
|
const label = plane.semanticLabel ?? '';
|
||||||
|
const isVertical = plane.orientation === 'vertical';
|
||||||
|
let color = '#64748b';
|
||||||
|
if (label === 'floor' || label === 'table' || (!isVertical && label !== 'ceiling')) {
|
||||||
|
color = '#22c55e';
|
||||||
|
} else if (label === 'wall' || isVertical) {
|
||||||
|
color = '#3b82f6';
|
||||||
|
} else if (label === 'ceiling') {
|
||||||
|
color = '#a855f7';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XRSpace key={i} space={plane.planeSpace}>
|
||||||
|
<XRPlaneModel plane={plane}>
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={color}
|
||||||
|
transparent
|
||||||
|
opacity={0.12}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</XRPlaneModel>
|
||||||
|
</XRSpace>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Placed model or default position */}
|
{/* Placed model or default position */}
|
||||||
<group
|
<group
|
||||||
position={placed ? placedPosition.current : [0, 0, -1.5]}
|
position={placed ? placedPosition.current : [0, 0, -1.5]}
|
||||||
@@ -131,3 +197,73 @@ export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTe
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Find the nearest plane to a point within maxDistance */
|
||||||
|
function findSnapPlane(planes: readonly XRPlane[], point: THREE.Vector3, maxDistance: number): XRPlane | null {
|
||||||
|
let nearest: XRPlane | null = null;
|
||||||
|
let minDist = maxDistance;
|
||||||
|
|
||||||
|
for (const plane of planes) {
|
||||||
|
const polygon = plane.polygon;
|
||||||
|
if (!polygon || polygon.length === 0) continue;
|
||||||
|
|
||||||
|
// Compute plane center from polygon
|
||||||
|
let cx = 0, cy = 0, cz = 0;
|
||||||
|
for (const pt of polygon) {
|
||||||
|
cx += pt.x; cy += pt.y; cz += pt.z;
|
||||||
|
}
|
||||||
|
const len = polygon.length;
|
||||||
|
const center = new THREE.Vector3(cx / len, cy / len, cz / len);
|
||||||
|
|
||||||
|
const dist = center.distanceTo(point);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = plane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project a point onto a plane defined by its polygon */
|
||||||
|
function projectOntoPlane(point: THREE.Vector3, plane: XRPlane): THREE.Vector3 {
|
||||||
|
const polygon = plane.polygon;
|
||||||
|
if (!polygon || polygon.length < 3) return point.clone();
|
||||||
|
|
||||||
|
// Get plane normal from first 3 vertices
|
||||||
|
const p0 = new THREE.Vector3(polygon[0].x, polygon[0].y, polygon[0].z);
|
||||||
|
const p1 = new THREE.Vector3(polygon[1].x, polygon[1].y, polygon[1].z);
|
||||||
|
const p2 = new THREE.Vector3(polygon[2].x, polygon[2].y, polygon[2].z);
|
||||||
|
|
||||||
|
const normal = new THREE.Vector3().crossVectors(
|
||||||
|
new THREE.Vector3().subVectors(p1, p0),
|
||||||
|
new THREE.Vector3().subVectors(p2, p0)
|
||||||
|
).normalize();
|
||||||
|
|
||||||
|
// Project point onto plane
|
||||||
|
const toPoint = new THREE.Vector3().subVectors(point, p0);
|
||||||
|
const dist = toPoint.dot(normal);
|
||||||
|
return point.clone().sub(normal.clone().multiplyScalar(dist));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a quaternion that aligns with the plane's normal direction */
|
||||||
|
function getPlaneAlignedQuaternion(plane: XRPlane): THREE.Quaternion {
|
||||||
|
const polygon = plane.polygon;
|
||||||
|
if (!polygon || polygon.length < 3) return new THREE.Quaternion();
|
||||||
|
|
||||||
|
const p0 = new THREE.Vector3(polygon[0].x, polygon[0].y, polygon[0].z);
|
||||||
|
const p1 = new THREE.Vector3(polygon[1].x, polygon[1].y, polygon[1].z);
|
||||||
|
const p2 = new THREE.Vector3(polygon[2].x, polygon[2].y, polygon[2].z);
|
||||||
|
|
||||||
|
const normal = new THREE.Vector3().crossVectors(
|
||||||
|
new THREE.Vector3().subVectors(p1, p0),
|
||||||
|
new THREE.Vector3().subVectors(p2, p0)
|
||||||
|
).normalize();
|
||||||
|
|
||||||
|
// Create rotation from up vector to plane normal
|
||||||
|
const quat = new THREE.Quaternion();
|
||||||
|
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), normal);
|
||||||
|
return quat;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useXRPlanes, XRPlaneModel } from '@react-three/xr';
|
||||||
|
import { XRSpace } from '@react-three/xr';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
interface XRPlaneOverlayProps {
|
||||||
|
showPlanes: boolean;
|
||||||
|
highlightNearest?: THREE.Vector3 | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders semi-transparent overlays on all detected WebXR planes.
|
||||||
|
* Walls are tinted blue, floors/ceilings green, others grey.
|
||||||
|
*/
|
||||||
|
export function XRPlaneOverlay({ showPlanes, highlightNearest }: XRPlaneOverlayProps) {
|
||||||
|
const allPlanes = useXRPlanes();
|
||||||
|
|
||||||
|
if (!showPlanes || allPlanes.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{allPlanes.map((plane, i) => {
|
||||||
|
const label = plane.semanticLabel ?? '';
|
||||||
|
const isHorizontal = plane.orientation === 'horizontal';
|
||||||
|
const isVertical = plane.orientation === 'vertical';
|
||||||
|
|
||||||
|
// Color by type
|
||||||
|
let color = '#64748b'; // default grey
|
||||||
|
if (label === 'floor' || label === 'table' || (isHorizontal && label !== 'ceiling')) {
|
||||||
|
color = '#22c55e'; // green for floor/table/horizontal
|
||||||
|
} else if (label === 'wall' || isVertical) {
|
||||||
|
color = '#3b82f6'; // blue for walls
|
||||||
|
} else if (label === 'ceiling') {
|
||||||
|
color = '#a855f7'; // purple for ceiling
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<XRSpace key={i} space={plane.planeSpace}>
|
||||||
|
<XRPlaneModel plane={plane}>
|
||||||
|
<meshBasicMaterial
|
||||||
|
color={color}
|
||||||
|
transparent
|
||||||
|
opacity={0.15}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</XRPlaneModel>
|
||||||
|
</XRSpace>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to find the nearest detected plane to a given point.
|
||||||
|
* Returns the plane's center position and orientation quaternion.
|
||||||
|
*/
|
||||||
|
export function useNearestPlane(
|
||||||
|
targetPoint: THREE.Vector3 | null,
|
||||||
|
maxDistance = 0.5
|
||||||
|
): { position: THREE.Vector3; quaternion: THREE.Quaternion; plane: XRPlane } | null {
|
||||||
|
const allPlanes = useXRPlanes();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!targetPoint || allPlanes.length === 0) return null;
|
||||||
|
|
||||||
|
let nearest: { position: THREE.Vector3; quaternion: THREE.Quaternion; plane: XRPlane } | null = null;
|
||||||
|
let minDist = maxDistance;
|
||||||
|
|
||||||
|
for (const plane of allPlanes) {
|
||||||
|
// Use the plane's polygon center as approximation
|
||||||
|
const polygon = plane.polygon;
|
||||||
|
if (!polygon || polygon.length === 0) continue;
|
||||||
|
|
||||||
|
const center = new THREE.Vector3();
|
||||||
|
for (const pt of polygon) {
|
||||||
|
center.add(new THREE.Vector3(pt.x, pt.y, pt.z));
|
||||||
|
}
|
||||||
|
center.divideScalar(polygon.length);
|
||||||
|
|
||||||
|
const dist = center.distanceTo(targetPoint);
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
nearest = {
|
||||||
|
position: center.clone(),
|
||||||
|
quaternion: new THREE.Quaternion(),
|
||||||
|
plane,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}, [targetPoint, allPlanes, maxDistance]);
|
||||||
|
}
|
||||||
@@ -317,6 +317,7 @@ const XRSession = () => {
|
|||||||
const [inXR, setInXR] = useState(false);
|
const [inXR, setInXR] = useState(false);
|
||||||
const [freeMove, setFreeMove] = useState(true);
|
const [freeMove, setFreeMove] = useState(true);
|
||||||
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
|
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
|
||||||
|
const [snapToPlanes, setSnapToPlanes] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!model) navigate('/');
|
if (!model) navigate('/');
|
||||||
@@ -410,6 +411,7 @@ const XRSession = () => {
|
|||||||
|
|
||||||
<XRHitTestPlacement
|
<XRHitTestPlacement
|
||||||
placementMode={placementMode}
|
placementMode={placementMode}
|
||||||
|
snapToPlanes={snapToPlanes}
|
||||||
onPlace={() => {
|
onPlace={() => {
|
||||||
setPlacementMode(false);
|
setPlacementMode(false);
|
||||||
toast.success('Modelo posicionado na superfície!');
|
toast.success('Modelo posicionado na superfície!');
|
||||||
@@ -431,6 +433,8 @@ const XRSession = () => {
|
|||||||
onToggleFreeMove={() => setFreeMove(!freeMove)}
|
onToggleFreeMove={() => setFreeMove(!freeMove)}
|
||||||
placementMode={placementMode}
|
placementMode={placementMode}
|
||||||
onTogglePlacement={() => setPlacementMode(!placementMode)}
|
onTogglePlacement={() => setPlacementMode(!placementMode)}
|
||||||
|
snapToPlanes={snapToPlanes}
|
||||||
|
onToggleSnap={() => setSnapToPlanes(!snapToPlanes)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user