sidebar + starting controls
This commit is contained in:
@@ -5,11 +5,12 @@ import {
|
|||||||
initSpatialGridSync,
|
initSpatialGridSync,
|
||||||
ItemNode,
|
ItemNode,
|
||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
|
useRegistry,
|
||||||
useScene,
|
useScene,
|
||||||
WallNode,
|
WallNode,
|
||||||
} from "@pascal-app/core";
|
} from "@pascal-app/core";
|
||||||
import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer";
|
import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer";
|
||||||
import { Stats } from "@react-three/drei";
|
import { CameraControls, CameraControlsImpl, Stats } from "@react-three/drei";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { Color, Object3D } from "three";
|
import { Color, Object3D } from "three";
|
||||||
@@ -54,10 +55,37 @@ export default function Editor() {
|
|||||||
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
||||||
<Passes />
|
<Passes />
|
||||||
<ToolManager />
|
<ToolManager />
|
||||||
|
<EditorCameraManager />
|
||||||
</Viewer>
|
</Viewer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const EditorCameraManager = () => {
|
||||||
|
const controls = useRef<CameraControlsImpl>(null!);
|
||||||
|
const currentLevelId = useViewer((state) => state.currentLevelId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let targetY = 0;
|
||||||
|
if (currentLevelId) {
|
||||||
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||||
|
if (levelMesh) {
|
||||||
|
targetY = levelMesh.position.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { position } = controls.current.camera;
|
||||||
|
controls.current.setLookAt(
|
||||||
|
position.x,
|
||||||
|
targetY + 5,
|
||||||
|
position.z,
|
||||||
|
0,
|
||||||
|
targetY,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}, [currentLevelId]);
|
||||||
|
|
||||||
|
return <CameraControls ref={controls} />;
|
||||||
|
};
|
||||||
|
|
||||||
const TestUndo = () => {
|
const TestUndo = () => {
|
||||||
const { undo, redo, futureStates, pastStates } = useScene.temporal.getState();
|
const { undo, redo, futureStates, pastStates } = useScene.temporal.getState();
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ function SidebarProvider({
|
|||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
|
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar pointer-events-none",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-slot="sidebar-wrapper"
|
data-slot="sidebar-wrapper"
|
||||||
@@ -229,7 +229,7 @@ function Sidebar({
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex pointer-events-auto",
|
||||||
side === "left"
|
side === "left"
|
||||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||||
@@ -243,7 +243,7 @@ function Sidebar({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
|
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm pointer-events-auto"
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-slot="sidebar-inner"
|
data-slot="sidebar-inner"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { useScene } from "@pascal-app/core";
|
|
||||||
|
|
||||||
export function SitePanel() {
|
|
||||||
const rootNodes = useScene((state) => state.rootNodeIds);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{rootNodes.map((nodeId) => (
|
|
||||||
<div key={nodeId}>{nodeId}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { BuildingNode, LevelNode, useScene } from "@pascal-app/core";
|
||||||
|
import { Building2, Plus } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/primitives/tooltip";
|
||||||
|
|
||||||
|
interface BuildingTreeNodeProps {
|
||||||
|
node: BuildingNode;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BuildingTreeNode({ node, depth }: BuildingTreeNodeProps) {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
const createNode = useScene((state) => state.createNode);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
// Handle building selection
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddLevel = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const newLevel = LevelNode.parse({
|
||||||
|
level: node.children.length,
|
||||||
|
children: [],
|
||||||
|
parentId: node.id,
|
||||||
|
});
|
||||||
|
createNode(newLevel, node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeNodeWrapper
|
||||||
|
icon={<Building2 className="w-3.5 h-3.5" />}
|
||||||
|
label={node.name || "Building"}
|
||||||
|
depth={depth}
|
||||||
|
hasChildren={node.children.length > 0}
|
||||||
|
expanded={expanded}
|
||||||
|
onToggle={() => setExpanded(!expanded)}
|
||||||
|
onClick={handleClick}
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{node.children.map((childId) => (
|
||||||
|
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||||
|
))}
|
||||||
|
</TreeNodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useScene } from "@pascal-app/core";
|
||||||
|
import { TreeNode } from "./tree-node";
|
||||||
|
|
||||||
|
export function SitePanel() {
|
||||||
|
const rootNodeIds = useScene((state) => state.rootNodeIds);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-1">
|
||||||
|
{rootNodeIds.map((nodeId) => (
|
||||||
|
<TreeNode key={nodeId} nodeId={nodeId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ItemNode } from "@pascal-app/core";
|
||||||
|
import { Box } from "lucide-react";
|
||||||
|
import { TreeNodeWrapper } from "./tree-node";
|
||||||
|
|
||||||
|
interface ItemTreeNodeProps {
|
||||||
|
node: ItemNode;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
|
||||||
|
const handleClick = () => {
|
||||||
|
// Handle item selection
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeNodeWrapper
|
||||||
|
icon={<Box className="w-3.5 h-3.5" />}
|
||||||
|
label={node.name || node.asset.name || "Item"}
|
||||||
|
depth={depth}
|
||||||
|
hasChildren={false}
|
||||||
|
expanded={false}
|
||||||
|
onToggle={() => {}}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { LevelNode } from "@pascal-app/core";
|
||||||
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
|
import { Layers } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||||
|
|
||||||
|
interface LevelTreeNodeProps {
|
||||||
|
node: LevelNode;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
const currentLevelId = useViewer((state) => state.currentLevelId);
|
||||||
|
const setCurrentLevel = useViewer((state) => state.setCurrentLevelId);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setCurrentLevel(node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelected = currentLevelId === node.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeNodeWrapper
|
||||||
|
icon={<Layers className="w-3.5 h-3.5" />}
|
||||||
|
label={node.name || `Level ${node.level}`}
|
||||||
|
depth={depth}
|
||||||
|
hasChildren={node.children.length > 0}
|
||||||
|
expanded={expanded}
|
||||||
|
onToggle={() => setExpanded(!expanded)}
|
||||||
|
onClick={handleClick}
|
||||||
|
isSelected={isSelected}
|
||||||
|
>
|
||||||
|
{node.children.map((childId) => (
|
||||||
|
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||||
|
))}
|
||||||
|
</TreeNodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { AnyNodeId, useScene } from "@pascal-app/core";
|
||||||
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { BuildingTreeNode } from "./building-tree-node";
|
||||||
|
import { LevelTreeNode } from "./level-tree-node";
|
||||||
|
import { WallTreeNode } from "./wall-tree-node";
|
||||||
|
import { ItemTreeNode } from "./item-tree-node";
|
||||||
|
|
||||||
|
interface TreeNodeProps {
|
||||||
|
nodeId: AnyNodeId;
|
||||||
|
depth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TreeNode({ nodeId, depth = 0 }: TreeNodeProps) {
|
||||||
|
const node = useScene((state) => state.nodes[nodeId]);
|
||||||
|
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
switch (node.type) {
|
||||||
|
case "building":
|
||||||
|
return <BuildingTreeNode node={node} depth={depth} />;
|
||||||
|
case "level":
|
||||||
|
return <LevelTreeNode node={node} depth={depth} />;
|
||||||
|
case "wall":
|
||||||
|
return <WallTreeNode node={node} depth={depth} />;
|
||||||
|
case "item":
|
||||||
|
return <ItemTreeNode node={node} depth={depth} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeNodeWrapperProps {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
depth: number;
|
||||||
|
hasChildren: boolean;
|
||||||
|
expanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
onClick: () => void;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
isSelected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TreeNodeWrapper({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
depth,
|
||||||
|
hasChildren,
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
onClick,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
isSelected,
|
||||||
|
}: TreeNodeWrapperProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center h-7 hover:bg-accent/50 cursor-pointer group",
|
||||||
|
"text-sm",
|
||||||
|
isSelected ? "text-primary-foreground bg-primary/80" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
style={{ paddingLeft: depth * 12 + 4 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="w-4 h-4 flex items-center justify-center shrink-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasChildren ? (
|
||||||
|
expanded ? (
|
||||||
|
<ChevronDown className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3 h-3" />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1.5 flex-1 min-w-0"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{label}</span>
|
||||||
|
</div>
|
||||||
|
{actions && (
|
||||||
|
<div className="opacity-0 group-hover:opacity-100 pr-1">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{expanded && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { WallNode, useScene } from "@pascal-app/core";
|
||||||
|
import { Square } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TreeNode, TreeNodeWrapper } from "./tree-node";
|
||||||
|
|
||||||
|
interface WallTreeNodeProps {
|
||||||
|
node: WallNode;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
// Handle wall selection
|
||||||
|
useScene.getState().markDirty(node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wallLength = Math.sqrt(
|
||||||
|
Math.pow(node.end[0] - node.start[0], 2) +
|
||||||
|
Math.pow(node.end[1] - node.start[1], 2)
|
||||||
|
).toFixed(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeNodeWrapper
|
||||||
|
icon={<Square className="w-3.5 h-3.5" />}
|
||||||
|
label={node.name || `Wall (${wallLength}m)`}
|
||||||
|
depth={depth}
|
||||||
|
hasChildren={node.children.length > 0}
|
||||||
|
expanded={expanded}
|
||||||
|
onToggle={() => setExpanded(!expanded)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{node.children.map((childId) => (
|
||||||
|
<TreeNode key={childId} nodeId={childId} depth={depth + 1} />
|
||||||
|
))}
|
||||||
|
</TreeNodeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
|||||||
camera={{ position: [3, 3, 3], fov: 50 }}
|
camera={{ position: [3, 3, 3], fov: 50 }}
|
||||||
>
|
>
|
||||||
<color attach="background" args={["#ececec"]} />
|
<color attach="background" args={["#ececec"]} />
|
||||||
<OrbitControls />
|
|
||||||
<Environment preset="sunset" />
|
<Environment preset="sunset" />
|
||||||
<Bvh>
|
<Bvh>
|
||||||
<SceneRenderer />
|
<SceneRenderer />
|
||||||
|
|||||||
Reference in New Issue
Block a user