🚀 Auto-deploy: BrainWind atualizado em 12/07/2026 17:11:46
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const dir = path.join(__dirname, 'app/src/components/three');
|
||||||
|
const pagesDir = path.join(__dirname, 'app/src/pages');
|
||||||
|
|
||||||
|
// For components that return <SceneCanvas> inside them
|
||||||
|
const componentsToUpdate = [
|
||||||
|
{ file: 'app/src/components/three/Sign3D.tsx', id: 'muro_placa' },
|
||||||
|
{ file: 'app/src/components/three/IsolatedRoof3D.tsx', id: 'cobertura_isolada' },
|
||||||
|
{ file: 'app/src/components/three/Bridge3D.tsx', id: 'ponte' },
|
||||||
|
{ file: 'app/src/components/three/Vault3D.tsx', id: 'abobada' },
|
||||||
|
{ file: 'app/src/components/three/Dome3D.tsx', id: 'cupula' },
|
||||||
|
{ file: 'app/src/components/three/Cylinder3D.tsx', id: 'cilindro' },
|
||||||
|
{ file: 'app/src/components/three/Tower3D.tsx', id: 'torre' },
|
||||||
|
{ file: 'app/src/components/Warehouse3D.tsx', id: 'galpao' },
|
||||||
|
{ file: 'app/src/components/three/BarSelector3D.tsx', id: 'barras' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const comp of componentsToUpdate) {
|
||||||
|
const filePath = path.join(__dirname, comp.file);
|
||||||
|
if (!fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
content = content.replace(/<SceneCanvas/g, `<SceneCanvas moduleId="${comp.id}"`);
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
console.log(`Updated ${comp.file} with moduleId="${comp.id}"`);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, type ReactNode } from 'react';
|
import React, { useEffect, type ReactNode } from 'react';
|
||||||
import { Canvas, type CanvasProps } from '@react-three/fiber';
|
import { Canvas, type CanvasProps } from '@react-three/fiber';
|
||||||
import { isWebGLSupported } from '../lib/webgl-detect';
|
import { isWebGLSupported } from '../lib/webgl-detect';
|
||||||
import { useCaptureStore } from '../store/captureStore';
|
import { useCaptureStore } from '../store/captureStore';
|
||||||
@@ -7,8 +7,11 @@ import WebglErrorBoundary from './WebglErrorBoundary';
|
|||||||
import { Lock, Unlock } from 'lucide-react';
|
import { Lock, Unlock } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
|
export const ViewerModuleContext = React.createContext<string | undefined>(undefined);
|
||||||
|
|
||||||
interface SceneCanvasProps extends CanvasProps {
|
interface SceneCanvasProps extends CanvasProps {
|
||||||
fallback: ReactNode;
|
fallback: ReactNode;
|
||||||
|
moduleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps) {
|
function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps) {
|
||||||
@@ -48,7 +51,7 @@ function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SceneCanvas({ fallback, style, className, ...rest }: SceneCanvasProps) {
|
export default function SceneCanvas({ fallback, style, className, moduleId, ...rest }: SceneCanvasProps) {
|
||||||
if (!isWebGLSupported()) {
|
if (!isWebGLSupported()) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -60,9 +63,19 @@ export default function SceneCanvas({ fallback, style, className, ...rest }: Sce
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isLocked, toggleLock } = useViewerStore();
|
const { locks, toggleLock } = useViewerStore();
|
||||||
|
|
||||||
|
// If no moduleId is provided, use a default key or treat as unlocked globally
|
||||||
|
const isLocked = moduleId ? !!locks[moduleId] : false;
|
||||||
|
|
||||||
|
const handleToggleLock = () => {
|
||||||
|
if (moduleId) {
|
||||||
|
toggleLock(moduleId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ViewerModuleContext.Provider value={moduleId}>
|
||||||
<div
|
<div
|
||||||
style={{ width: '100%', height: '100%', minHeight: '500px', borderRadius: 'var(--radius-lg)', overflow: 'hidden', position: 'relative', ...style }}
|
style={{ width: '100%', height: '100%', minHeight: '500px', borderRadius: 'var(--radius-lg)', overflow: 'hidden', position: 'relative', ...style }}
|
||||||
className={`${className ?? ''} glass-panel`}
|
className={`${className ?? ''} glass-panel`}
|
||||||
@@ -71,16 +84,19 @@ export default function SceneCanvas({ fallback, style, className, ...rest }: Sce
|
|||||||
<CanvasInner {...rest} fallback={fallback} />
|
<CanvasInner {...rest} fallback={fallback} />
|
||||||
</WebglErrorBoundary>
|
</WebglErrorBoundary>
|
||||||
|
|
||||||
|
{moduleId && (
|
||||||
<div className="absolute bottom-4 right-4 z-20">
|
<div className="absolute bottom-4 right-4 z-20">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={toggleLock}
|
onClick={handleToggleLock}
|
||||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-background/80 hover:bg-background border border-border shadow-sm backdrop-blur-md transition-colors"
|
className={`flex items-center justify-center w-10 h-10 rounded-full border shadow-sm backdrop-blur-md transition-colors ${
|
||||||
|
isLocked ? 'bg-destructive/80 hover:bg-destructive border-destructive/20' : 'bg-background/80 hover:bg-background border-border'
|
||||||
|
}`}
|
||||||
aria-label={isLocked ? 'Destravar visualização 3D' : 'Travar visualização 3D'}
|
aria-label={isLocked ? 'Destravar visualização 3D' : 'Travar visualização 3D'}
|
||||||
>
|
>
|
||||||
{isLocked ? <Lock className="w-5 h-5 text-destructive" /> : <Unlock className="w-5 h-5 text-muted-foreground" />}
|
{isLocked ? <Lock className="w-5 h-5 text-destructive-foreground" /> : <Unlock className="w-5 h-5 text-muted-foreground" />}
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="left">
|
<TooltipContent side="left">
|
||||||
@@ -89,6 +105,8 @@ export default function SceneCanvas({ fallback, style, className, ...rest }: Sce
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ViewerModuleContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export default function Warehouse3DViewer() {
|
|||||||
const maxDimension = Math.max(width, length, height);
|
const maxDimension = Math.max(width, length, height);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="galpao"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [width * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
camera={{ position: [width * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default function Bar3DViewer(input: Bar3DInput) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="barras"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [size, size * 0.6, size], fov: 45 }}
|
camera={{ position: [size, size * 0.6, size], fov: 45 }}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default function Bridge3DViewer(input: Bridge3DInput) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="ponte"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }}
|
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }:
|
|||||||
const maxDim = Math.max(diameter, height);
|
const maxDim = Math.max(diameter, height);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="cilindro"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [diameter * 1.5, height * 1.2, diameter * 1.5], fov: 40 }}
|
camera={{ position: [diameter * 1.5, height * 1.2, diameter * 1.5], fov: 40 }}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export default function Dome3DViewer(props: Dome3DInput) {
|
|||||||
const maxDim = Math.max(props.diameter, props.wallHeight + props.rise);
|
const maxDim = Math.max(props.diameter, props.wallHeight + props.rise);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="cupula"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [props.diameter * 1.5, (props.wallHeight + props.rise) * 1.5, props.diameter * 1.5], fov: 40 }}
|
camera={{ position: [props.diameter * 1.5, (props.wallHeight + props.rise) * 1.5, props.diameter * 1.5], fov: 40 }}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="dinamica"
|
||||||
frameloop="always"
|
frameloop="always"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
|
|||||||
@@ -28,11 +28,17 @@ export interface IsolatedRoof3DInput {
|
|||||||
forceKN: number;
|
forceKN: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pressureColor(cpe: number): THREE.Color {
|
function pressureColor(cpe: number, isDark: boolean): THREE.Color {
|
||||||
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
|
if (Math.abs(cpe) < 0.05) return new THREE.Color(isDark ? '#475569' : '#cbd5e1');
|
||||||
const t = (clamped + 2.5) / 4.0;
|
if (cpe > 0) {
|
||||||
const h = 240 - t * 240; // azul -> vermelho
|
// Sucção (Vermelho)
|
||||||
return new THREE.Color(`hsl(${h}, 75%, 50%)`);
|
const intensity = Math.min(1, cpe / 2.0);
|
||||||
|
return new THREE.Color(`hsl(0, ${60 + intensity * 30}%, ${50 - intensity * 10}%)`);
|
||||||
|
} else {
|
||||||
|
// Pressão (Azul)
|
||||||
|
const intensity = Math.min(1, Math.abs(cpe) / 2.0);
|
||||||
|
return new THREE.Color(`hsl(210, ${60 + intensity * 30}%, ${50 - intensity * 10}%)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function IsolatedRoofModel({
|
function IsolatedRoofModel({
|
||||||
@@ -56,8 +62,8 @@ function IsolatedRoofModel({
|
|||||||
const halfDepth = depth / 2;
|
const halfDepth = depth / 2;
|
||||||
const halfWidth = width / 2;
|
const halfWidth = width / 2;
|
||||||
|
|
||||||
const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]);
|
const windwardColor = useMemo(() => pressureColor(cpeWindward, isDark), [cpeWindward, isDark]);
|
||||||
const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]);
|
const leewardColor = useMemo(() => pressureColor(cpeLeeward, isDark), [cpeLeeward, isDark]);
|
||||||
|
|
||||||
const h_diff = depth * Math.tan(thetaRad);
|
const h_diff = depth * Math.tan(thetaRad);
|
||||||
const h_half = (depth / 2) * Math.tan(thetaRad);
|
const h_half = (depth / 2) * Math.tan(thetaRad);
|
||||||
@@ -214,6 +220,31 @@ function IsolatedRoofModel({
|
|||||||
</Text>
|
</Text>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
{/* Cota de Largura (b) */}
|
||||||
|
<group position={[0, height / 2, halfDepth + 0.4]}>
|
||||||
|
<mesh position={[0, 0, 0]}>
|
||||||
|
<boxGeometry args={[width, 0.015, 0.015]} />
|
||||||
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[halfWidth, 0, 0]}>
|
||||||
|
<boxGeometry args={[0.015, 0.1, 0.015]} />
|
||||||
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[-halfWidth, 0, 0]}>
|
||||||
|
<boxGeometry args={[0.015, 0.1, 0.015]} />
|
||||||
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
|
</mesh>
|
||||||
|
<Text
|
||||||
|
position={[0, 0.15, 0]}
|
||||||
|
fontSize={0.25}
|
||||||
|
color={labelColor}
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
b = {width}m
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
|
|
||||||
{/* Rótulo Superior */}
|
{/* Rótulo Superior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, centerY + 3, 0]}
|
position={[0, centerY + 3, 0]}
|
||||||
@@ -248,7 +279,7 @@ export default function IsolatedRoof3DViewer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="cobertura_isolada"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [cameraDistance, cameraDistance * 0.8, cameraDistance], fov: 40 }}
|
camera={{ position: [cameraDistance, cameraDistance * 0.8, cameraDistance], fov: 40 }}
|
||||||
|
|||||||
@@ -53,11 +53,10 @@ function SignModel({
|
|||||||
[applicationPoint, height, baseY],
|
[applicationPoint, height, baseY],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cor da placa baseada no Cf (mais vermelho = mais carga)
|
// Cor da placa: Azul (Pressão/Empuxo) com intensidade proporcional ao Cf
|
||||||
const plateColor = useMemo(() => {
|
const plateColor = useMemo(() => {
|
||||||
const intensity = Math.min(1, Math.abs(cf) / 2.0);
|
const intensity = Math.min(1, Math.abs(cf) / 2.0);
|
||||||
const hue = 220 - intensity * 220; // azul → vermelho
|
return new THREE.Color(`hsl(210, ${60 + intensity * 30}%, ${50 - intensity * 10}%)`);
|
||||||
return new THREE.Color(`hsl(${hue}, ${60 + intensity * 30}%, ${50 - intensity * 10}%)`);
|
|
||||||
}, [cf]);
|
}, [cf]);
|
||||||
|
|
||||||
// Pontas de extremidade (placas de extremidade opcionais)
|
// Pontas de extremidade (placas de extremidade opcionais)
|
||||||
@@ -185,7 +184,7 @@ function SignModel({
|
|||||||
{/* Texto Informativo Superior */}
|
{/* Texto Informativo Superior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, topY + 0.6, 0]}
|
position={[0, topY + 0.6, 0]}
|
||||||
rotation={[0, Math.PI / 2, 0]}
|
rotation={[0, -Math.PI / 2, 0]}
|
||||||
fontSize={Math.max(0.25, Math.min(0.4, length / 12))}
|
fontSize={Math.max(0.25, Math.min(0.4, length / 12))}
|
||||||
color={isDark ? "#cbd5e1" : "#1a202c"}
|
color={isDark ? "#cbd5e1" : "#1a202c"}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
@@ -221,7 +220,7 @@ export default function Sign3DViewer({
|
|||||||
const maxDim = Math.max(length, height);
|
const maxDim = Math.max(length, height);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="muro_placa"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [length * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
camera={{ position: [length * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export default function Tower3DViewer(input: Tower3DInput) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="torre"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpe
|
|||||||
const maxDimension = Math.max(span, length, rise);
|
const maxDimension = Math.max(span, length, rise);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas moduleId="abobada"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [span * 1.2, rise * 1.5, length * 1.2], fov: 40 }}
|
camera={{ position: [span * 1.2, rise * 1.5, length * 1.2], fov: 40 }}
|
||||||
|
|||||||
@@ -1,11 +1,51 @@
|
|||||||
import React from 'react';
|
import React, { useContext, useEffect, useRef } from 'react';
|
||||||
import { OrbitControls } from '@react-three/drei';
|
import { OrbitControls } from '@react-three/drei';
|
||||||
|
import { useThree } from '@react-three/fiber';
|
||||||
import { useViewerStore } from '@/store/viewerStore';
|
import { useViewerStore } from '@/store/viewerStore';
|
||||||
|
import { ViewerModuleContext } from '../SceneCanvas';
|
||||||
|
|
||||||
type OrbitControlsProps = React.ComponentProps<typeof OrbitControls>;
|
type OrbitControlsProps = React.ComponentProps<typeof OrbitControls>;
|
||||||
|
|
||||||
export const ViewerOrbitControls: React.FC<OrbitControlsProps> = (props) => {
|
export const ViewerOrbitControls: React.FC<OrbitControlsProps> = (props) => {
|
||||||
const isLocked = useViewerStore((state) => state.isLocked);
|
const moduleId = useContext(ViewerModuleContext);
|
||||||
|
const { locks, cameras, saveCamera } = useViewerStore();
|
||||||
|
const { camera } = useThree();
|
||||||
|
const controlsRef = useRef<any>(null);
|
||||||
|
|
||||||
return <OrbitControls enabled={!isLocked} {...props} />;
|
const isLocked = moduleId ? !!locks[moduleId] : false;
|
||||||
|
|
||||||
|
// Carrega a posição salva da câmera se existir
|
||||||
|
useEffect(() => {
|
||||||
|
if (moduleId && cameras[moduleId] && controlsRef.current) {
|
||||||
|
const saved = cameras[moduleId];
|
||||||
|
camera.position.set(saved.position[0], saved.position[1], saved.position[2]);
|
||||||
|
controlsRef.current.target.set(saved.target[0], saved.target[1], saved.target[2]);
|
||||||
|
controlsRef.current.update();
|
||||||
|
}
|
||||||
|
}, [moduleId, camera]); // executado uma vez por módulo
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
if (moduleId && controlsRef.current) {
|
||||||
|
saveCamera(moduleId, {
|
||||||
|
position: [camera.position.x, camera.position.y, camera.position.z],
|
||||||
|
target: [
|
||||||
|
controlsRef.current.target.x,
|
||||||
|
controlsRef.current.target.y,
|
||||||
|
controlsRef.current.target.z
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (props.onEnd) {
|
||||||
|
props.onEnd(undefined as any);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OrbitControls
|
||||||
|
ref={controlsRef}
|
||||||
|
enabled={!isLocked}
|
||||||
|
onEnd={handleEnd}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
interface ViewerState {
|
interface CameraState {
|
||||||
isLocked: boolean;
|
position: [number, number, number];
|
||||||
toggleLock: () => void;
|
target: [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useViewerStore = create<ViewerState>((set) => ({
|
interface ViewerState {
|
||||||
isLocked: false,
|
locks: Record<string, boolean>;
|
||||||
toggleLock: () => set((state) => ({ isLocked: !state.isLocked })),
|
toggleLock: (moduleId: string) => void;
|
||||||
}));
|
cameras: Record<string, CameraState>;
|
||||||
|
saveCamera: (moduleId: string, camera: CameraState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useViewerStore = create<ViewerState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
locks: {},
|
||||||
|
toggleLock: (moduleId) => set((state) => ({
|
||||||
|
locks: { ...state.locks, [moduleId]: !state.locks[moduleId] }
|
||||||
|
})),
|
||||||
|
cameras: {},
|
||||||
|
saveCamera: (moduleId, camera) => set((state) => ({
|
||||||
|
cameras: { ...state.cameras, [moduleId]: camera }
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'brainwind-viewer-storage',
|
||||||
|
partialize: (state) => ({ locks: state.locks, cameras: state.cameras }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user