From f59e12193d47d9d5a108c336dfa4b8ec313b523b Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 20 Jan 2026 07:56:40 +0900 Subject: [PATCH] wall item checks --- apps/editor/components/editor.tsx | 4 +- .../components/tools/item/item-tool.tsx | 52 ++++++++++++------- .../ui/item-catalog/catalog-items.tsx | 2 +- .../spatial-grid/spatial-grid-manager.ts | 19 ++++--- .../hooks/spatial-grid/wall-spatial-grid.ts | 14 +++-- 5 files changed, 60 insertions(+), 31 deletions(-) diff --git a/apps/editor/components/editor.tsx b/apps/editor/components/editor.tsx index 0248cbd4..67035aad 100644 --- a/apps/editor/components/editor.tsx +++ b/apps/editor/components/editor.tsx @@ -267,13 +267,13 @@ const DraftSelector = () => { const itemSelectedAt = useRef(0); useEffect(() => { emitter.on("building:enter", (event) => { - console.log("Entered building:", event.node.id); + // 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); + // console.log("Leaving building:", event.node.id); selectedObjects.length = 0; }); diff --git a/apps/editor/components/tools/item/item-tool.tsx b/apps/editor/components/tools/item/item-tool.tsx index f23fd80f..ccac63b2 100644 --- a/apps/editor/components/tools/item/item-tool.tsx +++ b/apps/editor/components/tools/item/item-tool.tsx @@ -14,7 +14,7 @@ import { useViewer } from "@pascal-app/viewer"; import { useFrame } from "@react-three/fiber"; import { is } from "@react-three/fiber/dist/declarations/src/core/utils"; import { use, useEffect, useRef } from "react"; -import { BoxGeometry, Line, Mesh, Vector3 } from "three"; +import { BoxGeometry, Line, Mesh, MeshStandardMaterial, Vector3 } from "three"; import { randInt } from "three/src/math/MathUtils.js"; export const ItemTool: React.FC = () => { @@ -23,13 +23,13 @@ export const ItemTool: React.FC = () => { const gridPosition = useRef(new Vector3(0, 0, 0)); const selectedItem = useEditor((state) => state.selectedItem); const { canPlaceOnFloor, canPlaceOnWall } = useSpatialQuery(); + const isOnWall = useRef(false); useEffect(() => { if (!selectedItem) { return; } - let isOnWall = false; let currentWallId: string | null = null; const checkCanPlace = () => { @@ -37,7 +37,7 @@ export const ItemTool: React.FC = () => { if (currentLevelId && draftItem.current) { let placeable = true; if (draftItem.current.asset.attachTo) { - if (!isOnWall || !currentWallId) { + if (!isOnWall.current || !currentWallId) { placeable = false; } else { const result = canPlaceOnWall( @@ -60,10 +60,12 @@ export const ItemTool: React.FC = () => { ).valid; } if (placeable) { - cursorRef.current.material.color.set("green"); + (cursorRef.current.material as MeshStandardMaterial).color.set( + "green", + ); return true; } else { - cursorRef.current.material.color.set("red"); + (cursorRef.current.material as MeshStandardMaterial).color.set("red"); return false; } } @@ -91,7 +93,7 @@ export const ItemTool: React.FC = () => { const onGridMove = (event: GridEvent) => { if (!cursorRef.current) return; - if (isOnWall) return; + if (isOnWall.current) return; gridPosition.current.set( Math.round(event.position[0] * 2) / 2, @@ -103,18 +105,18 @@ export const ItemTool: React.FC = () => { 0, gridPosition.current.z, ); + checkCanPlace(); if (draftItem.current) { draftItem.current.position = [ gridPosition.current.x, 0, gridPosition.current.z, ]; - checkCanPlace(); } }; const onGridClick = (event: GridEvent) => { const { currentLevelId } = useViewer.getState(); - if (isOnWall) return; + if (isOnWall.current) return; if (!currentLevelId || !draftItem.current || !checkCanPlace()) return; @@ -134,7 +136,7 @@ export const ItemTool: React.FC = () => { draftItem.current?.asset.attachTo === "wall-side" ) { event.stopPropagation(); - isOnWall = true; + isOnWall.current = true; currentWallId = event.node.id; gridPosition.current.set( Math.round(event.localPosition[0] * 2) / 2, @@ -155,7 +157,7 @@ export const ItemTool: React.FC = () => { }; const onWallLeave = (event: WallEvent) => { - isOnWall = false; + isOnWall.current = false; currentWallId = null; event.stopPropagation(); if (!draftItem.current) return; @@ -174,7 +176,7 @@ export const ItemTool: React.FC = () => { const onWallClick = (event: WallEvent) => { event.stopPropagation(); - if (!isOnWall) return; + if (!isOnWall.current) return; const currentLevelId = useViewer.getState().currentLevelId; if (!currentLevelId || !draftItem.current || !checkCanPlace()) return; @@ -197,7 +199,7 @@ export const ItemTool: React.FC = () => { }; const onWallMove = (event: WallEvent) => { - if (isOnWall === false) return; + if (isOnWall.current === false) return; event.stopPropagation(); if (!draftItem.current) return; gridPosition.current.set( @@ -206,18 +208,32 @@ export const ItemTool: React.FC = () => { Math.round(event.localPosition[2] * 2) / 2, ); cursorRef.current.position.set( - event.position[0], - event.position[1], - event.position[2], + Math.round(event.position[0] * 2) / 2, + Math.round(event.position[1] * 2) / 2, + Math.round(event.position[2] * 2) / 2, ); - if (draftItem.current) { + const { + node: { start, end }, + } = event; + const dx = end[0] - start[0]; + const dz = end[1] - start[1]; + const { normal } = event; + const wallAngle = Math.atan2(dx, dz); + + cursorRef.current.rotation.y = wallAngle + Math.PI / 2; + const canPlace = checkCanPlace(); + if (draftItem.current && canPlace) { draftItem.current.position = [ gridPosition.current.x, gridPosition.current.y, gridPosition.current.z, ]; + const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id); + if (draftItemMesh) { + draftItemMesh.position.copy(gridPosition.current); + } + useScene.getState().dirtyNodes.add(event.node.id); - checkCanPlace(); } }; @@ -254,7 +270,7 @@ export const ItemTool: React.FC = () => { }, [selectedItem]); useFrame((_, delta) => { - if (draftItem.current) { + if (draftItem.current && !isOnWall.current) { const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id); if (draftItemMesh) { draftItemMesh.position.lerp(gridPosition.current, delta * 20); diff --git a/apps/editor/components/ui/item-catalog/catalog-items.tsx b/apps/editor/components/ui/item-catalog/catalog-items.tsx index bb99eecd..d1bdec46 100644 --- a/apps/editor/components/ui/item-catalog/catalog-items.tsx +++ b/apps/editor/components/ui/item-catalog/catalog-items.tsx @@ -325,7 +325,7 @@ export const CATALOG_ITEMS: AssetInput[] = [ name: "Window", thumbnail: "/items/window-small/thumbnail.webp", src: "/items/window-small/model.glb", - offset: [0, 0, 0], + offset: [0, 0.15, 0], scale: [1, 1, 1], rotation: [0, Math.PI, 0], dimensions: [1, 1.5, 0.1], diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts index f37cb4e4..b3bb9ec2 100644 --- a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts @@ -34,6 +34,11 @@ export class SpatialGridManager { return Math.sqrt(dx * dx + dy * dy); } + private getWallHeight(wallId: string): number { + const wall = this.walls.get(wallId); + return wall?.height ?? 2.5; // Default wall height + } + // Called when nodes change handleNodeCreated(node: AnyNode, levelId: string) { if (node.type === "wall") { @@ -52,16 +57,16 @@ export class SpatialGridManager { if (wallLength > 0) { const [width, height] = item.asset.dimensions; const halfW = width / wallLength / 2; - const halfH = height / 2; // Calculate t from local X position (position[0] is distance along wall) const t = item.position[0] / wallLength; + // position[1] is the bottom of the item this.getWallGrid(levelId).insert({ itemId: item.id, wallId: wallId, tStart: t - halfW, tEnd: t + halfW, - yStart: item.position[1] - halfH, - yEnd: item.position[1] + halfH, + yStart: item.position[1], + yEnd: item.position[1] + height, }); } } @@ -95,16 +100,16 @@ export class SpatialGridManager { if (wallLength > 0) { const [width, height] = item.asset.dimensions; const halfW = width / wallLength / 2; - const halfH = height / 2; // Calculate t from local X position (position[0] is distance along wall) const t = item.position[0] / wallLength; + // position[1] is the bottom of the item this.getWallGrid(levelId).insert({ itemId: item.id, wallId: wallId, tStart: t - halfW, tEnd: t + halfW, - yStart: item.position[1] - halfH, - yEnd: item.position[1] + halfH, + yStart: item.position[1], + yEnd: item.position[1] + height, }); } } @@ -165,12 +170,14 @@ export class SpatialGridManager { if (wallLength === 0) { return { valid: false, conflictIds: [] }; } + const wallHeight = this.getWallHeight(wallId); // Convert local X position to parametric t (0-1) const tCenter = localX / wallLength; const [itemWidth, itemHeight] = dimensions; return this.getWallGrid(levelId).canPlaceOnWall( wallId, wallLength, + wallHeight, tCenter, itemWidth, localY, diff --git a/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts b/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts index 677da88a..af421c0b 100644 --- a/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts +++ b/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts @@ -14,18 +14,24 @@ export class WallSpatialGrid { canPlaceOnWall( wallId: string, wallLength: number, + wallHeight: number, tCenter: number, itemWidth: number, - yCenter: number, + yBottom: number, itemHeight: number, ignoreIds: string[] = [], ): { valid: boolean; conflictIds: string[] } { const halfW = itemWidth / wallLength / 2; - const halfH = itemHeight / 2; const tStart = tCenter - halfW; const tEnd = tCenter + halfW; - const yStart = yCenter - halfH; - const yEnd = yCenter + halfH; + // yBottom is the bottom of the item, so yEnd = yBottom + itemHeight + const yStart = yBottom; + const yEnd = yBottom + itemHeight; + + // Check wall boundaries + if (tStart < 0 || tEnd > 1 || yStart < 0 || yEnd > wallHeight) { + return { valid: false, conflictIds: [] }; + } const existing = this.wallItems.get(wallId) ?? []; const ignoreSet = new Set(ignoreIds);