draft wall builder
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
useScene,
|
useScene,
|
||||||
WallNode,
|
WallNode,
|
||||||
} from "@pascal-app/core";
|
} 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 { 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";
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "three/tsl";
|
} from "three/tsl";
|
||||||
import { MeshBasicNodeMaterial, PostProcessing } from "three/webgpu";
|
import { MeshBasicNodeMaterial, PostProcessing } from "three/webgpu";
|
||||||
import { ActionMenu } from "./ui/action-menu";
|
import { ActionMenu } from "./ui/action-menu";
|
||||||
|
import { WallEditor } from "./editors/wall/wall-editor";
|
||||||
|
|
||||||
const selectedObjects: Object3D[] = [];
|
const selectedObjects: Object3D[] = [];
|
||||||
|
|
||||||
@@ -37,10 +38,11 @@ export default function Editor() {
|
|||||||
|
|
||||||
<ActionMenu />
|
<ActionMenu />
|
||||||
<Viewer>
|
<Viewer>
|
||||||
<Selector />
|
<DraftSelector />
|
||||||
<Stats />
|
<Stats />
|
||||||
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
||||||
<Passes />
|
<Passes />
|
||||||
|
<WallEditor />
|
||||||
</Viewer>
|
</Viewer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -187,17 +189,31 @@ const Grid = ({
|
|||||||
fadeStrength,
|
fadeStrength,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const handlers = useGridEvents();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh rotation-x={-Math.PI / 2} material={material}>
|
<mesh rotation-x={-Math.PI / 2} material={material} {...handlers}>
|
||||||
<planeGeometry args={[fadeDistance * 2, fadeDistance * 2]} />
|
<planeGeometry args={[fadeDistance * 2, fadeDistance * 2]} />
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Selector = () => {
|
const DraftSelector = () => {
|
||||||
const selectedItemId = useRef<ItemNode["id"] | WallNode["id"]>(null);
|
const selectedItemId = useRef<ItemNode["id"] | WallNode["id"]>(null);
|
||||||
const itemSelectedAt = useRef<number>(0);
|
const itemSelectedAt = useRef<number>(0);
|
||||||
useEffect(() => {
|
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) => {
|
emitter.on("item:click", (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (Date.now() - itemSelectedAt.current < 50) {
|
if (Date.now() - itemSelectedAt.current < 50) {
|
||||||
|
|||||||
@@ -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<Mesh>(null);
|
||||||
|
const drawingLineRef = useRef<Line>(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 (
|
||||||
|
<group>
|
||||||
|
<mesh ref={cursorRef}>
|
||||||
|
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||||
|
<meshStandardMaterial color="red" />
|
||||||
|
</mesh>
|
||||||
|
<group>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<line ref={drawingLineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
||||||
|
<bufferGeometry />
|
||||||
|
<lineDashedNodeMaterial color="blue" linewidth={4} linecap="round" depthTest={false} depthWrite={false} dashSize={2} gapSize={0.1} />
|
||||||
|
</line>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { BuildingNode, LevelNode, useScene } from "@pascal-app/core";
|
||||||
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
export type Phase = "site" | "structure" | "furnish";
|
export type Phase = "site" | "structure" | "furnish";
|
||||||
@@ -45,7 +47,49 @@ type EditorState = {
|
|||||||
|
|
||||||
const useEditor = create<EditorState>()((set, get) => ({
|
const useEditor = create<EditorState>()((set, get) => ({
|
||||||
phase: "site",
|
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",
|
mode: "select",
|
||||||
setMode: (mode) => set({ mode }),
|
setMode: (mode) => set({ mode }),
|
||||||
tool: null,
|
tool: null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import mitt from "mitt";
|
import mitt from "mitt";
|
||||||
import { ItemNode, WallNode } from "../schema";
|
import { BuildingNode, ItemNode, WallNode } from "../schema";
|
||||||
import { AnyNode } from "../schema/types";
|
import { AnyNode } from "../schema/types";
|
||||||
|
|
||||||
// Base event interfaces
|
// Base event interfaces
|
||||||
@@ -16,6 +16,7 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {
|
|||||||
|
|
||||||
export type WallEvent = NodeEvent<WallNode>;
|
export type WallEvent = NodeEvent<WallNode>;
|
||||||
export type ItemEvent = NodeEvent<ItemNode>;
|
export type ItemEvent = NodeEvent<ItemNode>;
|
||||||
|
export type BuildingEvent = NodeEvent<BuildingNode>;
|
||||||
|
|
||||||
// Event suffixes - exported for use in hooks
|
// Event suffixes - exported for use in hooks
|
||||||
export const eventSuffixes = [
|
export const eventSuffixes = [
|
||||||
@@ -41,6 +42,7 @@ type GridEvents = {
|
|||||||
|
|
||||||
type EditorEvents = GridEvents &
|
type EditorEvents = GridEvents &
|
||||||
NodeEvents<"wall", WallEvent> &
|
NodeEvents<"wall", WallEvent> &
|
||||||
NodeEvents<"item", ItemEvent>;
|
NodeEvents<"item", ItemEvent> &
|
||||||
|
NodeEvents<"building", BuildingEvent>;
|
||||||
|
|
||||||
export const emitter = mitt<EditorEvents>();
|
export const emitter = mitt<EditorEvents>();
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type SceneState = {
|
|||||||
loadScene: () => void;
|
loadScene: () => void;
|
||||||
markDirty: (id: AnyNodeId) => void;
|
markDirty: (id: AnyNodeId) => void;
|
||||||
clearDirty: (id: AnyNodeId) => void;
|
clearDirty: (id: AnyNodeId) => void;
|
||||||
|
|
||||||
|
createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useScene = create<SceneState>()((set, get) => ({
|
const useScene = create<SceneState>()((set, get) => ({
|
||||||
@@ -123,6 +125,27 @@ const useScene = create<SceneState>()((set, get) => ({
|
|||||||
clearDirty: (id) => {
|
clearDirty: (id) => {
|
||||||
get().dirtyNodes.delete(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();
|
useScene.getState().loadScene();
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { BuildingNode, LevelNode, useRegistry } from "@pascal-app/core";
|
import { BuildingNode, useRegistry } from "@pascal-app/core";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { Group } from "three";
|
import { Group } from "three";
|
||||||
import { NodeRenderer } from "../node-renderer";
|
import { NodeRenderer } from "../node-renderer";
|
||||||
|
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||||
|
|
||||||
export const BuildingRenderer = ({ node }: { node: BuildingNode }) => {
|
export const BuildingRenderer = ({ node }: { node: BuildingNode }) => {
|
||||||
const ref = useRef<Group>(null!);
|
const ref = useRef<Group>(null!);
|
||||||
|
|
||||||
useRegistry(node.id, node.type, ref);
|
useRegistry(node.id, node.type, ref);
|
||||||
|
const handlers = useNodeEvents(node, "building");
|
||||||
return (
|
return (
|
||||||
<group ref={ref}>
|
<group ref={ref} {...handlers}>
|
||||||
{/* <mesh receiveShadow>
|
|
||||||
<boxGeometry args={[10, 0.1, 10]} />
|
|
||||||
<meshStandardMaterial color="orange" />
|
|
||||||
</mesh> */}
|
|
||||||
{node.children.map((childId) => (
|
{node.children.map((childId) => (
|
||||||
<NodeRenderer key={childId} nodeId={childId} />
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
|||||||
const ref = useRef<Group>(null!);
|
const ref = useRef<Group>(null!);
|
||||||
|
|
||||||
useRegistry(node.id, node.type, ref);
|
useRegistry(node.id, node.type, ref);
|
||||||
|
|
||||||
|
console.log('rendering level:', node.id, node.children);
|
||||||
return (
|
return (
|
||||||
<group ref={ref}>
|
<group ref={ref}>
|
||||||
{/* <mesh receiveShadow>
|
{/* <mesh receiveShadow>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { emitter, useRegistry, WallNode } from "@pascal-app/core";
|
import { useRegistry, WallNode } from "@pascal-app/core";
|
||||||
import { ThreeEvent } from "@react-three/fiber";
|
import { useRef } from "react";
|
||||||
import { useCallback, useRef } from "react";
|
|
||||||
import { Mesh } from "three";
|
import { Mesh } from "three";
|
||||||
import { NodeRenderer } from "../node-renderer";
|
import { NodeRenderer } from "../node-renderer";
|
||||||
import { useNodeEvents } from "../../../hooks/use-node-events";
|
import { useNodeEvents } from "../../../hooks/use-node-events";
|
||||||
|
|||||||
@@ -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<PointerEvent>) => {
|
||||||
|
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<PointerEvent>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
emit("pointerdown", e);
|
||||||
|
},
|
||||||
|
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
emit("pointerup", e);
|
||||||
|
},
|
||||||
|
onClick: (e: ThreeEvent<PointerEvent>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
emit("click", e);
|
||||||
|
},
|
||||||
|
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit("enter", e),
|
||||||
|
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit("leave", e),
|
||||||
|
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit("move", e),
|
||||||
|
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit("double-click", e),
|
||||||
|
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit("context-menu", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BuildingNode,
|
||||||
emitter,
|
emitter,
|
||||||
EventSuffix,
|
EventSuffix,
|
||||||
ItemEvent,
|
ItemEvent,
|
||||||
@@ -7,10 +8,12 @@ import {
|
|||||||
WallNode,
|
WallNode,
|
||||||
} from "@pascal-app/core";
|
} from "@pascal-app/core";
|
||||||
import { ThreeEvent } from "@react-three/fiber";
|
import { ThreeEvent } from "@react-three/fiber";
|
||||||
|
import { BuildingEvent } from "../../../core/src/events/bus";
|
||||||
|
|
||||||
type NodeConfig = {
|
type NodeConfig = {
|
||||||
item: { node: ItemNode; event: ItemEvent };
|
item: { node: ItemNode; event: ItemEvent };
|
||||||
wall: { node: WallNode; event: WallEvent };
|
wall: { node: WallNode; event: WallEvent };
|
||||||
|
building: { node: BuildingNode; event: BuildingEvent };
|
||||||
};
|
};
|
||||||
|
|
||||||
type NodeType = keyof NodeConfig;
|
type NodeType = keyof NodeConfig;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { useGridEvents } from "./hooks/use-grid-events";
|
||||||
|
|
||||||
export { default as Viewer } from "./components/viewer/viewer";
|
export { default as Viewer } from "./components/viewer/viewer";
|
||||||
|
|
||||||
export { default as useViewer } from "./store/use-viewer";
|
export { default as useViewer } from "./store/use-viewer";
|
||||||
|
|
||||||
|
export { useGridEvents } from "./hooks/use-grid-events";
|
||||||
Reference in New Issue
Block a user