🚀 Auto-deploy: melhoria no snap e medição AR em 30/05/2026 10:42:44

This commit is contained in:
2026-05-30 10:42:44 +00:00
parent b6912bd070
commit 54a3f83375
3 changed files with 149 additions and 5 deletions
+21 -1
View File
@@ -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
+119 -2
View File
@@ -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;
}}
+9 -2
View File
@@ -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,