rewrite selection system
This commit is contained in:
@@ -6,9 +6,12 @@ import { CameraControls, CameraControlsImpl } from "@react-three/drei";
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { Vector3 } from "three";
|
import { Vector3 } from "three";
|
||||||
|
|
||||||
|
const currentTarget = new Vector3();
|
||||||
|
|
||||||
export const CustomCameraControls = () => {
|
export const CustomCameraControls = () => {
|
||||||
const controls = useRef<CameraControlsImpl>(null!);
|
const controls = useRef<CameraControlsImpl>(null!);
|
||||||
const currentLevelId = useViewer((state) => state.currentLevelId);
|
const currentLevelId = useViewer((state) => state.selection.levelId);
|
||||||
|
const firstLoad = useRef(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let targetY = 0;
|
let targetY = 0;
|
||||||
@@ -18,7 +21,18 @@ export const CustomCameraControls = () => {
|
|||||||
targetY = levelMesh.position.y;
|
targetY = levelMesh.position.y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const currentTarget = new Vector3();
|
if (firstLoad.current) {
|
||||||
|
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).getTarget(currentTarget);
|
||||||
(controls.current as CameraControlsImpl).moveTo(
|
(controls.current as CameraControlsImpl).moveTo(
|
||||||
currentTarget.x,
|
currentTarget.x,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Editor() {
|
|||||||
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
||||||
<Passes />
|
<Passes />
|
||||||
<ToolManager />
|
<ToolManager />
|
||||||
<CustomControls />
|
<CustomCameraControls />
|
||||||
</Viewer>
|
</Viewer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -232,7 +232,7 @@ const Grid = ({
|
|||||||
const gridRef = useRef<Mesh>(null!);
|
const gridRef = useRef<Mesh>(null!);
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
const currentLevelId = useViewer.getState().currentLevelId;
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
let targetY = 0;
|
let targetY = 0;
|
||||||
if (currentLevelId) {
|
if (currentLevelId) {
|
||||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
let currentWallId: string | null = null;
|
let currentWallId: string | null = null;
|
||||||
|
|
||||||
const checkCanPlace = () => {
|
const checkCanPlace = () => {
|
||||||
const currentLevelId = useViewer.getState().currentLevelId;
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
if (currentLevelId && draftItem.current) {
|
if (currentLevelId && draftItem.current) {
|
||||||
let placeable = true;
|
let placeable = true;
|
||||||
if (draftItem.current.asset.attachTo) {
|
if (draftItem.current.asset.attachTo) {
|
||||||
@@ -72,7 +72,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const createDraftItem = () => {
|
const createDraftItem = () => {
|
||||||
const { currentLevelId } = useViewer.getState();
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
if (!currentLevelId) {
|
if (!currentLevelId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
const { currentLevelId } = useViewer.getState();
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
if (isOnWall.current) return;
|
if (isOnWall.current) return;
|
||||||
|
|
||||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
||||||
@@ -133,7 +133,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
|
|
||||||
const onWallEnter = (event: WallEvent) => {
|
const onWallEnter = (event: WallEvent) => {
|
||||||
if (
|
if (
|
||||||
useViewer.getState().currentLevelId !==
|
useViewer.getState().selection.levelId !==
|
||||||
resolveLevelId(event.node, useScene.getState().nodes)
|
resolveLevelId(event.node, useScene.getState().nodes)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -169,7 +169,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
currentWallId = null;
|
currentWallId = null;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!draftItem.current) return;
|
if (!draftItem.current) return;
|
||||||
const currentLevelId = useViewer.getState().currentLevelId;
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
draftItem.current.parentId = currentLevelId;
|
draftItem.current.parentId = currentLevelId;
|
||||||
useScene.getState().updateNode(draftItem.current.id, {
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
position: [
|
position: [
|
||||||
@@ -186,7 +186,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!isOnWall.current) return;
|
if (!isOnWall.current) return;
|
||||||
|
|
||||||
const currentLevelId = useViewer.getState().currentLevelId;
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return;
|
||||||
|
|
||||||
useScene.temporal.getState().resume();
|
useScene.temporal.getState().resume();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { Line, Mesh, Vector3 } from "three";
|
import { Line, Mesh, Vector3 } from "three";
|
||||||
|
|
||||||
const commitWallDrawing = (start: [number, number], end: [number, number]) => {
|
const commitWallDrawing = (start: [number, number], end: [number, number]) => {
|
||||||
const { currentLevelId } = useViewer.getState();
|
const currentLevelId = useViewer.getState().selection.levelId;
|
||||||
const { createNode } = useScene.getState();
|
const { createNode } = useScene.getState();
|
||||||
|
|
||||||
if (!currentLevelId) return;
|
if (!currentLevelId) return;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function AppSidebar() {
|
|||||||
// const handleDeleteSelected = useEditor((state) => state.handleDeleteSelected);
|
// const handleDeleteSelected = useEditor((state) => state.handleDeleteSelected);
|
||||||
// const serializeLayout = useEditor((state) => state.serializeLayout);
|
// const serializeLayout = useEditor((state) => state.serializeLayout);
|
||||||
const activeTool = useEditor((state) => state.tool);
|
const activeTool = useEditor((state) => state.tool);
|
||||||
const currentLevelId = useViewer((state) => state.currentLevelId);
|
const currentLevelId = useViewer((state) => state.selection.levelId);
|
||||||
const setActiveTool = useEditor((state) => state.setTool);
|
const setActiveTool = useEditor((state) => state.setTool);
|
||||||
|
|
||||||
const [jsonCollapsed, setJsonCollapsed] = useState<boolean | number>(1);
|
const [jsonCollapsed, setJsonCollapsed] = useState<boolean | number>(1);
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ interface LevelTreeNodeProps {
|
|||||||
|
|
||||||
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
const currentLevelId = useViewer((state) => state.currentLevelId);
|
const currentLevelId = useViewer((state) => state.selection.levelId);
|
||||||
const setCurrentLevel = useViewer((state) => state.setCurrentLevelId);
|
const setSelection = useViewer((state) => state.setSelection);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setCurrentLevel(node.id);
|
setSelection({ levelId: node.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelected = currentLevelId === node.id;
|
const isSelected = currentLevelId === node.id;
|
||||||
|
|||||||
@@ -73,25 +73,26 @@ const useEditor = create<EditorState>()((set, get) => ({
|
|||||||
switch (phase) {
|
switch (phase) {
|
||||||
case "site":
|
case "site":
|
||||||
// In Site mode, we zoom out and deselect specific levels/buildings
|
// In Site mode, we zoom out and deselect specific levels/buildings
|
||||||
viewer.setCurrentBuildingId(null);
|
viewer.resetSelection();
|
||||||
viewer.setCurrentLevelId(null);
|
|
||||||
viewer.setLevelMode("stacked");
|
viewer.setLevelMode("stacked");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "structure":
|
case "structure":
|
||||||
// In Structure mode, we often want to focus on a specific building/level
|
// In Structure mode, we often want to focus on a specific building/level
|
||||||
// Auto-select the first building if none is selected
|
// Auto-select the first building if none is selected
|
||||||
if (!viewer.currentBuildingId) {
|
if (!viewer.selection.buildingId) {
|
||||||
const firstBuildingId = scene.rootNodeIds.find((id) => {
|
const firstBuildingId = scene.rootNodeIds.find((id) => {
|
||||||
const node = scene.nodes[id];
|
const node = scene.nodes[id];
|
||||||
return node?.type === "building" || null;
|
return node?.type === "building" || null;
|
||||||
});
|
});
|
||||||
if (firstBuildingId) {
|
if (firstBuildingId) {
|
||||||
viewer.setCurrentBuildingId(firstBuildingId as BuildingNode["id"]);
|
viewer.setSelection({
|
||||||
|
buildingId: firstBuildingId as BuildingNode["id"],
|
||||||
|
});
|
||||||
const buildingNode = scene.nodes[firstBuildingId] as BuildingNode;
|
const buildingNode = scene.nodes[firstBuildingId] as BuildingNode;
|
||||||
const firstLevelId = buildingNode.children[0];
|
const firstLevelId = buildingNode.children[0];
|
||||||
if (firstLevelId) {
|
if (firstLevelId) {
|
||||||
viewer.setCurrentLevelId(firstLevelId as LevelNode["id"]);
|
viewer.setSelection({ levelId: firstLevelId as LevelNode["id"] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ export type { AnyNodeType, AnyNodeId } from "./types";
|
|||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
export { CameraSchema } from "./camera";
|
export { CameraSchema } from "./camera";
|
||||||
|
|
||||||
|
// Zones
|
||||||
|
export type { Zone, ZonePolygon } from "./zone";
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import dedent from "dedent";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { objectId } from "./base";
|
||||||
|
import { LevelNode } from "./nodes/level";
|
||||||
|
|
||||||
|
// Polygon boundary for zone area - array of [x, z] coordinates
|
||||||
|
export const ZonePolygon = z.array(z.tuple([z.number(), z.number()]));
|
||||||
|
|
||||||
|
export const ZoneSchema = z
|
||||||
|
.object({
|
||||||
|
id: objectId("zone"),
|
||||||
|
object: z.literal("zone").default("zone"),
|
||||||
|
levelId: LevelNode.shape.id, // Required - must be attached to a level
|
||||||
|
name: z.string(),
|
||||||
|
// Polygon boundary - array of [x, z] coordinates defining the zone
|
||||||
|
polygon: ZonePolygon,
|
||||||
|
// Visual styling
|
||||||
|
color: z.string().default("#3b82f6"), // Default blue
|
||||||
|
metadata: z.json().optional().default({}),
|
||||||
|
})
|
||||||
|
.describe(
|
||||||
|
dedent`
|
||||||
|
Zone schema - a polygon zone attached to a level
|
||||||
|
- object: "zone"
|
||||||
|
- id: zone id
|
||||||
|
- levelId: level this zone is attached to
|
||||||
|
- name: zone name
|
||||||
|
- polygon: array of [x, z] points defining the zone boundary
|
||||||
|
- color: hex color for visual styling
|
||||||
|
- metadata: zone metadata (optional)
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Zone = z.infer<typeof ZoneSchema>;
|
||||||
|
export type ZonePolygon = z.infer<typeof ZonePolygon>;
|
||||||
@@ -29,7 +29,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
|||||||
return renderer;
|
return renderer;
|
||||||
}}
|
}}
|
||||||
shadows
|
shadows
|
||||||
camera={{ position: [3, 3, 3], fov: 50 }}
|
camera={{ position: [50, 50, 50], fov: 50 }}
|
||||||
>
|
>
|
||||||
<color attach="background" args={["#ececec"]} />
|
<color attach="background" args={["#ececec"]} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,60 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BuildingNode, LevelNode } from "@pascal-app/core";
|
import { BuildingNode, ItemNode, LevelNode, Zone } from "@pascal-app/core";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type SelectionPath = {
|
||||||
|
buildingId: BuildingNode["id"] | null;
|
||||||
|
levelId: LevelNode["id"] | null;
|
||||||
|
zoneId: Zone["id"] | null;
|
||||||
|
selectedIds: ItemNode["id"][]; // For items/assets (multi-select)
|
||||||
|
};
|
||||||
|
|
||||||
type ViewerState = {
|
type ViewerState = {
|
||||||
|
selection: SelectionPath;
|
||||||
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
||||||
|
|
||||||
|
// Actions
|
||||||
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
||||||
|
|
||||||
currentBuildingId: BuildingNode["id"] | null;
|
// Smart selection update
|
||||||
setCurrentBuildingId: (id: BuildingNode["id"] | null) => void;
|
setSelection: (updates: Partial<SelectionPath>) => void;
|
||||||
currentLevelId: LevelNode["id"] | null;
|
resetSelection: () => void;
|
||||||
setCurrentLevelId: (id: LevelNode["id"] | null) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const useViewer = create<ViewerState>()((set, get) => ({
|
const useViewer = create<ViewerState>()((set, get) => ({
|
||||||
|
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||||
levelMode: "stacked",
|
levelMode: "stacked",
|
||||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||||
currentLevelId: null,
|
|
||||||
setCurrentLevelId: (id) => set({ currentLevelId: id }),
|
setSelection: (updates) =>
|
||||||
currentBuildingId: null,
|
set((state) => {
|
||||||
setCurrentBuildingId: (id) => set({ currentBuildingId: id }),
|
const newSelection = { ...state.selection, ...updates };
|
||||||
|
|
||||||
|
// Hierarchy Guard: If we change a high-level parent, reset the children
|
||||||
|
if (updates.buildingId !== undefined) {
|
||||||
|
newSelection.levelId = null;
|
||||||
|
newSelection.zoneId = null;
|
||||||
|
newSelection.selectedIds = [];
|
||||||
|
} else if (updates.levelId !== undefined) {
|
||||||
|
newSelection.zoneId = null;
|
||||||
|
newSelection.selectedIds = [];
|
||||||
|
} else if (updates.zoneId !== undefined) {
|
||||||
|
newSelection.selectedIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { selection: newSelection };
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetSelection: () =>
|
||||||
|
set({
|
||||||
|
selection: {
|
||||||
|
buildingId: null,
|
||||||
|
levelId: null,
|
||||||
|
zoneId: null,
|
||||||
|
selectedIds: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useViewer;
|
export default useViewer;
|
||||||
|
|||||||
Reference in New Issue
Block a user