different level edition + fix

This commit is contained in:
wass08
2026-01-20 08:26:47 +09:00
parent f59e12193d
commit dabaefc57a
8 changed files with 99 additions and 57 deletions
+24 -2
View File
@@ -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;
+39 -26
View File
@@ -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}
+1 -1
View File
@@ -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> {
+7 -6
View File
@@ -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)}`;
+1 -1
View File
@@ -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);