draft wall builder
This commit is contained in:
@@ -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() {
|
||||
|
||||
<ActionMenu />
|
||||
<Viewer>
|
||||
<Selector />
|
||||
<DraftSelector />
|
||||
<Stats />
|
||||
<Grid cellColor="#666" sectionColor="#999" fadeDistance={30} />
|
||||
<Passes />
|
||||
<WallEditor />
|
||||
</Viewer>
|
||||
</div>
|
||||
);
|
||||
@@ -187,17 +189,31 @@ const Grid = ({
|
||||
fadeStrength,
|
||||
]);
|
||||
|
||||
const handlers = useGridEvents();
|
||||
|
||||
return (
|
||||
<mesh rotation-x={-Math.PI / 2} material={material}>
|
||||
<mesh rotation-x={-Math.PI / 2} material={material} {...handlers}>
|
||||
<planeGeometry args={[fadeDistance * 2, fadeDistance * 2]} />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
const Selector = () => {
|
||||
const DraftSelector = () => {
|
||||
const selectedItemId = useRef<ItemNode["id"] | WallNode["id"]>(null);
|
||||
const itemSelectedAt = useRef<number>(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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
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<EditorState>()((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,
|
||||
|
||||
Reference in New Issue
Block a user