🚀 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 { Button } from '@/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
@@ -31,6 +31,7 @@ export function ViewerControls() {
|
||||
wireframeThickness, setWireframeThickness,
|
||||
edgeThresholdAngle, setEdgeThresholdAngle,
|
||||
positionMode, setPositionMode,
|
||||
walkMode, setWalkMode,
|
||||
activeModelId, models,
|
||||
selectionMode, setSelectionMode,
|
||||
selectedElementKeys, hiddenElementKeys, isolatedElementKeys,
|
||||
@@ -268,6 +269,25 @@ export function ViewerControls() {
|
||||
</span>
|
||||
</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 */}
|
||||
<div className="flex items-end gap-1.5">
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense, useRef, useMemo, useEffect, type ReactNode, useState } from 'react';
|
||||
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 * as THREE from 'three';
|
||||
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) {
|
||||
const positionMode = useModelStore((s) => s.positionMode);
|
||||
const measureMode = useModelStore((s) => s.measureMode);
|
||||
const selectionMode = useModelStore((s) => s.selectionMode);
|
||||
const walkMode = useModelStore((s) => s.walkMode);
|
||||
return (
|
||||
<Canvas
|
||||
camera={{ position: [2, 2, 2], fov: 50, near: 0.001, far: 1000 }}
|
||||
@@ -1377,6 +1491,9 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
<SceneRefCapture />
|
||||
<ViewCubeAnimator />
|
||||
|
||||
<WalkControls />
|
||||
<WalkEnvironment />
|
||||
|
||||
<OrbitControls
|
||||
makeDefault
|
||||
enableDamping
|
||||
@@ -1385,7 +1502,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||
maxDistance={50}
|
||||
minZoom={5}
|
||||
maxZoom={5000}
|
||||
enabled={!positionMode}
|
||||
enabled={!positionMode && !walkMode}
|
||||
ref={(c: any) => {
|
||||
mainControlsRef.current = c;
|
||||
}}
|
||||
|
||||
@@ -298,6 +298,8 @@ interface ModelStore {
|
||||
setMeasureMode: (on: boolean) => void;
|
||||
positionMode: boolean;
|
||||
setPositionMode: (on: boolean) => void;
|
||||
walkMode: boolean;
|
||||
setWalkMode: (on: boolean) => void;
|
||||
|
||||
// ── Selection (hide/isolate/export elements) ────────────
|
||||
selectionMode: boolean;
|
||||
@@ -605,9 +607,14 @@ export const useModelStore = create<ModelStore>((set, get) => ({
|
||||
resetChecklist: () => set({ checklist: defaultChecklist.map(i => ({ ...i })) }),
|
||||
|
||||
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,
|
||||
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 ─────────────────────────────────────
|
||||
selectionMode: false,
|
||||
|
||||
Reference in New Issue
Block a user