Add Three.js 3D room renderer with interactive viewer

renderer.js: HouseRenderer class that loads house JSON and renders
rooms as 3D geometry - walls with door/window cutouts, floors with
material colors, translucent ceilings, orbit camera controls, room
click selection and highlighting.

index.html: Viewer page with sidebar showing floor switcher and
room list with areas. Click rooms in 3D or sidebar to focus camera.
Uses Three.js via CDN importmap (no build step needed).
This commit is contained in:
m
2026-02-07 11:32:08 +01:00
parent 46ed91307b
commit 0d3a4dddf5
2 changed files with 543 additions and 0 deletions

151
src/index.html Normal file
View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>House Design Viewer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; overflow: hidden; }
#viewer { width: 100vw; height: 100vh; }
#sidebar {
position: fixed;
top: 0;
right: 0;
width: 280px;
height: 100vh;
background: rgba(255, 255, 255, 0.95);
border-left: 1px solid #ddd;
padding: 16px;
overflow-y: auto;
z-index: 10;
}
#sidebar h2 { font-size: 16px; margin-bottom: 12px; color: #333; }
#sidebar h3 { font-size: 13px; margin: 12px 0 6px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
.floor-btn {
display: inline-block;
padding: 6px 14px;
margin: 2px 4px 2px 0;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 13px;
}
.floor-btn.active { background: #4a90d9; color: #fff; border-color: #4a90d9; }
.room-item {
padding: 8px 10px;
margin: 2px 0;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
display: flex;
justify-content: space-between;
align-items: center;
}
.room-item:hover { background: #e8f0fe; }
.room-item.active { background: #4a90d9; color: #fff; }
.room-item .area { font-size: 11px; opacity: 0.7; }
#info {
position: fixed;
bottom: 16px;
left: 16px;
background: rgba(0,0,0,0.7);
color: #fff;
padding: 8px 14px;
border-radius: 6px;
font-size: 13px;
z-index: 10;
}
</style>
</head>
<body>
<div id="viewer"></div>
<div id="sidebar">
<h2 id="house-name">Loading...</h2>
<h3>Floors</h3>
<div id="floor-buttons"></div>
<h3>Rooms</h3>
<div id="room-list"></div>
</div>
<div id="info">Click a room to select it. Scroll to zoom, drag to orbit.</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}
}
</script>
<script type="module">
import { HouseRenderer } from './renderer.js';
const viewer = document.getElementById('viewer');
const renderer = new HouseRenderer(viewer);
let selectedRoom = null;
renderer.loadHouse('../data/sample-house.json').then(house => {
document.getElementById('house-name').textContent = house.name;
buildFloorButtons();
buildRoomList();
});
function buildFloorButtons() {
const container = document.getElementById('floor-buttons');
container.innerHTML = '';
for (const floor of renderer.getFloors()) {
const btn = document.createElement('button');
btn.className = 'floor-btn' + (floor.index === renderer.currentFloor ? ' active' : '');
btn.textContent = floor.name;
btn.addEventListener('click', () => {
renderer.showFloor(floor.index);
document.querySelectorAll('.floor-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
buildRoomList();
selectedRoom = null;
});
container.appendChild(btn);
}
}
function buildRoomList() {
const container = document.getElementById('room-list');
container.innerHTML = '';
for (const room of renderer.getRooms()) {
const item = document.createElement('div');
item.className = 'room-item';
item.dataset.roomId = room.id;
item.innerHTML = `<span>${room.name}</span><span class="area">${room.area} m²</span>`;
item.addEventListener('click', () => selectRoom(room.id));
container.appendChild(item);
}
}
function selectRoom(roomId) {
selectedRoom = roomId;
renderer.focusRoom(roomId);
document.querySelectorAll('.room-item').forEach(el => {
el.classList.toggle('active', el.dataset.roomId === roomId);
});
const room = renderer.getRooms().find(r => r.id === roomId);
if (room) {
document.getElementById('info').textContent = `${room.name} (${room.nameEN}) — ${room.area}`;
}
}
viewer.addEventListener('roomclick', (e) => {
selectRoom(e.detail.roomId);
});
</script>
</body>
</html>