From 4d76d9525fa7d09f69de1d832f75ad56cb2712dc Mon Sep 17 00:00:00 2001 From: m Date: Sat, 7 Feb 2026 11:42:58 +0100 Subject: [PATCH] Add furniture placement to 3D renderer - loadCatalog() and loadDesign() methods on HouseRenderer - Builds Three.js meshes from catalog part definitions (box/cylinder) - Places furniture in room-local coordinates with rotation - Furniture clears and re-renders on floor switch - index.html loads catalog and design after house data --- src/index.html | 4 +- src/renderer.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index a2b9430..05d793c 100644 --- a/src/index.html +++ b/src/index.html @@ -94,8 +94,10 @@ let selectedRoom = null; - renderer.loadHouse('../data/sample-house.json').then(house => { + renderer.loadHouse('../data/sample-house.json').then(async (house) => { document.getElementById('house-name').textContent = house.name; + await renderer.loadCatalog('../data/furniture-catalog.json'); + await renderer.loadDesign('../designs/sample-house-design.json'); buildFloorButtons(); buildRoomList(); }); diff --git a/src/renderer.js b/src/renderer.js index bbf6099..1cc451b 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -22,9 +22,12 @@ export class HouseRenderer { constructor(container) { this.container = container; this.houseData = null; + this.catalogData = null; + this.designData = null; this.currentFloor = 0; this.roomMeshes = new Map(); this.roomLabels = new Map(); + this.furnitureMeshes = new Map(); this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0xf0f0f0); @@ -87,6 +90,23 @@ export class HouseRenderer { return this.houseData; } + async loadCatalog(url) { + const res = await fetch(url); + this.catalogData = await res.json(); + this._catalogIndex = new Map(); + for (const item of this.catalogData.items) { + this._catalogIndex.set(item.id, item); + } + return this.catalogData; + } + + async loadDesign(url) { + const res = await fetch(url); + this.designData = await res.json(); + this._placeFurnitureForFloor(); + return this.designData; + } + showFloor(index) { this.currentFloor = index; // Clear existing room meshes @@ -95,6 +115,11 @@ export class HouseRenderer { } this.roomMeshes.clear(); this.roomLabels.clear(); + // Clear existing furniture + for (const group of this.furnitureMeshes.values()) { + this.scene.remove(group); + } + this.furnitureMeshes.clear(); const floor = this.houseData.floors[index]; if (!floor) return; @@ -102,6 +127,8 @@ export class HouseRenderer { for (const room of floor.rooms) { this._renderRoom(room, floor.ceilingHeight); } + + this._placeFurnitureForFloor(); } _renderRoom(room, ceilingHeight) { @@ -376,6 +403,76 @@ export class HouseRenderer { } } + _placeFurnitureForFloor() { + if (!this.designData || !this.catalogData || !this.houseData) return; + + const floor = this.houseData.floors[this.currentFloor]; + if (!floor) return; + + const floorRoomIds = new Set(floor.rooms.map(r => r.id)); + + for (const roomDesign of this.designData.rooms) { + if (!floorRoomIds.has(roomDesign.roomId)) continue; + + const room = floor.rooms.find(r => r.id === roomDesign.roomId); + if (!room) continue; + + for (let i = 0; i < roomDesign.furniture.length; i++) { + const placement = roomDesign.furniture[i]; + const catalogItem = this._catalogIndex.get(placement.catalogId); + if (!catalogItem) continue; + + const mesh = this._buildFurnitureMesh(catalogItem); + // Position in room-local coords, then offset by room position + const rx = room.position.x + placement.position.x; + const rz = room.position.y + placement.position.z; + mesh.position.set(rx, 0, rz); + mesh.rotation.y = -(placement.rotation * Math.PI) / 180; + + const key = `${roomDesign.roomId}-${placement.instanceId || placement.catalogId}-${i}`; + mesh.userData = { + isFurniture: true, + catalogId: placement.catalogId, + roomId: roomDesign.roomId, + itemName: catalogItem.name + }; + + this.scene.add(mesh); + this.furnitureMeshes.set(key, mesh); + } + } + } + + _buildFurnitureMesh(catalogItem) { + const group = new THREE.Group(); + const meshDef = catalogItem.mesh; + if (!meshDef || meshDef.type !== 'group' || !meshDef.parts) return group; + + for (const part of meshDef.parts) { + let geo; + if (part.geometry === 'box') { + geo = new THREE.BoxGeometry(part.size[0], part.size[1], part.size[2]); + } else if (part.geometry === 'cylinder') { + geo = new THREE.CylinderGeometry(part.radius, part.radius, part.height, 16); + } else { + continue; + } + + const mat = new THREE.MeshStandardMaterial({ + color: new THREE.Color(part.color), + roughness: 0.7 + }); + + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(part.position[0], part.position[1], part.position[2]); + mesh.castShadow = true; + mesh.receiveShadow = true; + group.add(mesh); + } + + return group; + } + _onResize() { const w = this.container.clientWidth; const h = this.container.clientHeight;