camera view
This commit is contained in:
@@ -1,38 +1,51 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { sceneRegistry } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { Vector3 } from 'three'
|
||||
import {
|
||||
type CameraControlEvent,
|
||||
emitter,
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
} from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { CameraControls, CameraControlsImpl } from "@react-three/drei";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Vector3 } from "three";
|
||||
|
||||
const currentTarget = new Vector3()
|
||||
const currentTarget = new Vector3();
|
||||
|
||||
export const CustomCameraControls = () => {
|
||||
const controls = useRef<CameraControlsImpl>(null!)
|
||||
const currentLevelId = useViewer((state) => state.selection.levelId)
|
||||
const firstLoad = useRef(true)
|
||||
const controls = useRef<CameraControlsImpl>(null!);
|
||||
const currentLevelId = useViewer((state) => state.selection.levelId);
|
||||
const firstLoad = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
let targetY = 0
|
||||
let targetY = 0;
|
||||
if (currentLevelId) {
|
||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||
if (levelMesh) {
|
||||
targetY = levelMesh.position.y
|
||||
targetY = levelMesh.position.y;
|
||||
}
|
||||
}
|
||||
if (firstLoad.current) {
|
||||
firstLoad.current = false
|
||||
;(controls.current as CameraControlsImpl).setLookAt(20, 20, 20, 0, 0, 0, true)
|
||||
firstLoad.current = false;
|
||||
(controls.current as CameraControlsImpl).setLookAt(
|
||||
20,
|
||||
20,
|
||||
20,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
}
|
||||
;(controls.current as CameraControlsImpl).getTarget(currentTarget)
|
||||
;(controls.current as CameraControlsImpl).moveTo(
|
||||
(controls.current as CameraControlsImpl).getTarget(currentTarget);
|
||||
(controls.current as CameraControlsImpl).moveTo(
|
||||
currentTarget.x,
|
||||
targetY,
|
||||
currentTarget.z,
|
||||
true,
|
||||
)
|
||||
}, [currentLevelId])
|
||||
);
|
||||
}, [currentLevelId]);
|
||||
|
||||
// Configure mouse buttons based on control mode and camera mode
|
||||
const mouseButtons = useMemo(() => {
|
||||
@@ -41,15 +54,61 @@ export const CustomCameraControls = () => {
|
||||
// cameraMode === 'orthographic'
|
||||
// ? CameraControlsImpl.ACTION.ZOOM
|
||||
// : CameraControlsImpl.ACTION.DOLLY
|
||||
const wheelAction = CameraControlsImpl.ACTION.DOLLY
|
||||
const wheelAction = CameraControlsImpl.ACTION.DOLLY;
|
||||
|
||||
return {
|
||||
left: CameraControlsImpl.ACTION.NONE,
|
||||
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
|
||||
right: CameraControlsImpl.ACTION.ROTATE,
|
||||
wheel: wheelAction,
|
||||
}
|
||||
}, [])
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <CameraControls ref={controls} mouseButtons={mouseButtons} />
|
||||
}
|
||||
useEffect(() => {
|
||||
const handleNodeCapture = ({ nodeId }: CameraControlEvent) => {
|
||||
if (!controls.current) return;
|
||||
|
||||
const position = new Vector3();
|
||||
const target = new Vector3();
|
||||
controls.current.getPosition(position);
|
||||
controls.current.getTarget(target);
|
||||
|
||||
const state = useScene.getState();
|
||||
|
||||
state.updateNode(nodeId, {
|
||||
camera: {
|
||||
position: [position.x, position.y, position.z],
|
||||
target: [target.x, target.y, target.z],
|
||||
mode: useViewer.getState().cameraMode,
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleNodeView = ({ nodeId }: CameraControlEvent) => {
|
||||
if (!controls.current) return;
|
||||
|
||||
const node = useScene.getState().nodes[nodeId];
|
||||
if (!node || !node.camera) return;
|
||||
const { position, target } = node.camera;
|
||||
|
||||
controls.current.setLookAt(
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
target[0],
|
||||
target[1],
|
||||
target[2],
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
emitter.on("camera-controls:capture", handleNodeCapture);
|
||||
emitter.on("camera-controls:view", handleNodeView);
|
||||
|
||||
return () => {
|
||||
emitter.off("camera-controls:capture", handleNodeCapture);
|
||||
emitter.off("camera-controls:view", handleNodeView);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <CameraControls ref={controls} mouseButtons={mouseButtons} />;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useViewer } from "@pascal-app/viewer";
|
||||
import { Building2, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||
import { TreeNodeActions } from "./tree-node-actions";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -47,17 +48,20 @@ export function BuildingTreeNode({ node, depth }: BuildingTreeNodeProps) {
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
actions={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-accent"
|
||||
onClick={handleAddLevel}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Add new level</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<TreeNodeActions node={node} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-primary-foreground/20"
|
||||
onClick={handleAddLevel}
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Add new level</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{node.children.map((childId) => (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ItemNode } from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { Box } from "lucide-react";
|
||||
import { TreeNodeWrapper } from "./tree-node";
|
||||
import { TreeNodeActions } from "./tree-node-actions";
|
||||
|
||||
interface ItemTreeNodeProps {
|
||||
node: ItemNode;
|
||||
@@ -28,6 +29,7 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
actions={<TreeNodeActions node={node} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useViewer } from "@pascal-app/viewer";
|
||||
import { Layers } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||
import { TreeNodeActions } from "./tree-node-actions";
|
||||
|
||||
interface LevelTreeNodeProps {
|
||||
node: LevelNode;
|
||||
@@ -30,6 +31,7 @@ export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
actions={<TreeNodeActions node={node} />}
|
||||
>
|
||||
{node.children.map((childId) => (
|
||||
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { type AnyNode, emitter, useScene } from "@pascal-app/core";
|
||||
import { Camera, Eye, EyeOff, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/primitives/popover";
|
||||
|
||||
interface TreeNodeActionsProps {
|
||||
node: AnyNode;
|
||||
}
|
||||
|
||||
export function TreeNodeActions({ node }: TreeNodeActionsProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const updateNode = useScene((state) => state.updateNode);
|
||||
const hasCamera = !!node.camera;
|
||||
const isVisible = node.visible !== false;
|
||||
|
||||
const toggleVisibility = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
updateNode(node.id, { visible: !isVisible });
|
||||
};
|
||||
|
||||
const handleCaptureCamera = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
emitter.emit("camera-controls:capture", { nodeId: node.id });
|
||||
setOpen(false);
|
||||
};
|
||||
const handleViewCamera = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
emitter.emit("camera-controls:view", { nodeId: node.id });
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClearCamera = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
updateNode(node.id, { camera: undefined });
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
className="w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
|
||||
onClick={toggleVisibility}
|
||||
title={isVisible ? "Hide" : "Show"}
|
||||
>
|
||||
{isVisible ? (
|
||||
<Eye className="w-3 h-3" />
|
||||
) : (
|
||||
<EyeOff className="w-3 h-3 opacity-50" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="relative w-5 h-5 flex items-center justify-center rounded cursor-pointer hover:bg-primary-foreground/20"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Camera snapshot"
|
||||
>
|
||||
<Camera className="w-3 h-3" />
|
||||
{hasCamera && (
|
||||
<span className="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
className="w-auto p-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{hasCamera && (
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
|
||||
onClick={handleViewCamera}
|
||||
>
|
||||
<Camera className="w-3.5 h-3.5" />
|
||||
View snapshot
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-accent text-left w-full"
|
||||
onClick={handleCaptureCamera}
|
||||
>
|
||||
<Camera className="w-3.5 h-3.5" />
|
||||
{hasCamera ? "Update snapshot" : "Take snapshot"}
|
||||
</button>
|
||||
{hasCamera && (
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded cursor-pointer text-popover-foreground hover:bg-destructive hover:text-destructive-foreground text-left w-full"
|
||||
onClick={handleClearCamera}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Clear snapshot
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export function TreeNodeWrapper({
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center h-7 cursor-pointer group text-sm",
|
||||
"flex items-center h-7 cursor-pointer group/row text-sm",
|
||||
isSelected
|
||||
? "text-primary-foreground bg-primary/80 hover:bg-primary/90"
|
||||
: isHovered
|
||||
@@ -95,7 +95,7 @@ export function TreeNodeWrapper({
|
||||
<span className="truncate">{label}</span>
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="opacity-0 group-hover:opacity-100 pr-1">
|
||||
<div className="opacity-0 group-hover/row:opacity-100 pr-1">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useViewer } from "@pascal-app/viewer";
|
||||
import { Square } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||
import { TreeNodeActions } from "./tree-node-actions";
|
||||
|
||||
interface WallTreeNodeProps {
|
||||
node: WallNode;
|
||||
@@ -35,6 +36,7 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
||||
onClick={handleClick}
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
actions={<TreeNodeActions node={node} />}
|
||||
>
|
||||
{node.children.map((childId) => (
|
||||
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@pascal-app/core": "*",
|
||||
"@pascal-app/viewer": "*",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@pascal-app/core": "*",
|
||||
"@pascal-app/viewer": "*",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
@@ -317,6 +318,8 @@
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||
@@ -1109,6 +1112,8 @@
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
@@ -44,9 +44,18 @@ type GridEvents = {
|
||||
[K in `grid:${EventSuffix}`]: GridEvent;
|
||||
};
|
||||
|
||||
export interface CameraControlEvent {
|
||||
nodeId: AnyNode["id"];
|
||||
}
|
||||
|
||||
type CameraControlEvents = {
|
||||
"camera-controls:view": CameraControlEvent;
|
||||
"camera-controls:capture": CameraControlEvent;
|
||||
};
|
||||
type EditorEvents = GridEvents &
|
||||
NodeEvents<"wall", WallEvent> &
|
||||
NodeEvents<"item", ItemEvent> &
|
||||
NodeEvents<"building", BuildingEvent>;
|
||||
NodeEvents<"building", BuildingEvent> &
|
||||
CameraControlEvents;
|
||||
|
||||
export const emitter = mitt<EditorEvents>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Store
|
||||
|
||||
export type {
|
||||
CameraControlEvent,
|
||||
EventSuffix,
|
||||
GridEvent,
|
||||
ItemEvent,
|
||||
@@ -14,7 +15,10 @@ export {
|
||||
sceneRegistry,
|
||||
useRegistry,
|
||||
} from './hooks/scene-registry/scene-registry'
|
||||
export { initSpatialGridSync, resolveLevelId } from './hooks/spatial-grid/spatial-grid-sync'
|
||||
export {
|
||||
initSpatialGridSync,
|
||||
resolveLevelId,
|
||||
} from './hooks/spatial-grid/spatial-grid-sync'
|
||||
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
|
||||
// Schema
|
||||
export * from './schema'
|
||||
|
||||
@@ -28,6 +28,9 @@ type ViewerState = {
|
||||
hoveredId: AnyNode["id"] | null;
|
||||
setHoveredId: (id: AnyNode["id"] | null) => void;
|
||||
|
||||
cameraMode: "perspective" | "orthographic";
|
||||
setCameraMode: (mode: "perspective" | "orthographic") => void;
|
||||
|
||||
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
||||
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
||||
|
||||
@@ -43,6 +46,9 @@ const useViewer = create<ViewerState>()((set, get) => ({
|
||||
hoveredId: null,
|
||||
setHoveredId: (id) => set({ hoveredId: id }),
|
||||
|
||||
cameraMode: "perspective",
|
||||
setCameraMode: (mode) => set({ cameraMode: mode }),
|
||||
|
||||
levelMode: "stacked",
|
||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user