🚀 Auto-deploy: melhoria no snap e medição AR em 30/05/2026 10:42:44
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2, MousePointerClick, EyeOff, Focus, Download, Square, Scissors } from 'lucide-react';
|
import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2, MousePointerClick, EyeOff, Focus, Download, Square, Scissors, Footprints } from 'lucide-react';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
@@ -31,6 +31,7 @@ export function ViewerControls() {
|
|||||||
wireframeThickness, setWireframeThickness,
|
wireframeThickness, setWireframeThickness,
|
||||||
edgeThresholdAngle, setEdgeThresholdAngle,
|
edgeThresholdAngle, setEdgeThresholdAngle,
|
||||||
positionMode, setPositionMode,
|
positionMode, setPositionMode,
|
||||||
|
walkMode, setWalkMode,
|
||||||
activeModelId, models,
|
activeModelId, models,
|
||||||
selectionMode, setSelectionMode,
|
selectionMode, setSelectionMode,
|
||||||
selectedElementKeys, hiddenElementKeys, isolatedElementKeys,
|
selectedElementKeys, hiddenElementKeys, isolatedElementKeys,
|
||||||
@@ -268,6 +269,25 @@ export function ViewerControls() {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Walk mode */}
|
||||||
|
<Button
|
||||||
|
variant={walkMode ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 h-9"
|
||||||
|
onClick={() => {
|
||||||
|
if (!walkMode) {
|
||||||
|
toast.info("Modo Caminhada: Use WASD para se mover e o mouse para olhar ao redor. Pressione ESC para sair.");
|
||||||
|
}
|
||||||
|
setWalkMode(!walkMode);
|
||||||
|
}}
|
||||||
|
title="Modo primeira pessoa (WASD para andar)"
|
||||||
|
>
|
||||||
|
<Footprints className="h-3.5 w-3.5" />
|
||||||
|
<span className="font-mono text-xs">
|
||||||
|
Caminhar
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Measure tool */}
|
{/* Measure tool */}
|
||||||
<div className="flex items-end gap-1.5">
|
<div className="flex items-end gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Suspense, useRef, useMemo, useEffect, type ReactNode, useState } from 'react';
|
import { Suspense, useRef, useMemo, useEffect, type ReactNode, useState } from 'react';
|
||||||
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
||||||
import { OrbitControls, useGLTF, Grid, Html, Line, PerspectiveCamera, OrthographicCamera } from '@react-three/drei';
|
import { OrbitControls, useGLTF, Grid, Html, Line, PerspectiveCamera, OrthographicCamera, PointerLockControls, Sky, Environment } from '@react-three/drei';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
import { useModelStore, type SceneModel } from '@/stores/useModelStore';
|
||||||
@@ -1330,10 +1330,124 @@ function PositionDragHandler() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function WalkControls() {
|
||||||
|
const walkMode = useModelStore(s => s.walkMode);
|
||||||
|
const setWalkMode = useModelStore(s => s.setWalkMode);
|
||||||
|
const gridY = useModelStore(s => s.gridY);
|
||||||
|
const { camera } = useThree();
|
||||||
|
const controlsRef = useRef<any>(null);
|
||||||
|
const keys = useRef({ w: false, a: false, s: false, d: false, shift: false });
|
||||||
|
const initialized = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (walkMode && !initialized.current) {
|
||||||
|
camera.position.set(0, gridY + 1.7, 5);
|
||||||
|
// Give the camera an initial rotation looking forward
|
||||||
|
camera.rotation.set(0, 0, 0);
|
||||||
|
initialized.current = true;
|
||||||
|
} else if (!walkMode) {
|
||||||
|
initialized.current = false;
|
||||||
|
}
|
||||||
|
}, [walkMode, camera, gridY]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (walkMode) {
|
||||||
|
// Small timeout to allow canvas to render and be clickable
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
try { controlsRef.current?.lock(); } catch (e) {}
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [walkMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.code === 'KeyW') keys.current.w = true;
|
||||||
|
if (e.code === 'KeyA') keys.current.a = true;
|
||||||
|
if (e.code === 'KeyS') keys.current.s = true;
|
||||||
|
if (e.code === 'KeyD') keys.current.d = true;
|
||||||
|
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.current.shift = true;
|
||||||
|
};
|
||||||
|
const onKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.code === 'KeyW') keys.current.w = false;
|
||||||
|
if (e.code === 'KeyA') keys.current.a = false;
|
||||||
|
if (e.code === 'KeyS') keys.current.s = false;
|
||||||
|
if (e.code === 'KeyD') keys.current.d = false;
|
||||||
|
if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.current.shift = false;
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
window.addEventListener('keyup', onKeyUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
window.removeEventListener('keyup', onKeyUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFrame((_, dt) => {
|
||||||
|
if (!walkMode) return;
|
||||||
|
const speed = keys.current.shift ? 6.0 : 2.5;
|
||||||
|
const dir = new THREE.Vector3();
|
||||||
|
|
||||||
|
// Front vector (ignoring pitch to walk purely horizontally)
|
||||||
|
const front = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
||||||
|
front.y = 0;
|
||||||
|
if (front.lengthSq() > 0.001) front.normalize();
|
||||||
|
|
||||||
|
// Right vector
|
||||||
|
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
|
||||||
|
right.y = 0;
|
||||||
|
if (right.lengthSq() > 0.001) right.normalize();
|
||||||
|
|
||||||
|
if (keys.current.w) dir.add(front);
|
||||||
|
if (keys.current.s) dir.sub(front);
|
||||||
|
if (keys.current.a) dir.sub(right);
|
||||||
|
if (keys.current.d) dir.add(right);
|
||||||
|
|
||||||
|
if (dir.lengthSq() > 0) {
|
||||||
|
dir.normalize();
|
||||||
|
camera.position.addScaledVector(dir, speed * dt);
|
||||||
|
}
|
||||||
|
// Always stick to exactly 1.7m above ground
|
||||||
|
camera.position.y = gridY + 1.7;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!walkMode) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PointerLockControls
|
||||||
|
ref={controlsRef}
|
||||||
|
onUnlock={() => {
|
||||||
|
setWalkMode(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WalkEnvironment() {
|
||||||
|
const walkMode = useModelStore(s => s.walkMode);
|
||||||
|
const gridY = useModelStore(s => s.gridY);
|
||||||
|
if (!walkMode) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Realistic blue sky with some clouds/haze */}
|
||||||
|
<Sky sunPosition={[100, 20, 100]} turbidity={0.1} rayleigh={0.5} mieCoefficient={0.005} mieDirectionalG={0.8} />
|
||||||
|
{/* Background HDRI provides distant trees, mountains, houses on the horizon */}
|
||||||
|
<Environment preset="park" background />
|
||||||
|
{/* Infinite walkable grass floor */}
|
||||||
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, gridY - 0.01, 0]} receiveShadow>
|
||||||
|
<planeGeometry args={[1000, 1000]} />
|
||||||
|
<meshStandardMaterial color="#4ade80" roughness={0.9} metalness={0.1} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||||
const positionMode = useModelStore((s) => s.positionMode);
|
const positionMode = useModelStore((s) => s.positionMode);
|
||||||
const measureMode = useModelStore((s) => s.measureMode);
|
const measureMode = useModelStore((s) => s.measureMode);
|
||||||
const selectionMode = useModelStore((s) => s.selectionMode);
|
const selectionMode = useModelStore((s) => s.selectionMode);
|
||||||
|
const walkMode = useModelStore((s) => s.walkMode);
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
|
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
|
||||||
@@ -1377,6 +1491,9 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
|||||||
<SceneRefCapture />
|
<SceneRefCapture />
|
||||||
<ViewCubeAnimator />
|
<ViewCubeAnimator />
|
||||||
|
|
||||||
|
<WalkControls />
|
||||||
|
<WalkEnvironment />
|
||||||
|
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
makeDefault
|
makeDefault
|
||||||
enableDamping
|
enableDamping
|
||||||
@@ -1385,7 +1502,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
|||||||
maxDistance={50}
|
maxDistance={50}
|
||||||
minZoom={5}
|
minZoom={5}
|
||||||
maxZoom={5000}
|
maxZoom={5000}
|
||||||
enabled={!positionMode}
|
enabled={!positionMode && !walkMode}
|
||||||
ref={(c: any) => {
|
ref={(c: any) => {
|
||||||
mainControlsRef.current = c;
|
mainControlsRef.current = c;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ interface ModelStore {
|
|||||||
setMeasureMode: (on: boolean) => void;
|
setMeasureMode: (on: boolean) => void;
|
||||||
positionMode: boolean;
|
positionMode: boolean;
|
||||||
setPositionMode: (on: boolean) => void;
|
setPositionMode: (on: boolean) => void;
|
||||||
|
walkMode: boolean;
|
||||||
|
setWalkMode: (on: boolean) => void;
|
||||||
|
|
||||||
// ── Selection (hide/isolate/export elements) ────────────
|
// ── Selection (hide/isolate/export elements) ────────────
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
@@ -605,9 +607,14 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
|||||||
resetChecklist: () => set({ checklist: defaultChecklist.map(i => ({ ...i })) }),
|
resetChecklist: () => set({ checklist: defaultChecklist.map(i => ({ ...i })) }),
|
||||||
|
|
||||||
measureMode: false,
|
measureMode: false,
|
||||||
setMeasureMode: (measureMode) => set((s) => ({ measureMode, measurePoints: [], positionMode: measureMode ? false : s.positionMode, selectionMode: measureMode ? false : s.selectionMode })),
|
setMeasureMode: (measureMode) => set((s) => ({ measureMode, measurePoints: [], positionMode: measureMode ? false : s.positionMode, selectionMode: measureMode ? false : s.selectionMode, walkMode: false })),
|
||||||
positionMode: false,
|
positionMode: false,
|
||||||
setPositionMode: (positionMode) => set((s) => ({ positionMode, measureMode: positionMode ? false : s.measureMode, selectionMode: positionMode ? false : s.selectionMode })),
|
setPositionMode: (positionMode) => set((s) => ({ positionMode, measureMode: positionMode ? false : s.measureMode, selectionMode: positionMode ? false : s.selectionMode, walkMode: false })),
|
||||||
|
walkMode: false,
|
||||||
|
setWalkMode: (walkMode) => {
|
||||||
|
set({ walkMode });
|
||||||
|
if (walkMode) set({ positionMode: false, measureMode: false, selectionMode: false, cameraMode: 'persp' });
|
||||||
|
},
|
||||||
|
|
||||||
// ── Selection state ─────────────────────────────────────
|
// ── Selection state ─────────────────────────────────────
|
||||||
selectionMode: false,
|
selectionMode: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user