different level edition + fix
This commit is contained in:
@@ -13,7 +13,7 @@ import { useGridEvents, useViewer, Viewer } from "@pascal-app/viewer";
|
|||||||
import { CameraControls, CameraControlsImpl, Stats } from "@react-three/drei";
|
import { CameraControls, CameraControlsImpl, 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";
|
||||||
import { Color, Object3D } from "three";
|
import { Color, MathUtils, Mesh, Object3D } from "three";
|
||||||
import { outline } from "three/addons/tsl/display/OutlineNode.js";
|
import { outline } from "three/addons/tsl/display/OutlineNode.js";
|
||||||
import {
|
import {
|
||||||
color,
|
color,
|
||||||
@@ -254,9 +254,31 @@ const Grid = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handlers = useGridEvents();
|
const handlers = useGridEvents();
|
||||||
|
const gridRef = useRef<Mesh>(null!);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
const currentLevelId = useViewer.getState().currentLevelId;
|
||||||
|
let targetY = 0;
|
||||||
|
if (currentLevelId) {
|
||||||
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||||
|
if (levelMesh) {
|
||||||
|
targetY = levelMesh.position.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gridRef.current.position.y = MathUtils.lerp(
|
||||||
|
gridRef.current.position.y,
|
||||||
|
targetY,
|
||||||
|
12 * delta,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh rotation-x={-Math.PI / 2} material={material} {...handlers}>
|
<mesh
|
||||||
|
rotation-x={-Math.PI / 2}
|
||||||
|
material={material}
|
||||||
|
{...handlers}
|
||||||
|
ref={gridRef}
|
||||||
|
>
|
||||||
<planeGeometry args={[fadeDistance * 2, fadeDistance * 2]} />
|
<planeGeometry args={[fadeDistance * 2, fadeDistance * 2]} />
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
gridPosition.current.y,
|
gridPosition.current.y,
|
||||||
gridPosition.current.z,
|
gridPosition.current.z,
|
||||||
],
|
],
|
||||||
name: "Draft Item",
|
name: selectedItem.name,
|
||||||
asset: selectedItem,
|
asset: selectedItem,
|
||||||
});
|
});
|
||||||
useScene.getState().createNode(draftItem.current, currentLevelId);
|
useScene.getState().createNode(draftItem.current, currentLevelId);
|
||||||
@@ -98,11 +98,11 @@ export const ItemTool: React.FC = () => {
|
|||||||
gridPosition.current.set(
|
gridPosition.current.set(
|
||||||
Math.round(event.position[0] * 2) / 2,
|
Math.round(event.position[0] * 2) / 2,
|
||||||
0,
|
0,
|
||||||
Math.round(event.position[1] * 2) / 2,
|
Math.round(event.position[2] * 2) / 2,
|
||||||
);
|
);
|
||||||
cursorRef.current.position.set(
|
cursorRef.current.position.set(
|
||||||
gridPosition.current.x,
|
gridPosition.current.x,
|
||||||
0,
|
event.position[1],
|
||||||
gridPosition.current.z,
|
gridPosition.current.z,
|
||||||
);
|
);
|
||||||
checkCanPlace();
|
checkCanPlace();
|
||||||
@@ -212,6 +212,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
Math.round(event.position[1] * 2) / 2,
|
Math.round(event.position[1] * 2) / 2,
|
||||||
Math.round(event.position[2] * 2) / 2,
|
Math.round(event.position[2] * 2) / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
node: { start, end },
|
node: { start, end },
|
||||||
} = event;
|
} = event;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { emitter, GridEvent, useScene, WallNode } from "@pascal-app/core";
|
import { emitter, GridEvent, useScene, WallNode } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { useEffect, useRef } from "react"
|
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();
|
||||||
const { createNode } = useScene.getState();
|
const { createNode } = useScene.getState();
|
||||||
@@ -16,30 +15,31 @@ const commitWallDrawing = (start: [number, number], end: [number, number]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const WallTool: React.FC = () => {
|
export const WallTool: React.FC = () => {
|
||||||
|
|
||||||
const cursorRef = useRef<Mesh>(null);
|
const cursorRef = useRef<Mesh>(null);
|
||||||
const drawingLineRef = useRef<Line>(null!);
|
const drawingLineRef = useRef<Line>(null!);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
let buildingState = 0;
|
let buildingState = 0;
|
||||||
const startingPoint = new Vector3(0, 0, 0);
|
const startingPoint = new Vector3(0, 0, 0);
|
||||||
const endingPoint = new Vector3(0, 0, 0);
|
const endingPoint = new Vector3(0, 0, 0);
|
||||||
let gridPosition: [number, number] = [0, 0];
|
let gridPosition: [number, number] = [0, 0];
|
||||||
|
|
||||||
|
drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint]);
|
||||||
drawingLineRef.current.geometry.setFromPoints([
|
|
||||||
startingPoint,
|
|
||||||
endingPoint,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
if (!cursorRef.current) return;
|
if (!cursorRef.current) return;
|
||||||
|
|
||||||
gridPosition = [Math.round(event.position[0] * 2) / 2, Math.round(event.position[1] * 2) / 2];
|
gridPosition = [
|
||||||
cursorRef.current.position.set(gridPosition[0], 0.1, gridPosition[1]);
|
Math.round(event.position[0] * 2) / 2,
|
||||||
|
Math.round(event.position[2] * 2) / 2,
|
||||||
|
];
|
||||||
|
cursorRef.current.position.set(
|
||||||
|
gridPosition[0],
|
||||||
|
event.position[1],
|
||||||
|
gridPosition[1],
|
||||||
|
);
|
||||||
if (buildingState === 1) {
|
if (buildingState === 1) {
|
||||||
endingPoint.set(gridPosition[0], 0.1, gridPosition[1]);
|
endingPoint.set(gridPosition[0], event.position[1], gridPosition[1]);
|
||||||
}
|
}
|
||||||
drawingLineRef.current.geometry.setFromPoints([
|
drawingLineRef.current.geometry.setFromPoints([
|
||||||
startingPoint,
|
startingPoint,
|
||||||
@@ -48,26 +48,26 @@ export const WallTool: React.FC = () => {
|
|||||||
};
|
};
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
if (buildingState === 0) {
|
if (buildingState === 0) {
|
||||||
startingPoint.set(gridPosition[0], 0.1, gridPosition[1]);
|
startingPoint.set(gridPosition[0], event.position[1], gridPosition[1]);
|
||||||
buildingState = 1;
|
buildingState = 1;
|
||||||
console.log('starting building at:', startingPoint);
|
console.log("starting building at:", startingPoint);
|
||||||
drawingLineRef.current.visible = true;
|
drawingLineRef.current.visible = true;
|
||||||
|
|
||||||
} else if (buildingState === 1) {
|
} else if (buildingState === 1) {
|
||||||
commitWallDrawing([startingPoint.x, startingPoint.z], [gridPosition[0], gridPosition[1]]);
|
commitWallDrawing(
|
||||||
|
[startingPoint.x, startingPoint.z],
|
||||||
|
[endingPoint.x, endingPoint.z],
|
||||||
|
);
|
||||||
drawingLineRef.current.visible = false;
|
drawingLineRef.current.visible = false;
|
||||||
buildingState = 0;
|
buildingState = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
emitter.on("grid:move", onGridMove);
|
||||||
emitter.on('grid:move', onGridMove);
|
emitter.on("grid:click", onGridClick);
|
||||||
emitter.on('grid:click', onGridClick);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off('grid:move', onGridMove);
|
emitter.off("grid:move", onGridMove);
|
||||||
emitter.off('grid:click', onGridClick);
|
emitter.off("grid:click", onGridClick);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -79,11 +79,24 @@ export const WallTool: React.FC = () => {
|
|||||||
</mesh>
|
</mesh>
|
||||||
<group>
|
<group>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<line ref={drawingLineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
<line
|
||||||
|
ref={drawingLineRef}
|
||||||
|
frustumCulled={false}
|
||||||
|
renderOrder={1}
|
||||||
|
visible={false}
|
||||||
|
>
|
||||||
<bufferGeometry />
|
<bufferGeometry />
|
||||||
<lineDashedNodeMaterial color="blue" linewidth={4} linecap="round" depthTest={false} depthWrite={false} dashSize={2} gapSize={0.1} />
|
<lineDashedNodeMaterial
|
||||||
|
color="blue"
|
||||||
|
linewidth={4}
|
||||||
|
linecap="round"
|
||||||
|
depthTest={false}
|
||||||
|
depthWrite={false}
|
||||||
|
dashSize={2}
|
||||||
|
gapSize={0.1}
|
||||||
|
/>
|
||||||
</line>
|
</line>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ export function WallTreeNode({ node, depth }: WallTreeNodeProps) {
|
|||||||
|
|
||||||
const wallLength = Math.sqrt(
|
const wallLength = Math.sqrt(
|
||||||
Math.pow(node.end[0] - node.start[0], 2) +
|
Math.pow(node.end[0] - node.start[0], 2) +
|
||||||
Math.pow(node.end[1] - node.start[1], 2)
|
Math.pow(node.end[1] - node.start[1], 2),
|
||||||
).toFixed(1);
|
).toFixed(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TreeNodeWrapper
|
<TreeNodeWrapper
|
||||||
icon={<Square className="w-3.5 h-3.5" />}
|
icon={<Square className="w-3.5 h-3.5" />}
|
||||||
label={node.name || `Wall (${wallLength}m)`}
|
label={node.name || `Wall (${wallLength}m/${node.height || 2.5}m)`}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
hasChildren={node.children.length > 0}
|
hasChildren={node.children.length > 0}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AnyNode } from "../schema/types";
|
|||||||
|
|
||||||
// Base event interfaces
|
// Base event interfaces
|
||||||
export interface GridEvent {
|
export interface GridEvent {
|
||||||
position: [number, number];
|
position: [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
||||||
|
|||||||
@@ -90,26 +90,27 @@ const useScene = create<SceneState>()(
|
|||||||
start: [5, 5],
|
start: [5, 5],
|
||||||
end: [5, 0],
|
end: [5, 0],
|
||||||
children: [],
|
children: [],
|
||||||
parentId: level0.id,
|
parentId: level1.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const window1 = ItemNode.parse({
|
const window1 = ItemNode.parse({
|
||||||
type: "item",
|
type: "item",
|
||||||
name: "Window",
|
name: "Window",
|
||||||
position: [2.5, 0.5, 0],
|
position: [2.5, 0.5, 0],
|
||||||
parentId: wall0.id,
|
parentId: wall3.id,
|
||||||
asset: {
|
asset: {
|
||||||
name: "Round Window",
|
name: "Round Window",
|
||||||
thumbnail: "/items/window-round/thumbnail.png",
|
thumbnail: "/items/window-small/thumbnail.png",
|
||||||
category: "windows",
|
category: "windows",
|
||||||
attachTo: "wall",
|
attachTo: "wall",
|
||||||
src: "/items/window-round/model.glb",
|
src: "/items/window-small/model.glb",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
wall0.children.push(window1.id);
|
wall3.children.push(window1.id);
|
||||||
|
|
||||||
level0.children.push(wall0.id, wall1.id, wall2.id, wall3.id);
|
level0.children.push(wall0.id, wall1.id, wall2.id);
|
||||||
|
level1.children.push(wall3.id);
|
||||||
|
|
||||||
building.children.push(level0.id, level1.id, level2.id);
|
building.children.push(level0.id, level1.id, level2.id);
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ export function generateExtrudedWall(
|
|||||||
wallNode.end[0] - wallNode.start[0],
|
wallNode.end[0] - wallNode.start[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get the wall mesh's world Y position (from level offset)
|
||||||
|
const wallMesh = sceneRegistry.nodes.get(wallNode.id) as THREE.Mesh;
|
||||||
|
const wallWorldY = wallMesh?.getWorldPosition(new THREE.Vector3()).y ?? 0;
|
||||||
|
|
||||||
childrenNodes.forEach((child) => {
|
childrenNodes.forEach((child) => {
|
||||||
// Only process items that are intended to be wall cutouts
|
// Only process items that are intended to be wall cutouts
|
||||||
if (child.type !== "item") return;
|
if (child.type !== "item") return;
|
||||||
@@ -112,7 +116,7 @@ export function generateExtrudedWall(
|
|||||||
const cutoutMesh = childMesh.getObjectByName("cutout") as THREE.Mesh;
|
const cutoutMesh = childMesh.getObjectByName("cutout") as THREE.Mesh;
|
||||||
if (!cutoutMesh) return;
|
if (!cutoutMesh) return;
|
||||||
|
|
||||||
const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle);
|
const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle, wallWorldY);
|
||||||
if (holePath) {
|
if (holePath) {
|
||||||
shape.holes.push(holePath);
|
shape.holes.push(holePath);
|
||||||
}
|
}
|
||||||
@@ -138,12 +142,13 @@ export function generateExtrudedWall(
|
|||||||
* Wall-local space:
|
* Wall-local space:
|
||||||
* - Origin at wall start point
|
* - Origin at wall start point
|
||||||
* - X axis runs along the wall (toward end point)
|
* - X axis runs along the wall (toward end point)
|
||||||
* - Y axis is height (world Y)
|
* - Y axis is height (relative to wall's world Y position)
|
||||||
*/
|
*/
|
||||||
function createPathFromCutout(
|
function createPathFromCutout(
|
||||||
cutoutMesh: THREE.Mesh,
|
cutoutMesh: THREE.Mesh,
|
||||||
wallStart: [number, number],
|
wallStart: [number, number],
|
||||||
wallAngle: number,
|
wallAngle: number,
|
||||||
|
wallWorldY: number,
|
||||||
): THREE.Path | null {
|
): THREE.Path | null {
|
||||||
const geometry = cutoutMesh.geometry;
|
const geometry = cutoutMesh.geometry;
|
||||||
if (!geometry) return null;
|
if (!geometry) return null;
|
||||||
@@ -177,9 +182,9 @@ function createPathFromCutout(
|
|||||||
// 2. Rotate around Y axis to align wall with local X axis
|
// 2. Rotate around Y axis to align wall with local X axis
|
||||||
// The wall shape is drawn on XY plane, so we need:
|
// The wall shape is drawn on XY plane, so we need:
|
||||||
// - localX = distance along wall
|
// - localX = distance along wall
|
||||||
// - localY = height (world Y)
|
// - localY = height relative to wall's Y position
|
||||||
const localX = worldX * cosAngle - worldZ * sinAngle;
|
const localX = worldX * cosAngle - worldZ * sinAngle;
|
||||||
const localY = v3.y; // Height stays the same
|
const localY = v3.y - wallWorldY; // Subtract wall's world Y to get local height
|
||||||
|
|
||||||
// Create a key for deduplication (with small tolerance)
|
// Create a key for deduplication (with small tolerance)
|
||||||
const key = `${localX.toFixed(4)},${localY.toFixed(4)}`;
|
const key = `${localX.toFixed(4)},${localY.toFixed(4)}`;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function useGridEvents() {
|
|||||||
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
||||||
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`;
|
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`;
|
||||||
const payload: GridEvent = {
|
const payload: GridEvent = {
|
||||||
position: [e.point.x, e.point.z],
|
position: [e.point.x, e.point.y, e.point.z],
|
||||||
};
|
};
|
||||||
|
|
||||||
emitter.emit(eventKey, payload);
|
emitter.emit(eventKey, payload);
|
||||||
|
|||||||
Reference in New Issue
Block a user