Reverted to commit c93e56640d
This commit is contained in:
@@ -3,38 +3,12 @@ import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, useGLTF, Grid, Html, Line } from '@react-three/drei';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import * as THREE from 'three';
|
||||
import { useModelStore, type SceneModel, type HoverInfo } from '@/stores/useModelStore';
|
||||
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
||||
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure';
|
||||
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
|
||||
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,
|
||||
XREnvironmentSelector,
|
||||
} from './XRSceneComponents';
|
||||
import { XRGrabbable } from './XRGrabbable';
|
||||
import { DesktopFirstPersonControls } from './DesktopFirstPersonControls';
|
||||
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
|
||||
}
|
||||
@@ -61,15 +35,6 @@ export function findElementRoot(obj: THREE.Object3D | null): THREE.Object3D | nu
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function findModelId(obj: THREE.Object3D | null): string | undefined {
|
||||
let cur: THREE.Object3D | null = obj;
|
||||
while (cur) {
|
||||
if (cur.userData?.modelId) return cur.userData.modelId as string;
|
||||
cur = cur.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasIfcAncestor(obj: THREE.Object3D): boolean {
|
||||
let cur = obj.parent;
|
||||
while (cur) {
|
||||
@@ -196,19 +161,25 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
||||
originalColors.current.set(mat, mat.color.clone());
|
||||
}
|
||||
|
||||
const isEdgesOrMeasure = renderMode === 'edges' || measureMode;
|
||||
mat.visible = true;
|
||||
const targetOpacity = isEdgesOrMeasure ? 0.25 : opacity;
|
||||
mat.transparent = targetOpacity < 1;
|
||||
mat.opacity = targetOpacity;
|
||||
mat.wireframe = false;
|
||||
if (hasRejected) {
|
||||
mat.color.setHSL(0, 0.7, 0.5);
|
||||
} else if (allApproved) {
|
||||
mat.color.setHSL(0.38, 0.7, 0.45);
|
||||
if (renderMode === 'edges' && !measureMode) {
|
||||
mat.visible = false;
|
||||
} else {
|
||||
// Tint with the per-model color (subtle)
|
||||
mat.color.set(sceneModel.color);
|
||||
mat.visible = true;
|
||||
const targetOpacity = measureMode ? 0.25 : opacity;
|
||||
mat.transparent = targetOpacity < 1;
|
||||
mat.opacity = targetOpacity;
|
||||
mat.wireframe = renderMode === 'wireframe';
|
||||
if (renderMode === 'wireframe') {
|
||||
mat.wireframeLinewidth = wireframeThickness;
|
||||
mat.color.set(wireframeColor);
|
||||
} else if (hasRejected) {
|
||||
mat.color.setHSL(0, 0.7, 0.5);
|
||||
} else if (allApproved) {
|
||||
mat.color.setHSL(0.38, 0.7, 0.45);
|
||||
} else {
|
||||
// Tint with the per-model color (subtle)
|
||||
mat.color.set(sceneModel.color);
|
||||
}
|
||||
}
|
||||
mat.needsUpdate = true;
|
||||
}
|
||||
@@ -217,8 +188,8 @@ function GLBModel({ sceneModel, isActive }: { sceneModel: SceneModel; isActive:
|
||||
if ((renderMode === 'edges' || measureMode) && child.geometry) {
|
||||
const edgesGeo = new THREE.EdgesGeometry(child.geometry, edgeThresholdAngle);
|
||||
const lineMat = new THREE.LineBasicMaterial({
|
||||
color: (renderMode === 'edges' || measureMode) ? '#00f3ff' : wireframeColor,
|
||||
linewidth: (renderMode === 'edges' || measureMode) ? 2 : wireframeThickness,
|
||||
color: measureMode ? '#00f3ff' : wireframeColor,
|
||||
linewidth: measureMode ? 2 : wireframeThickness,
|
||||
});
|
||||
const lineSegments = new THREE.LineSegments(edgesGeo, lineMat);
|
||||
lineSegments.userData.__edgeLine = true;
|
||||
@@ -384,28 +355,16 @@ function useLabelDistanceFactor(position: [number, number, number]): number {
|
||||
const dist = camera.position.distanceTo(v);
|
||||
const perspCam = camera as THREE.PerspectiveCamera;
|
||||
const fov = (perspCam.fov ?? 50) * (Math.PI / 180);
|
||||
const height = size.height || 1;
|
||||
const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / height;
|
||||
const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / size.height;
|
||||
// distanceFactor scales the html so it stays ~140 px tall at this distance
|
||||
ref.current = isNaN(worldPerPixel) || !isFinite(worldPerPixel) ? 1.5 : Math.max(0.05, worldPerPixel * 140);
|
||||
ref.current = Math.max(0.05, worldPerPixel * 140);
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function DynamicLabel({ position, text, borderClass, textClass }: { position: [number, number, number]; text: string; borderClass: string; textClass: string }) {
|
||||
const dfDynamic = useLabelDistanceFactor(position);
|
||||
return (
|
||||
<Html position={position} center distanceFactor={dfDynamic} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function MeasurementLabel({ position, text, variant, fixed = false }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' | 'accent'; fixed?: boolean }) {
|
||||
const dfDynamic = useLabelDistanceFactor(position);
|
||||
const df = fixed ? dfDynamic : 1.5;
|
||||
const borderClass = variant === 'success'
|
||||
? 'border-success/60'
|
||||
: variant === 'accent'
|
||||
@@ -416,11 +375,8 @@ function MeasurementLabel({ position, text, variant, fixed = false }: { position
|
||||
: variant === 'accent'
|
||||
? 'text-amber-400'
|
||||
: 'text-primary';
|
||||
|
||||
return fixed ? (
|
||||
<DynamicLabel position={position} text={text} borderClass={borderClass} textClass={textClass} />
|
||||
) : (
|
||||
<Html position={position} center distanceFactor={1.5} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
|
||||
return (
|
||||
<Html position={position} center distanceFactor={df} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
|
||||
<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}
|
||||
@@ -486,43 +442,6 @@ 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: HoverInfo }) {
|
||||
const position = hoverInfo.position;
|
||||
const text = 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 === 'accent'
|
||||
? 'border-amber-400/70'
|
||||
: 'border-primary/60';
|
||||
const textClass = variant === 'accent'
|
||||
? 'text-amber-400'
|
||||
: 'text-primary';
|
||||
|
||||
return (
|
||||
<Html
|
||||
position={position}
|
||||
center
|
||||
distanceFactor={dfDynamic}
|
||||
zIndexRange={[100, 0]}
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<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*
|
||||
* world-frame measurements that aren't attached to a specific model. */
|
||||
function MeasurementOverlay() {
|
||||
@@ -541,7 +460,14 @@ function MeasurementOverlay() {
|
||||
|
||||
{/* 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) */}
|
||||
@@ -630,9 +556,7 @@ function SmartSnapHandler() {
|
||||
}
|
||||
}
|
||||
if (bestHole) {
|
||||
// Find the measurement we matched
|
||||
const matched = st.measurements.find(m => m.pointA.x === bestHole.x && m.pointA.y === bestHole.y && m.pointA.z === bestHole.z);
|
||||
setSnapPoint({ ...bestHole, modelId: matched?.modelId });
|
||||
setSnapPoint(bestHole);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -643,8 +567,7 @@ function SmartSnapHandler() {
|
||||
10
|
||||
);
|
||||
if (snap) {
|
||||
const hitModelId = findModelId(hit.object);
|
||||
setSnapPoint({ x: snap.x, y: snap.y, z: snap.z, modelId: hitModelId });
|
||||
setSnapPoint({ x: snap.x, y: snap.y, z: snap.z });
|
||||
} else {
|
||||
setSnapPoint(null);
|
||||
}
|
||||
@@ -712,13 +635,11 @@ function HoverDetector() {
|
||||
|
||||
// 1) Try circle fit from edge vertices around the hit (catches holes on flat faces)
|
||||
const circ = detectCircularEdgeAtPoint(hit.object, hit.point, camera, canvasSize, 50);
|
||||
const hitModelId = findModelId(hit.object);
|
||||
if (circ) {
|
||||
setHoverInfo({
|
||||
type: 'hole',
|
||||
position: [circ.center.x, circ.center.y, circ.center.z],
|
||||
value: circ.diameterMM,
|
||||
modelId: hitModelId,
|
||||
});
|
||||
} else if (hit.faceIndex !== undefined && (() => {
|
||||
const hole = detectHoleAtFace(hit.object, hit.faceIndex);
|
||||
@@ -727,7 +648,6 @@ function HoverDetector() {
|
||||
type: 'hole',
|
||||
position: [hole.center.x, hole.center.y, hole.center.z],
|
||||
value: hole.diameterMM,
|
||||
modelId: hitModelId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -742,7 +662,6 @@ function HoverDetector() {
|
||||
type: 'edge',
|
||||
position: [edge.midpoint.x, edge.midpoint.y, edge.midpoint.z],
|
||||
value: edge.lengthMM,
|
||||
modelId: hitModelId,
|
||||
endpoints: {
|
||||
a: { x: edge.a.x, y: edge.a.y, z: edge.a.z },
|
||||
b: { x: edge.b.x, y: edge.b.y, z: edge.b.z },
|
||||
@@ -826,7 +745,7 @@ function MeasureClickHandler() {
|
||||
|
||||
// Prefer snap (vertex snap, or hole-center snap from SmartSnapHandler)
|
||||
if (st.snapPoint) {
|
||||
st.addMeasurePoint({ x: st.snapPoint.x, y: st.snapPoint.y, z: st.snapPoint.z, modelId: st.snapPoint.modelId });
|
||||
st.addMeasurePoint({ x: st.snapPoint.x, y: st.snapPoint.y, z: st.snapPoint.z });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -845,8 +764,7 @@ function MeasureClickHandler() {
|
||||
return true;
|
||||
});
|
||||
if (hit) {
|
||||
const hitModelId = findModelId(hit.object);
|
||||
st.addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z, modelId: hitModelId });
|
||||
st.addMeasurePoint({ x: hit.point.x, y: hit.point.y, z: hit.point.z });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1094,8 +1012,8 @@ function PositionDragHandler() {
|
||||
const ft = active.fineTuning;
|
||||
const factor = st.scaleRatio?.factor ?? 1;
|
||||
|
||||
if (button === 0) {
|
||||
// Left button (Trigger in Quest emulated mouse): rotate (unifies with AR / VR standard)
|
||||
if (button === 2) {
|
||||
// Right button: rotate
|
||||
const sens = 0.4; // deg per pixel
|
||||
if (shiftKey) {
|
||||
useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + dx * sens });
|
||||
@@ -1122,8 +1040,8 @@ function PositionDragHandler() {
|
||||
} else {
|
||||
useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + delta });
|
||||
}
|
||||
} else if (button === 2 && shiftKey) {
|
||||
// Shift+Right button: depth (Z in camera space)
|
||||
} else if (button === 0 && shiftKey) {
|
||||
// Shift+left: depth (Z in camera space)
|
||||
const worldDelta = dy * pixelsPerWorldUnit;
|
||||
const camDir = new THREE.Vector3();
|
||||
camera.getWorldDirection(camDir);
|
||||
@@ -1134,7 +1052,7 @@ function PositionDragHandler() {
|
||||
posZ: ft.posZ + move.z,
|
||||
});
|
||||
} else {
|
||||
// Right button (Grip in Quest emulated mouse): translate in camera plane (matches AR pan-only grip)
|
||||
// Left: translate in camera plane
|
||||
const right = new THREE.Vector3();
|
||||
const up = new THREE.Vector3();
|
||||
camera.matrixWorld.extractBasis(right, up, new THREE.Vector3());
|
||||
@@ -1176,288 +1094,57 @@ function PositionDragHandler() {
|
||||
|
||||
|
||||
|
||||
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.session);
|
||||
export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
const positionMode = useModelStore((s) => s.positionMode);
|
||||
const activeId = useModelStore((s) => s.activeModelId);
|
||||
const models = useModelStore((s) => s.models);
|
||||
const activeModel = models.find(m => m.id === activeId);
|
||||
const isActiveLocked = !!activeModel?.locked;
|
||||
const xrEnvironment = useModelStore((s) => s.xrEnvironment);
|
||||
const desktopCameraMode = useModelStore((s) => s.desktopCameraMode);
|
||||
|
||||
const rigRef = useRef<THREE.Group>(null);
|
||||
const devkit = useDevKit();
|
||||
const effectiveInXR = isPresenting || simXR;
|
||||
|
||||
useEffect(() => {
|
||||
onPresentationChange(isPresenting);
|
||||
}, [isPresenting, onPresentationChange]);
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
{xrEnvironment === 'passthrough' && (
|
||||
<>
|
||||
<ambientLight intensity={0.6} />
|
||||
<directionalLight position={[5, 10, 5]} intensity={1} castShadow />
|
||||
<directionalLight position={[-5, 5, -5]} intensity={0.3} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<XREnvironmentSelector />
|
||||
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<SceneModels />
|
||||
</Suspense>
|
||||
|
||||
<MeasurementOverlay />
|
||||
<MeasureClickHandler />
|
||||
<SmartSnapHandler />
|
||||
<HoverDetector />
|
||||
|
||||
<GridLayer />
|
||||
<GridAutoFollower />
|
||||
|
||||
<PositionDragHandler />
|
||||
<SelectionHandler />
|
||||
<VisibilityApplier />
|
||||
<SceneRefCapture />
|
||||
|
||||
{desktopCameraMode === 'orbit' ? (
|
||||
<OrbitControls
|
||||
makeDefault
|
||||
enableDamping
|
||||
dampingFactor={0.1}
|
||||
minDistance={0.05}
|
||||
maxDistance={50}
|
||||
enabled={!positionMode}
|
||||
/>
|
||||
) : (
|
||||
<DesktopFirstPersonControls />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Modo AR imersivo (ou SimXR)
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const selectionMode = useModelStore((s) => s.selectionMode);
|
||||
return (
|
||||
<>
|
||||
{xrEnvironment === 'passthrough' && (
|
||||
<>
|
||||
<ambientLight intensity={0.8} />
|
||||
<directionalLight position={[3, 5, 3]} intensity={1.2} />
|
||||
<directionalLight position={[-3, 3, -3]} intensity={0.4} />
|
||||
</>
|
||||
)}
|
||||
<Canvas
|
||||
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
|
||||
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }}
|
||||
frameloop="always"
|
||||
className="!bg-background"
|
||||
onPointerMissed={(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);
|
||||
const maxRatio = isApple ? 2 : 1.5;
|
||||
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>
|
||||
|
||||
<XREnvironmentSelector />
|
||||
<MeasurementOverlay />
|
||||
<MeasureClickHandler />
|
||||
<SmartSnapHandler />
|
||||
<HoverDetector />
|
||||
|
||||
<group ref={rigRef}>
|
||||
<XROrigin />
|
||||
</group>
|
||||
<GridLayer />
|
||||
<GridAutoFollower />
|
||||
|
||||
{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} />
|
||||
<PositionDragHandler />
|
||||
<SelectionHandler />
|
||||
<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?.()}
|
||||
<OrbitControls
|
||||
makeDefault
|
||||
enableDamping
|
||||
dampingFactor={0.1}
|
||||
minDistance={0.05}
|
||||
maxDistance={50}
|
||||
enabled={!positionMode}
|
||||
/>
|
||||
|
||||
<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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="relative w-full h-full">
|
||||
<Canvas
|
||||
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
|
||||
gl={{ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }}
|
||||
frameloop="always"
|
||||
className="!bg-background"
|
||||
onPointerMissed={(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);
|
||||
const maxRatio = isApple ? 2 : 1.5;
|
||||
gl.setPixelRatio(Math.min(window.devicePixelRatio, maxRatio));
|
||||
}}
|
||||
>
|
||||
<XR store={xrStore}>
|
||||
<SceneInsideXR
|
||||
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 className="hidden">
|
||||
<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>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user