Adds a "Create Custom Furniture" button at the bottom of the catalog sidebar that expands into a form with fields for name, dimensions (W/D/H in meters), color picker, and category selector. Submitting creates a simple box-geometry catalog item and adds it to the in-memory catalog for immediate placement into rooms.
505 lines
14 KiB
HTML
505 lines
14 KiB
HTML
<!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; }
|
|
|
|
.theme-btn {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
margin: 2px 3px 2px 0;
|
|
border: 2px solid #ccc;
|
|
border-radius: 4px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
}
|
|
.theme-btn.active { border-color: #4a90d9; }
|
|
.theme-swatch {
|
|
display: inline-block;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 2px;
|
|
vertical-align: middle;
|
|
margin-right: 4px;
|
|
border: 1px solid rgba(0,0,0,0.15);
|
|
}
|
|
|
|
#info {
|
|
position: fixed;
|
|
bottom: 16px;
|
|
left: 260px;
|
|
background: rgba(0,0,0,0.7);
|
|
color: #fff;
|
|
padding: 8px 14px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
z-index: 10;
|
|
}
|
|
|
|
/* Catalog panel (left sidebar) */
|
|
#catalog-panel {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 250px;
|
|
height: 100vh;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-right: 1px solid #ddd;
|
|
z-index: 10;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.catalog-panel h3 {
|
|
font-size: 13px;
|
|
padding: 12px 12px 0;
|
|
color: #666;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.catalog-search {
|
|
padding: 12px 12px 8px;
|
|
}
|
|
.catalog-search-input {
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
.catalog-search-input:focus {
|
|
border-color: #4a90d9;
|
|
}
|
|
.catalog-categories {
|
|
padding: 0 12px 8px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
.catalog-cat-btn {
|
|
padding: 3px 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
}
|
|
.catalog-cat-btn.active {
|
|
background: #4a90d9;
|
|
color: #fff;
|
|
border-color: #4a90d9;
|
|
}
|
|
.catalog-items {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 12px 12px;
|
|
}
|
|
.catalog-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px;
|
|
margin: 2px 0;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
}
|
|
.catalog-item:hover {
|
|
background: #e8f0fe;
|
|
}
|
|
.catalog-item-swatch {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 4px;
|
|
flex-shrink: 0;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
}
|
|
.catalog-item-info {
|
|
flex: 1;
|
|
margin-left: 8px;
|
|
min-width: 0;
|
|
}
|
|
.catalog-item-name {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.catalog-item-dims {
|
|
font-size: 10px;
|
|
color: #888;
|
|
margin-top: 2px;
|
|
}
|
|
.catalog-item-add {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
color: #4a90d9;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.catalog-item-add:hover {
|
|
background: #4a90d9;
|
|
color: #fff;
|
|
border-color: #4a90d9;
|
|
}
|
|
.catalog-empty {
|
|
text-align: center;
|
|
color: #999;
|
|
padding: 20px 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Custom furniture creator */
|
|
.catalog-create-btn {
|
|
display: block;
|
|
width: calc(100% - 24px);
|
|
margin: 8px 12px;
|
|
padding: 8px;
|
|
border: 1px dashed #4a90d9;
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
color: #4a90d9;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
.catalog-create-btn:hover {
|
|
background: #e8f0fe;
|
|
}
|
|
.catalog-create-form {
|
|
padding: 0 12px 12px;
|
|
border-top: 1px solid #eee;
|
|
margin-top: 4px;
|
|
}
|
|
.catalog-create-form label {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: #666;
|
|
margin-top: 8px;
|
|
margin-bottom: 2px;
|
|
}
|
|
.catalog-create-form input,
|
|
.catalog-create-form select {
|
|
width: 100%;
|
|
padding: 5px 8px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
outline: none;
|
|
}
|
|
.catalog-create-form input:focus,
|
|
.catalog-create-form select:focus {
|
|
border-color: #4a90d9;
|
|
}
|
|
.catalog-create-dims {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
.catalog-create-dims > div {
|
|
flex: 1;
|
|
}
|
|
.catalog-create-dims label {
|
|
margin-top: 0 !important;
|
|
}
|
|
.catalog-create-color-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.catalog-create-color-row input[type="color"] {
|
|
width: 32px;
|
|
height: 28px;
|
|
padding: 1px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.catalog-create-color-row input[type="text"] {
|
|
flex: 1;
|
|
}
|
|
.catalog-create-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-top: 10px;
|
|
}
|
|
.catalog-create-actions button {
|
|
flex: 1;
|
|
padding: 6px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.catalog-create-actions .btn-submit {
|
|
background: #4a90d9;
|
|
color: #fff;
|
|
border-color: #4a90d9;
|
|
}
|
|
.catalog-create-actions .btn-submit:hover {
|
|
background: #3a7bc8;
|
|
}
|
|
.catalog-create-actions .btn-cancel {
|
|
background: #fff;
|
|
}
|
|
.catalog-create-actions .btn-cancel:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.export-section {
|
|
margin-top: 16px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid #ddd;
|
|
}
|
|
.export-btn {
|
|
display: inline-block;
|
|
padding: 6px 12px;
|
|
margin: 3px 4px 3px 0;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.export-btn:hover { background: #f0f0f0; }
|
|
.export-btn:active { background: #e0e0e0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viewer"></div>
|
|
|
|
<div id="catalog-panel"></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>
|
|
<h3>Theme</h3>
|
|
<div id="theme-buttons"></div>
|
|
<div class="export-section">
|
|
<h3>File</h3>
|
|
<button class="export-btn" id="btn-save">Save JSON</button>
|
|
<button class="export-btn" id="btn-load">Load JSON</button>
|
|
<button class="export-btn" id="btn-screenshot">Screenshot</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="info">Click a room to select it. Click furniture to edit. 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';
|
|
import { DesignState } from './state.js';
|
|
import { InteractionManager } from './interaction.js';
|
|
import { ThemeManager } from './themes.js';
|
|
import { ExportManager } from './export.js';
|
|
import { CatalogPanel } from './catalog.js';
|
|
|
|
const viewer = document.getElementById('viewer');
|
|
const houseRenderer = new HouseRenderer(viewer);
|
|
|
|
let selectedRoom = null;
|
|
let designState = null;
|
|
let interaction = null;
|
|
let themeManager = null;
|
|
let exportManager = null;
|
|
let catalogPanel = null;
|
|
|
|
houseRenderer.loadHouse('../data/sample-house.json').then(async (house) => {
|
|
document.getElementById('house-name').textContent = house.name;
|
|
await houseRenderer.loadCatalog('../data/furniture-catalog.json');
|
|
const design = await houseRenderer.loadDesign('../designs/sample-house-design.json');
|
|
|
|
// Initialize state and interaction manager
|
|
designState = new DesignState(design);
|
|
interaction = new InteractionManager(houseRenderer, designState);
|
|
|
|
interaction.onChange((type, detail) => {
|
|
if (type === 'select') {
|
|
document.getElementById('info').textContent =
|
|
`Selected: ${detail.itemName} — R to rotate, Delete to remove, Escape to deselect`;
|
|
} else if (type === 'deselect') {
|
|
document.getElementById('info').textContent =
|
|
'Click a room to select it. Click furniture to edit. Scroll to zoom, drag to orbit.';
|
|
}
|
|
});
|
|
|
|
themeManager = new ThemeManager(houseRenderer);
|
|
exportManager = new ExportManager(houseRenderer, designState);
|
|
catalogPanel = new CatalogPanel(document.getElementById('catalog-panel'), {
|
|
renderer: houseRenderer,
|
|
state: designState,
|
|
interaction
|
|
});
|
|
buildFloorButtons();
|
|
buildRoomList();
|
|
buildThemeButtons();
|
|
wireExportButtons();
|
|
}).catch(err => {
|
|
document.getElementById('house-name').textContent = 'Error loading data';
|
|
document.getElementById('info').textContent = err.message;
|
|
});
|
|
|
|
function buildFloorButtons() {
|
|
const container = document.getElementById('floor-buttons');
|
|
container.innerHTML = '';
|
|
for (const floor of houseRenderer.getFloors()) {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'floor-btn' + (floor.index === houseRenderer.currentFloor ? ' active' : '');
|
|
btn.textContent = floor.name;
|
|
btn.addEventListener('click', () => {
|
|
houseRenderer.showFloor(floor.index);
|
|
document.querySelectorAll('.floor-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
buildRoomList();
|
|
selectedRoom = null;
|
|
if (catalogPanel) catalogPanel.setSelectedRoom(null);
|
|
});
|
|
container.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function buildRoomList() {
|
|
const container = document.getElementById('room-list');
|
|
container.innerHTML = '';
|
|
for (const room of houseRenderer.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;
|
|
houseRenderer.focusRoom(roomId);
|
|
document.querySelectorAll('.room-item').forEach(el => {
|
|
el.classList.toggle('active', el.dataset.roomId === roomId);
|
|
});
|
|
if (catalogPanel) catalogPanel.setSelectedRoom(roomId);
|
|
const room = houseRenderer.getRooms().find(r => r.id === roomId);
|
|
if (room) {
|
|
document.getElementById('info').textContent = `${room.name} (${room.nameEN}) — ${room.area} m²`;
|
|
}
|
|
}
|
|
|
|
viewer.addEventListener('roomclick', (e) => {
|
|
selectRoom(e.detail.roomId);
|
|
});
|
|
|
|
function buildThemeButtons() {
|
|
const container = document.getElementById('theme-buttons');
|
|
container.innerHTML = '';
|
|
for (const theme of themeManager.getThemes()) {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'theme-btn' + (theme.id === themeManager.currentTheme ? ' active' : '');
|
|
btn.innerHTML = `<span class="theme-swatch" style="background:${theme.swatch}"></span>${theme.name}`;
|
|
btn.addEventListener('click', () => {
|
|
themeManager.applyTheme(theme.id);
|
|
document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
buildRoomList(); // re-render room list since floor was rebuilt
|
|
});
|
|
container.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function wireExportButtons() {
|
|
document.getElementById('btn-save').addEventListener('click', () => {
|
|
exportManager.exportDesignJSON();
|
|
});
|
|
document.getElementById('btn-load').addEventListener('click', () => {
|
|
exportManager.importDesignJSON();
|
|
});
|
|
document.getElementById('btn-screenshot').addEventListener('click', () => {
|
|
exportManager.exportScreenshot();
|
|
});
|
|
|
|
// Ctrl+S / Cmd+S to save design
|
|
window.addEventListener('keydown', (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
e.preventDefault();
|
|
if (exportManager) {
|
|
exportManager.exportDesignJSON();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update info bar when a design is loaded from file
|
|
viewer.addEventListener('designloaded', (e) => {
|
|
document.getElementById('info').textContent = `Loaded design: ${e.detail.name}`;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|