sidebar + starting controls
This commit is contained in:
@@ -5,11 +5,12 @@ import {
|
||||
initSpatialGridSync,
|
||||
ItemNode,
|
||||
sceneRegistry,
|
||||
useRegistry,
|
||||
useScene,
|
||||
WallNode,
|
||||
} from "@pascal-app/core";
|
||||
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 { useEffect, useMemo, useRef } from "react";
|
||||
import { Color, Object3D } from "three";
|
||||
@@ -54,10 +55,37 @@ export default function Editor() {
|
||||
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
||||
<Passes />
|
||||
<ToolManager />
|
||||
<EditorCameraManager />
|
||||
</Viewer>
|
||||
</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 { undo, redo, futureStates, pastStates } = useScene.temporal.getState();
|
||||
|
||||
@@ -132,7 +132,7 @@ function SidebarProvider({
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
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,
|
||||
)}
|
||||
data-slot="sidebar-wrapper"
|
||||
@@ -229,7 +229,7 @@ function Sidebar({
|
||||
/>
|
||||
<div
|
||||
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"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
@@ -243,7 +243,7 @@ function Sidebar({
|
||||
{...props}
|
||||
>
|
||||
<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-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 }}
|
||||
>
|
||||
<color attach="background" args={["#ececec"]} />
|
||||
<OrbitControls />
|
||||
|
||||
<Environment preset="sunset" />
|
||||
<Bvh>
|
||||
<SceneRenderer />
|
||||
|
||||
Reference in New Issue
Block a user