🚀 Auto-deploy: melhoria no snap e medição AR em 24/05/2026 02:10:48
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user