diff --git a/apps/editor/components/editor.tsx b/apps/editor/components/editor.tsx
index efc76076..acc718aa 100644
--- a/apps/editor/components/editor.tsx
+++ b/apps/editor/components/editor.tsx
@@ -7,7 +7,7 @@ import {
useScene,
WallNode,
} from "@pascal-app/core";
-import { useViewer, Viewer } from "@pascal-app/viewer";
+import { useGridEvents, useNodeEvents, useViewer, Viewer } from "@pascal-app/viewer";
import { Stats } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { useEffect, useMemo, useRef } from "react";
@@ -27,6 +27,7 @@ import {
} from "three/tsl";
import { MeshBasicNodeMaterial, PostProcessing } from "three/webgpu";
import { ActionMenu } from "./ui/action-menu";
+import { WallEditor } from "./editors/wall/wall-editor";
const selectedObjects: Object3D[] = [];
@@ -37,10 +38,11 @@ export default function Editor() {
-
+
+
);
@@ -187,17 +189,31 @@ const Grid = ({
fadeStrength,
]);
+ const handlers = useGridEvents();
+
return (
-
+
);
};
-const Selector = () => {
+const DraftSelector = () => {
const selectedItemId = useRef(null);
const itemSelectedAt = useRef(0);
useEffect(() => {
+ emitter.on('building:enter', (event) => {
+ console.log('Entered building:', event.node.id);
+ const itemMesh = sceneRegistry.nodes.get(event.node.id);
+ selectedObjects.length = 0;
+ selectedObjects.push(itemMesh);
+ });
+ emitter.on('building:leave', (event) => {
+ console.log('Leaving building:', event.node.id);
+ selectedObjects.length = 0;
+ });
+
+
emitter.on("item:click", (event) => {
event.stopPropagation();
if (Date.now() - itemSelectedAt.current < 50) {
diff --git a/apps/editor/components/editors/wall/wall-editor.tsx b/apps/editor/components/editors/wall/wall-editor.tsx
new file mode 100644
index 00000000..c19b75e0
--- /dev/null
+++ b/apps/editor/components/editors/wall/wall-editor.tsx
@@ -0,0 +1,85 @@
+import { emitter, GridEvent, useScene, WallNode } from "@pascal-app/core";
+import { useViewer } from "@pascal-app/viewer";
+import { useEffect, useRef } from "react"
+import { Line, Mesh, Vector3 } from "three";
+
+export const WallEditor: React.FC = () => {
+
+ const cursorRef = useRef(null);
+ const drawingLineRef = useRef(null!);
+
+ useEffect(() => {
+
+ let buildingState = 0;
+ const startingPoint = new Vector3(0, 0, 0);
+ const endingPoint = new Vector3(0, 0, 0);
+ let gridPosition: [number, number] = [0, 0];
+
+
+ drawingLineRef.current.geometry.setFromPoints([
+ startingPoint,
+ endingPoint,
+ ]);
+
+ const onGridMove = (event: GridEvent) => {
+ if (!cursorRef.current) return;
+
+ gridPosition = [Math.round(event.position[0] * 2) / 2, Math.round(event.position[1] * 2) / 2];
+ cursorRef.current.position.set(gridPosition[0], 0.1, gridPosition[1]);
+ if (buildingState === 1) {
+ endingPoint.set(gridPosition[0], 0.1, gridPosition[1]);
+ }
+ drawingLineRef.current.geometry.setFromPoints([
+ startingPoint,
+ endingPoint,
+ ]);
+ };
+ const onGridClick = (event: GridEvent) => {
+ if (buildingState === 0) {
+ startingPoint.set(gridPosition[0], 0.1, gridPosition[1]);
+ buildingState = 1;
+ console.log('starting building at:', startingPoint);
+ drawingLineRef.current.visible = true;
+
+ } else if (buildingState === 1) {
+ const currentLevelId = useViewer.getState().currentLevelId;
+ console.log('currentLevelId:', currentLevelId);
+ if (currentLevelId) {
+ const wallNode = WallNode.parse({
+ start: [startingPoint.x, startingPoint.z],
+ end: [gridPosition[0], gridPosition[1]],
+ })
+ useScene.getState().createNode(wallNode, currentLevelId);
+ }
+ drawingLineRef.current.visible = false;
+ buildingState = 0;
+ }
+
+ };
+
+
+ emitter.on('grid:move', onGridMove);
+ emitter.on('grid:click', onGridClick);
+
+ return () => {
+ emitter.off('grid:move', onGridMove);
+ emitter.off('grid:click', onGridClick);
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+
+ {/* @ts-ignore */}
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx
index ac13a872..ceb439f8 100644
--- a/apps/editor/store/use-editor.tsx
+++ b/apps/editor/store/use-editor.tsx
@@ -1,5 +1,7 @@
"use client";
+import { BuildingNode, LevelNode, useScene } from "@pascal-app/core";
+import { useViewer } from "@pascal-app/viewer";
import { create } from "zustand";
export type Phase = "site" | "structure" | "furnish";
@@ -45,7 +47,49 @@ type EditorState = {
const useEditor = create()((set, get) => ({
phase: "site",
- setPhase: (phase) => set({ phase }),
+ setPhase: (phase) => {
+ const currentPhase = get().phase;
+ if (currentPhase === phase) return;
+
+ set({ phase });
+
+ const viewer = useViewer.getState();
+ const scene = useScene.getState();
+
+ switch (phase) {
+ case "site":
+ // In Site mode, we zoom out and deselect specific levels/buildings
+ viewer.setCurrentBuildingId(null);
+ viewer.setCurrentLevelId(null);
+ viewer.setLevelMode("stacked");
+ break;
+
+ case "structure":
+ // In Structure mode, we often want to focus on a specific building/level
+ // Auto-select the first building if none is selected
+ if (!viewer.currentBuildingId) {
+ const firstBuildingId = scene.rootNodeIds.find((id) => {
+ const node = scene.nodes[id];
+ return node?.type === "building" || null;
+ });
+ if (firstBuildingId) {
+ viewer.setCurrentBuildingId(firstBuildingId as BuildingNode["id"]);
+ const buildingNode = scene.nodes[firstBuildingId] as BuildingNode;
+ const firstLevelId = buildingNode.children[0];
+ if (firstLevelId) {
+ viewer.setCurrentLevelId(firstLevelId as LevelNode["id"]);
+ }
+ }
+ }
+ viewer.setLevelMode("exploded"); // Better for structure editing
+ break;
+
+ case "furnish":
+ // Maybe in furnish mode we force "solo" level view to see inside rooms
+ viewer.setLevelMode("solo");
+ break;
+ }
+ },
mode: "select",
setMode: (mode) => set({ mode }),
tool: null,
diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts
index b2094e34..bb6943d5 100644
--- a/packages/core/src/events/bus.ts
+++ b/packages/core/src/events/bus.ts
@@ -1,5 +1,5 @@
import mitt from "mitt";
-import { ItemNode, WallNode } from "../schema";
+import { BuildingNode, ItemNode, WallNode } from "../schema";
import { AnyNode } from "../schema/types";
// Base event interfaces
@@ -16,6 +16,7 @@ export interface NodeEvent {
export type WallEvent = NodeEvent;
export type ItemEvent = NodeEvent;
+export type BuildingEvent = NodeEvent;
// Event suffixes - exported for use in hooks
export const eventSuffixes = [
@@ -41,6 +42,7 @@ type GridEvents = {
type EditorEvents = GridEvents &
NodeEvents<"wall", WallEvent> &
- NodeEvents<"item", ItemEvent>;
+ NodeEvents<"item", ItemEvent> &
+ NodeEvents<"building", BuildingEvent>;
export const emitter = mitt();
diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts
index 8e13ee38..3b5c7120 100644
--- a/packages/core/src/store/use-scene.ts
+++ b/packages/core/src/store/use-scene.ts
@@ -20,6 +20,8 @@ type SceneState = {
loadScene: () => void;
markDirty: (id: AnyNodeId) => void;
clearDirty: (id: AnyNodeId) => void;
+
+ createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
};
const useScene = create()((set, get) => ({
@@ -123,6 +125,27 @@ const useScene = create()((set, get) => ({
clearDirty: (id) => {
get().dirtyNodes.delete(id);
},
+
+ createNode: (node, parentId) => {
+ set((state) => {
+ const newNodes = { ...state.nodes, [node.id]: node };
+
+ if (parentId) {
+ const parent = state.nodes[parentId];
+ if (parent) {
+ newNodes[parentId] = {
+ ...parent,
+ children: [...parent.children, node.id],
+ };
+ }
+ }
+
+ return { nodes: newNodes };
+ });
+
+ get().markDirty(node.id);
+ if (parentId) get().markDirty(parentId);
+ }
}));
useScene.getState().loadScene();
diff --git a/packages/viewer/src/components/renderers/building/building-renderer.tsx b/packages/viewer/src/components/renderers/building/building-renderer.tsx
index fdc9a0e3..52161f69 100644
--- a/packages/viewer/src/components/renderers/building/building-renderer.tsx
+++ b/packages/viewer/src/components/renderers/building/building-renderer.tsx
@@ -1,18 +1,16 @@
-import { BuildingNode, LevelNode, useRegistry } from "@pascal-app/core";
+import { BuildingNode, useRegistry } from "@pascal-app/core";
import { useRef } from "react";
import { Group } from "three";
import { NodeRenderer } from "../node-renderer";
+import { useNodeEvents } from "../../../hooks/use-node-events";
export const BuildingRenderer = ({ node }: { node: BuildingNode }) => {
const ref = useRef(null!);
useRegistry(node.id, node.type, ref);
+ const handlers = useNodeEvents(node, "building");
return (
-
- {/*
-
-
- */}
+
{node.children.map((childId) => (
))}
diff --git a/packages/viewer/src/components/renderers/level/level-renderer.tsx b/packages/viewer/src/components/renderers/level/level-renderer.tsx
index d89e569c..f6659034 100644
--- a/packages/viewer/src/components/renderers/level/level-renderer.tsx
+++ b/packages/viewer/src/components/renderers/level/level-renderer.tsx
@@ -7,6 +7,8 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => {
const ref = useRef(null!);
useRegistry(node.id, node.type, ref);
+
+ console.log('rendering level:', node.id, node.children);
return (
{/*
diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
index d71df886..3abd8c53 100644
--- a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
+++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
@@ -1,6 +1,5 @@
-import { emitter, useRegistry, WallNode } from "@pascal-app/core";
-import { ThreeEvent } from "@react-three/fiber";
-import { useCallback, useRef } from "react";
+import { useRegistry, WallNode } from "@pascal-app/core";
+import { useRef } from "react";
import { Mesh } from "three";
import { NodeRenderer } from "../node-renderer";
import { useNodeEvents } from "../../../hooks/use-node-events";
diff --git a/packages/viewer/src/hooks/use-grid-events.ts b/packages/viewer/src/hooks/use-grid-events.ts
new file mode 100644
index 00000000..fd4a2914
--- /dev/null
+++ b/packages/viewer/src/hooks/use-grid-events.ts
@@ -0,0 +1,33 @@
+import { emitter, EventSuffix, GridEvent } from "@pascal-app/core";
+import { ThreeEvent } from "@react-three/fiber";
+
+export function useGridEvents() {
+ const emit = (suffix: EventSuffix, e: ThreeEvent) => {
+ const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`;
+ const payload: GridEvent = {
+ position: [e.point.x, e.point.z],
+ };
+
+ emitter.emit(eventKey, payload);
+ };
+
+ return {
+ onPointerDown: (e: ThreeEvent) => {
+ if (e.button !== 0) return;
+ emit("pointerdown", e);
+ },
+ onPointerUp: (e: ThreeEvent) => {
+ if (e.button !== 0) return;
+ emit("pointerup", e);
+ },
+ onClick: (e: ThreeEvent) => {
+ if (e.button !== 0) return;
+ emit("click", e);
+ },
+ onPointerEnter: (e: ThreeEvent) => emit("enter", e),
+ onPointerLeave: (e: ThreeEvent) => emit("leave", e),
+ onPointerMove: (e: ThreeEvent) => emit("move", e),
+ onDoubleClick: (e: ThreeEvent) => emit("double-click", e),
+ onContextMenu: (e: ThreeEvent) => emit("context-menu", e),
+ };
+}
\ No newline at end of file
diff --git a/packages/viewer/src/hooks/use-node-events.ts b/packages/viewer/src/hooks/use-node-events.ts
index c41d83d6..5ffffcc0 100644
--- a/packages/viewer/src/hooks/use-node-events.ts
+++ b/packages/viewer/src/hooks/use-node-events.ts
@@ -1,4 +1,5 @@
import {
+ BuildingNode,
emitter,
EventSuffix,
ItemEvent,
@@ -7,10 +8,12 @@ import {
WallNode,
} from "@pascal-app/core";
import { ThreeEvent } from "@react-three/fiber";
+import { BuildingEvent } from "../../../core/src/events/bus";
type NodeConfig = {
item: { node: ItemNode; event: ItemEvent };
wall: { node: WallNode; event: WallEvent };
+ building: { node: BuildingNode; event: BuildingEvent };
};
type NodeType = keyof NodeConfig;
diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts
index fdc2b577..969357f8 100644
--- a/packages/viewer/src/index.ts
+++ b/packages/viewer/src/index.ts
@@ -1,3 +1,7 @@
+import { useGridEvents } from "./hooks/use-grid-events";
+
export { default as Viewer } from "./components/viewer/viewer";
export { default as useViewer } from "./store/use-viewer";
+
+export { useGridEvents } from "./hooks/use-grid-events";
\ No newline at end of file