From 8bf933ed366feba5cebcd7419d59fe39bfad58b8 Mon Sep 17 00:00:00 2001 From: wass08 Date: Sun, 18 Jan 2026 10:54:02 +0900 Subject: [PATCH] draft spatial grid --- apps/editor/components/editor.tsx | 3 + .../components/tools/item/item-tool.tsx | 41 +++- .../ui/item-catalog/catalog-items.tsx | 12 +- apps/editor/store/use-editor.tsx | 5 +- .../spatial-grid/spatial-grid-manager.ts | 194 ++++++++++++++++++ .../hooks/spatial-grid/spatial-grid-sync.ts | 62 ++++++ .../src/hooks/spatial-grid/spatial-grid.ts | 161 +++++++++++++++ .../hooks/spatial-grid/use-spatial-query.ts | 26 +++ .../hooks/spatial-grid/wall-spatial-grid.ts | 97 +++++++++ packages/core/src/index.ts | 12 +- packages/core/src/schema/nodes/item.ts | 5 + .../core/src/systems/wall/wall-system.tsx | 2 +- .../viewer/src/components/viewer/viewer.tsx | 3 +- 13 files changed, 604 insertions(+), 19 deletions(-) create mode 100644 packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts create mode 100644 packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts create mode 100644 packages/core/src/hooks/spatial-grid/spatial-grid.ts create mode 100644 packages/core/src/hooks/spatial-grid/use-spatial-query.ts create mode 100644 packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts diff --git a/apps/editor/components/editor.tsx b/apps/editor/components/editor.tsx index 1b699bea..aa1ad1fc 100644 --- a/apps/editor/components/editor.tsx +++ b/apps/editor/components/editor.tsx @@ -2,6 +2,7 @@ import { emitter, + initSpatialGridSync, ItemNode, sceneRegistry, useScene, @@ -32,6 +33,8 @@ import { ToolManager } from "./tools/tool-manager"; const selectedObjects: Object3D[] = []; +initSpatialGridSync(); + export default function Editor() { return (
diff --git a/apps/editor/components/tools/item/item-tool.tsx b/apps/editor/components/tools/item/item-tool.tsx index 988c99f5..6a1d9803 100644 --- a/apps/editor/components/tools/item/item-tool.tsx +++ b/apps/editor/components/tools/item/item-tool.tsx @@ -6,19 +6,21 @@ import { sceneRegistry, useRegistry, useScene, + useSpatialQuery, WallNode, } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; import { useFrame } from "@react-three/fiber"; import { use, useEffect, useRef } from "react"; -import { Line, Mesh, Vector3 } from "three"; +import { BoxGeometry, Line, Mesh, Vector3 } from "three"; import { randInt } from "three/src/math/MathUtils.js"; export const ItemTool: React.FC = () => { - const cursorRef = useRef(null); + const cursorRef = useRef(null!); const draftItem = useRef(null); const gridPosition = useRef(new Vector3(0, 0, 0)); const selectedItem = useEditor((state) => state.selectedItem); + const { canPlace } = useSpatialQuery(); useEffect(() => { if (!selectedItem) { @@ -53,7 +55,7 @@ export const ItemTool: React.FC = () => { ); cursorRef.current.position.set( gridPosition.current.x, - 0.1, + 0, gridPosition.current.z, ); if (draftItem.current) { @@ -62,15 +64,29 @@ export const ItemTool: React.FC = () => { 0, gridPosition.current.z, ]; + + const currentLevelId = useViewer.getState().currentLevelId; + if (currentLevelId) { + const placeable = canPlace( + currentLevelId, + [gridPosition.current.x, 0, gridPosition.current.z], + selectedItem.dimensions, + [0, 0, 0], + ); + console.log( + "placeable", + placeable, + [gridPosition.current.x, 0, gridPosition.current.z], + selectedItem.dimensions, + ); + } } }; const onGridClick = (event: GridEvent) => { const { currentLevelId } = useViewer.getState(); - console.log("oh", currentLevelId, draftItem.current); if (!currentLevelId || !draftItem.current) return; - console.log("oh"); useScene.temporal.getState().resume(); useScene.getState().updateNode(draftItem.current.id, { position: [ @@ -88,6 +104,17 @@ export const ItemTool: React.FC = () => { emitter.on("grid:move", onGridMove); emitter.on("grid:click", onGridClick); + const setupBoundingBox = () => { + const boxGeometry = new BoxGeometry( + selectedItem.dimensions[0], + selectedItem.dimensions[1], + selectedItem.dimensions[2], + ); + boxGeometry.translate(0, selectedItem.dimensions[1] / 2, 0); + cursorRef.current.geometry = boxGeometry; + }; + setupBoundingBox(); + return () => { if (draftItem.current) { useScene.getState().deleteNode(draftItem.current.id); @@ -109,8 +136,8 @@ export const ItemTool: React.FC = () => { return ( - - + + ); diff --git a/apps/editor/components/ui/item-catalog/catalog-items.tsx b/apps/editor/components/ui/item-catalog/catalog-items.tsx index 9827f4c4..12ba8dab 100644 --- a/apps/editor/components/ui/item-catalog/catalog-items.tsx +++ b/apps/editor/components/ui/item-catalog/catalog-items.tsx @@ -5,16 +5,16 @@ export const CATALOG_ITEMS: AssetInput[] = [ name: "Couch", thumbnail: "/items/couch-medium/thumbnail.webp", src: "/items/couch-medium/model.glb", - scale: [0.4, 0.4, 0.4], - dimensions: [4, 2, 2], + scale: [0.35, 0.35, 0.35], + dimensions: [2, 0.8, 1], }, { category: "furniture", name: "Small Couch", thumbnail: "/items/couch-small/thumbnail.webp", src: "/items/couch-small/model.glb", - scale: [0.4, 0.4, 0.4], - dimensions: [3, 2, 2], + scale: [0.35, 0.35, 0.35], + dimensions: [1, 0.8, 1], }, { category: "furniture", @@ -154,10 +154,10 @@ export const CATALOG_ITEMS: AssetInput[] = [ name: "Wall Art", thumbnail: "/items/wall-art-06/thumbnail.webp", src: "/items/wall-art-06/model.glb", - offset: [0, 1, 0.15], + offset: [0, 0.5, 0], scale: [1, 1, 1], rotation: [0, Math.PI, 0], - dimensions: [2, 2, 1], + dimensions: [1, 1, 0.1], attachTo: "wall-side", }, { diff --git a/apps/editor/store/use-editor.tsx b/apps/editor/store/use-editor.tsx index a39d887f..4c3231dc 100644 --- a/apps/editor/store/use-editor.tsx +++ b/apps/editor/store/use-editor.tsx @@ -8,6 +8,7 @@ import { } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; import { create } from "zustand"; +import { Asset } from "../../../packages/core/src/schema/nodes/item"; export type Phase = "site" | "structure" | "furnish"; @@ -54,8 +55,8 @@ type EditorState = { setTool: (tool: Tool | null) => void; catalogCategory: CatalogCategory | null; setCatalogCategory: (category: CatalogCategory | null) => void; - selectedItem: AssetInput | null; - setSelectedItem: (item: AssetInput) => void; + selectedItem: Asset | null; + setSelectedItem: (item: Asset) => void; }; const useEditor = create()((set, get) => ({ diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts new file mode 100644 index 00000000..2ec270af --- /dev/null +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts @@ -0,0 +1,194 @@ +import { AnyNode, ItemNode, WallNode } from "../../schema"; +import { SpatialGrid } from "./spatial-grid"; +import { WallSpatialGrid } from "./wall-spatial-grid"; + +export class SpatialGridManager { + private floorGrids = new Map(); // levelId -> grid + private wallGrids = new Map(); // levelId -> wall grid + private walls = new Map(); // wallId -> wall data (for length calculations) + + constructor(private cellSize = 0.5) {} + + private getFloorGrid(levelId: string): SpatialGrid { + if (!this.floorGrids.has(levelId)) { + this.floorGrids.set( + levelId, + new SpatialGrid({ cellSize: this.cellSize }), + ); + } + return this.floorGrids.get(levelId)!; + } + + private getWallGrid(levelId: string): WallSpatialGrid { + if (!this.wallGrids.has(levelId)) { + this.wallGrids.set(levelId, new WallSpatialGrid()); + } + return this.wallGrids.get(levelId)!; + } + + private getWallLength(wallId: string): number { + const wall = this.walls.get(wallId); + if (!wall) return 0; + const dx = wall.end[0] - wall.start[0]; + const dy = wall.end[1] - wall.start[1]; + return Math.sqrt(dx * dx + dy * dy); + } + + // Called when nodes change + handleNodeCreated(node: AnyNode, levelId: string) { + if (node.type === "wall") { + const wall = node as WallNode; + this.walls.set(wall.id, wall); + } else if (node.type === "item") { + const item = node as ItemNode; + if ( + item.asset.attachTo === "wall" || + item.asset.attachTo === "wall-side" + ) { + // Wall-attached item + if (item.wallId && item.wallT !== undefined) { + const wallLength = this.getWallLength(item.wallId); + if (wallLength > 0) { + const [width, height] = item.asset.dimensions; + const halfW = width / wallLength / 2; + const halfH = height / 2; + this.getWallGrid(levelId).insert({ + itemId: item.id, + wallId: item.wallId, + tStart: item.wallT - halfW, + tEnd: item.wallT + halfW, + yStart: item.position[1] - halfH, + yEnd: item.position[1] + halfH, + }); + } + } + } else if (!item.asset.attachTo) { + // Floor item + this.getFloorGrid(levelId).insert( + item.id, + item.position, + item.asset.dimensions, + item.rotation, + ); + console.log( + "inserting floor item", + item.id, + item.position, + item.asset.dimensions, + ); + } + } + } + + handleNodeUpdated(node: AnyNode, levelId: string) { + if (node.type === "wall") { + const wall = node as WallNode; + this.walls.set(wall.id, wall); + } else if (node.type === "item") { + const item = node as ItemNode; + if ( + item.asset.attachTo === "wall" || + item.asset.attachTo === "wall-side" + ) { + // Remove old placement and re-insert + this.getWallGrid(levelId).removeByItemId(item.id); + if (item.wallId && item.wallT !== undefined) { + const wallLength = this.getWallLength(item.wallId); + if (wallLength > 0) { + const [width, height] = item.asset.dimensions; + const halfW = width / wallLength / 2; + const halfH = height / 2; + this.getWallGrid(levelId).insert({ + itemId: item.id, + wallId: item.wallId, + tStart: item.wallT - halfW, + tEnd: item.wallT + halfW, + yStart: item.position[1] - halfH, + yEnd: item.position[1] + halfH, + }); + } + } + } else if (!item.asset.attachTo) { + this.getFloorGrid(levelId).update( + item.id, + item.position, + item.asset.dimensions, + item.rotation, + ); + } + } + } + + handleNodeDeleted(nodeId: string, nodeType: string, levelId: string) { + if (nodeType === "wall") { + this.walls.delete(nodeId); + // Remove all items attached to this wall from the spatial grid + const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId); + return removedItemIds; // Caller can use this to delete the items from scene + } else if (nodeType === "item") { + this.getFloorGrid(levelId).remove(nodeId); + this.getWallGrid(levelId).removeByItemId(nodeId); + } + return []; + } + + // Query methods + canPlaceOnFloor( + levelId: string, + position: [number, number, number], + dimensions: [number, number, number], + rotation: [number, number, number], + ignoreIds?: string[], + ) { + const grid = this.getFloorGrid(levelId); + console.log("canPlaceOnFloor - grid item count:", grid.getItemCount()); + return grid.canPlace( + position, + dimensions, + rotation, + ignoreIds, + ); + } + + canPlaceOnWall( + levelId: string, + wallId: string, + tCenter: number, + itemWidth: number, + yCenter: number, + itemHeight: number, + ignoreIds?: string[], + ) { + const wallLength = this.getWallLength(wallId); + if (wallLength === 0) { + return { valid: false, conflictIds: [] }; + } + return this.getWallGrid(levelId).canPlaceOnWall( + wallId, + wallLength, + tCenter, + itemWidth, + yCenter, + itemHeight, + ignoreIds, + ); + } + + getWallForItem(levelId: string, itemId: string): string | undefined { + return this.getWallGrid(levelId).getWallForItem(itemId); + } + + clearLevel(levelId: string) { + this.floorGrids.delete(levelId); + this.wallGrids.delete(levelId); + } + + clear() { + this.floorGrids.clear(); + this.wallGrids.clear(); + this.walls.clear(); + } +} + +// Singleton instance +export const spatialGridManager = new SpatialGridManager(); diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts b/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts new file mode 100644 index 00000000..2e8f4872 --- /dev/null +++ b/packages/core/src/hooks/spatial-grid/spatial-grid-sync.ts @@ -0,0 +1,62 @@ +import { AnyNode } from "../../schema"; +import useScene from "../../store/use-scene"; +import { spatialGridManager } from "./spatial-grid-manager"; + +function resolveLevelId(node: AnyNode, nodes: Record): string { + // If the node itself is a level + if (node.type === "level") return node.id; + + // Walk up parent chain to find level + // This assumes you track parentId or can derive it + let current: AnyNode | undefined = node; + + while (current && current.parentId) { + if (current.type === "level") return current.id; + // Find parent (you might need to add parentId to your schema or derive it) + current = nodes[current.parentId]; + } + + return "default"; // fallback for orphaned items +} + +// Call this once at app initialization +export function initSpatialGridSync() { + const store = useScene; + + // Subscribe to all changes + store.subscribe((state, prevState) => { + // Detect added nodes + for (const [id, node] of Object.entries(state.nodes)) { + if (!prevState.nodes[id as AnyNode["id"]]) { + const levelId = resolveLevelId(node, state.nodes); + spatialGridManager.handleNodeCreated(node, levelId); + } + } + + // Detect removed nodes + for (const [id, node] of Object.entries(prevState.nodes)) { + if (!state.nodes[id as AnyNode["id"]]) { + const levelId = resolveLevelId(node, prevState.nodes); + spatialGridManager.handleNodeDeleted(id, node.type, levelId); + } + } + + // Detect updated nodes (only items with position/rotation changes) + for (const [id, node] of Object.entries(state.nodes)) { + const prev = prevState.nodes[id as AnyNode["id"]]; + if (prev && node.type === "item" && prev.type === "item") { + if ( + !arraysEqual(node.position, prev.position) || + !arraysEqual(node.rotation, prev.rotation) + ) { + const levelId = resolveLevelId(node, state.nodes); + spatialGridManager.handleNodeUpdated(node, levelId); + } + } + } + }); +} + +function arraysEqual(a: number[], b: number[]): boolean { + return a.length === b.length && a.every((v, i) => v === b[i]); +} diff --git a/packages/core/src/hooks/spatial-grid/spatial-grid.ts b/packages/core/src/hooks/spatial-grid/spatial-grid.ts new file mode 100644 index 00000000..7da0f362 --- /dev/null +++ b/packages/core/src/hooks/spatial-grid/spatial-grid.ts @@ -0,0 +1,161 @@ +type CellKey = `${number},${number}`; + +interface GridCell { + itemIds: Set; +} + +interface SpatialGridConfig { + cellSize: number; // e.g., 0.5 meters = Sims-style half-tile +} + +export class SpatialGrid { + private cells = new Map(); + private itemCells = new Map>(); // reverse lookup + + constructor(private config: SpatialGridConfig) {} + + private posToCell(x: number, z: number): [number, number] { + return [ + Math.floor(x / this.config.cellSize), + Math.floor(z / this.config.cellSize), + ]; + } + + private cellKey(cx: number, cz: number): CellKey { + return `${cx},${cz}`; + } + + // Get all cells an item occupies based on its AABB + private getItemCells( + position: [number, number, number], + dimensions: [number, number, number], + rotation: [number, number, number], + ): CellKey[] { + // Simplified: axis-aligned bounding box + // For full rotation support, compute rotated corners + const [x, , z] = position; + const [w, , d] = dimensions; + const yRot = rotation[1]; // Y-axis rotation + + // Compute rotated footprint (simplified for 90° increments) + const cos = Math.abs(Math.cos(yRot)); + const sin = Math.abs(Math.sin(yRot)); + const rotatedW = w * cos + d * sin; + const rotatedD = w * sin + d * cos; + + const minX = x - rotatedW / 2; + const maxX = x + rotatedW / 2; + const minZ = z - rotatedD / 2; + const maxZ = z + rotatedD / 2; + + const [minCx, minCz] = this.posToCell(minX, minZ); + const [maxCx, maxCz] = this.posToCell(maxX, maxZ); + + const keys: CellKey[] = []; + for (let cx = minCx; cx <= maxCx; cx++) { + for (let cz = minCz; cz <= maxCz; cz++) { + keys.push(this.cellKey(cx, cz)); + } + } + return keys; + } + + // Register an item + insert( + itemId: string, + position: [number, number, number], + dimensions: [number, number, number], + rotation: [number, number, number], + ) { + const cellKeys = this.getItemCells(position, dimensions, rotation); + + this.itemCells.set(itemId, new Set(cellKeys)); + + for (const key of cellKeys) { + if (!this.cells.has(key)) { + this.cells.set(key, { itemIds: new Set() }); + } + this.cells.get(key)!.itemIds.add(itemId); + } + } + + // Remove an item + remove(itemId: string) { + const cellKeys = this.itemCells.get(itemId); + if (!cellKeys) return; + + for (const key of cellKeys) { + const cell = this.cells.get(key); + if (cell) { + cell.itemIds.delete(itemId); + if (cell.itemIds.size === 0) { + this.cells.delete(key); + } + } + } + this.itemCells.delete(itemId); + } + + // Update = remove + insert + update( + itemId: string, + position: [number, number, number], + dimensions: [number, number, number], + rotation: [number, number, number], + ) { + this.remove(itemId); + this.insert(itemId, position, dimensions, rotation); + } + + // Query: is this placement valid? + canPlace( + position: [number, number, number], + dimensions: [number, number, number], + rotation: [number, number, number], + ignoreIds: string[] = [], + ): { valid: boolean; conflictIds: string[] } { + const cellKeys = this.getItemCells(position, dimensions, rotation); + const ignoreSet = new Set(ignoreIds); + const conflicts = new Set(); + console.log("checking cells", cellKeys); + + for (const key of cellKeys) { + const cell = this.cells.get(key); + if (cell) { + for (const id of cell.itemIds) { + if (!ignoreSet.has(id)) { + conflicts.add(id); + } + } + } + } + + return { + valid: conflicts.size === 0, + conflictIds: [...conflicts], + }; + } + + // Query: get all items near a point (for snapping, selection, etc.) + queryRadius(x: number, z: number, radius: number): string[] { + const cellRadius = Math.ceil(radius / this.config.cellSize); + const [cx, cz] = this.posToCell(x, z); + const found = new Set(); + + for (let dx = -cellRadius; dx <= cellRadius; dx++) { + for (let dz = -cellRadius; dz <= cellRadius; dz++) { + const cell = this.cells.get(this.cellKey(cx + dx, cz + dz)); + if (cell) { + for (const id of cell.itemIds) { + found.add(id); + } + } + } + } + return [...found]; + } + + getItemCount(): number { + return this.itemCells.size; + } +} diff --git a/packages/core/src/hooks/spatial-grid/use-spatial-query.ts b/packages/core/src/hooks/spatial-grid/use-spatial-query.ts new file mode 100644 index 00000000..62577408 --- /dev/null +++ b/packages/core/src/hooks/spatial-grid/use-spatial-query.ts @@ -0,0 +1,26 @@ +import { useCallback } from "react"; +import { spatialGridManager } from "./spatial-grid-manager"; +import { LevelNode } from "../../schema"; + +export function useSpatialQuery() { + const canPlace = useCallback( + ( + levelId: LevelNode["id"], + position: [number, number, number], + dimensions: [number, number, number], + rotation: [number, number, number], + ignoreIds?: string[], + ) => { + return spatialGridManager.canPlaceOnFloor( + levelId, + position, + dimensions, + rotation, + ignoreIds, + ); + }, + [], + ); + + return { canPlace }; +} diff --git a/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts b/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts new file mode 100644 index 00000000..c89d4e6b --- /dev/null +++ b/packages/core/src/hooks/spatial-grid/wall-spatial-grid.ts @@ -0,0 +1,97 @@ +interface WallItemPlacement { + itemId: string; + wallId: string; + tStart: number; // 0-1 parametric position along wall + tEnd: number; + yStart: number; // height range + yEnd: number; +} + +export class WallSpatialGrid { + private wallItems = new Map(); // wallId -> placements + private itemToWall = new Map(); // itemId -> wallId (reverse lookup) + + canPlaceOnWall( + wallId: string, + wallLength: number, + tCenter: number, + itemWidth: number, + yCenter: 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; + + const existing = this.wallItems.get(wallId) ?? []; + const ignoreSet = new Set(ignoreIds); + const conflicts: string[] = []; + + for (const placement of existing) { + if (ignoreSet.has(placement.itemId)) continue; + + const tOverlap = tStart < placement.tEnd && tEnd > placement.tStart; + const yOverlap = yStart < placement.yEnd && yEnd > placement.yStart; + + if (tOverlap && yOverlap) { + conflicts.push(placement.itemId); + } + } + + return { valid: conflicts.length === 0, conflictIds: conflicts }; + } + + insert(placement: WallItemPlacement) { + const { wallId, itemId } = placement; + + if (!this.wallItems.has(wallId)) { + this.wallItems.set(wallId, []); + } + this.wallItems.get(wallId)!.push(placement); + this.itemToWall.set(itemId, wallId); + } + + remove(wallId: string, itemId: string) { + const items = this.wallItems.get(wallId); + if (items) { + const idx = items.findIndex((p) => p.itemId === itemId); + if (idx !== -1) items.splice(idx, 1); + } + this.itemToWall.delete(itemId); + } + + // The missing method! + removeByItemId(itemId: string) { + const wallId = this.itemToWall.get(itemId); + if (wallId) { + this.remove(wallId, itemId); + } + } + + // Useful for when a wall is deleted - remove all items on it + removeWall(wallId: string): string[] { + const items = this.wallItems.get(wallId) ?? []; + const removedIds = items.map((p) => p.itemId); + + for (const itemId of removedIds) { + this.itemToWall.delete(itemId); + } + this.wallItems.delete(wallId); + + return removedIds; // Return removed item IDs in case you need to delete them from scene + } + + // Get which wall an item is on + getWallForItem(itemId: string): string | undefined { + return this.itemToWall.get(itemId); + } + + clear() { + this.wallItems.clear(); + this.itemToWall.clear(); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c5744d8b..83c4ed8a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,13 +7,21 @@ export { useRegistry, } from "./hooks/scene-registry/scene-registry"; +export { useSpatialQuery } from "./hooks/spatial-grid/use-spatial-query"; +export { initSpatialGridSync } from "./hooks/spatial-grid/spatial-grid-sync"; + // Systems -export { LevelSystem } from "../../viewer/src/systems/level/level-system"; export { WallSystem } from "./systems/wall/wall-system"; // Events export { emitter, eventSuffixes } from "./events/bus"; -export type { ItemEvent, WallEvent, NodeEvent, GridEvent, EventSuffix } from "./events/bus"; +export type { + ItemEvent, + WallEvent, + NodeEvent, + GridEvent, + EventSuffix, +} from "./events/bus"; // Schema export * from "./schema"; diff --git a/packages/core/src/schema/nodes/item.ts b/packages/core/src/schema/nodes/item.ts index 7b4d1355..8d99dd5c 100644 --- a/packages/core/src/schema/nodes/item.ts +++ b/packages/core/src/schema/nodes/item.ts @@ -16,6 +16,7 @@ const assetSchema = z.object({ }); export type AssetInput = z.input; +export type Asset = z.infer; export const ItemNode = BaseNode.extend({ id: objectId("item"), @@ -24,6 +25,10 @@ export const ItemNode = BaseNode.extend({ rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]), side: z.enum(["front", "back"]).optional(), + // Wall attachment properties (only used when asset.attachTo is "wall" or "wall-side") + wallId: z.string().optional(), + wallT: z.number().optional(), // 0-1 parametric position along wall + asset: assetSchema, }).describe(dedent`Item node - used to represent a item in the building - position: position in level coordinate system (or parent coordinate system if attached) diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx index 1cb2850f..174dfd51 100644 --- a/packages/core/src/systems/wall/wall-system.tsx +++ b/packages/core/src/systems/wall/wall-system.tsx @@ -72,7 +72,7 @@ export function generateExtrudedWall( const end = new THREE.Vector2(wallNode.end[0], wallNode.end[1]); const length = start.distanceTo(end); const height = wallNode.height || 2.5; - const thickness = wallNode.thickness || 0.2; + const thickness = wallNode.thickness || 0.1; // 2. Create the Main Wall Shape (a rectangle in 2D) // We draw this on the XY plane, where X is "along the wall" and Y is "height" diff --git a/packages/viewer/src/components/viewer/viewer.tsx b/packages/viewer/src/components/viewer/viewer.tsx index 13fe3116..1b0f73aa 100644 --- a/packages/viewer/src/components/viewer/viewer.tsx +++ b/packages/viewer/src/components/viewer/viewer.tsx @@ -3,10 +3,11 @@ import { Bvh, Environment, OrbitControls } from "@react-three/drei"; import { Canvas, ThreeToJSXElements } from "@react-three/fiber"; -import { LevelSystem, WallSystem } from "@pascal-app/core"; +import { WallSystem } from "@pascal-app/core"; import { extend } from "@react-three/fiber"; import * as THREE from "three/webgpu"; import { SceneRenderer } from "../renderers/scene-renderer"; +import { LevelSystem } from "../../systems/level/level-system"; declare module "@react-three/fiber" { interface ThreeElements extends ThreeToJSXElements {}