From 08248c6cadbba047dcd671b7d9d2420b52e85f4e Mon Sep 17 00:00:00 2001 From: m Date: Sat, 7 Feb 2026 12:24:58 +0100 Subject: [PATCH] Add theme system with 5 presets and selector UI - 5 themes: Standard, Modern, Warm, Dark, Scandinavian - ThemeManager mutates shared COLORS, clears cache, re-renders - Updates scene background and light intensities per theme - Theme selector buttons with color swatch in sidebar - Exported COLORS from renderer.js for theme access --- src/index.html | 45 ++++++++++++++++++ src/themes.js | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 src/themes.js diff --git a/src/index.html b/src/index.html index 961617d..ae04e49 100644 --- a/src/index.html +++ b/src/index.html @@ -52,6 +52,28 @@ .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; @@ -74,6 +96,8 @@

Rooms

+

Theme

+
Click a room to select it. Click furniture to edit. Scroll to zoom, drag to orbit.
@@ -90,6 +114,7 @@ import { HouseRenderer } from './renderer.js'; import { DesignState } from './state.js'; import { InteractionManager } from './interaction.js'; + import { ThemeManager } from './themes.js'; const viewer = document.getElementById('viewer'); const houseRenderer = new HouseRenderer(viewer); @@ -97,6 +122,7 @@ let selectedRoom = null; let designState = null; let interaction = null; + let themeManager = null; houseRenderer.loadHouse('../data/sample-house.json').then(async (house) => { document.getElementById('house-name').textContent = house.name; @@ -117,8 +143,10 @@ } }); + themeManager = new ThemeManager(houseRenderer); buildFloorButtons(); buildRoomList(); + buildThemeButtons(); }).catch(err => { document.getElementById('house-name').textContent = 'Error loading data'; document.getElementById('info').textContent = err.message; @@ -170,6 +198,23 @@ 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 = `${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); + } + } diff --git a/src/themes.js b/src/themes.js new file mode 100644 index 0000000..01e6b4f --- /dev/null +++ b/src/themes.js @@ -0,0 +1,124 @@ +import { COLORS } from './renderer.js'; + +const THEMES = { + default: { + name: 'Standard', + swatch: '#e8e0d4', + colors: { + wall: { exterior: 0xe8e0d4, interior: 0xf5f0eb }, + floor: { tile: 0xc8beb0, hardwood: 0xb5894e }, + ceiling: 0xfaf8f5, + door: 0x8b6914, + window: 0x87ceeb, + windowFrame: 0xd0d0d0, + grid: 0xcccccc, + selected: 0x4a90d9 + }, + scene: { background: 0xf0f0f0, ambientIntensity: 0.6, directionalIntensity: 0.8 } + }, + modern: { + name: 'Modern', + swatch: '#f5f5f5', + colors: { + wall: { exterior: 0xf5f5f5, interior: 0xffffff }, + floor: { tile: 0xe0e0e0, hardwood: 0xc4a882 }, + ceiling: 0xffffff, + door: 0x333333, + window: 0xa8d4f0, + windowFrame: 0x666666, + grid: 0xe0e0e0, + selected: 0x2196f3 + }, + scene: { background: 0xfafafa, ambientIntensity: 0.7, directionalIntensity: 0.6 } + }, + warm: { + name: 'Warm', + swatch: '#ddd0b8', + colors: { + wall: { exterior: 0xddd0b8, interior: 0xf0e8d8 }, + floor: { tile: 0xb8a890, hardwood: 0x9b6b3a }, + ceiling: 0xf5efe5, + door: 0x6b4423, + window: 0x8bc4e0, + windowFrame: 0x8b7355, + grid: 0xc8b8a0, + selected: 0xd48b2c + }, + scene: { background: 0xf5efe5, ambientIntensity: 0.5, directionalIntensity: 0.9 } + }, + dark: { + name: 'Dark', + swatch: '#3a3a3a', + colors: { + wall: { exterior: 0x3a3a3a, interior: 0x4a4a4a }, + floor: { tile: 0x2a2a2a, hardwood: 0x5a4030 }, + ceiling: 0x333333, + door: 0x5a4030, + window: 0x4080b0, + windowFrame: 0x555555, + grid: 0x444444, + selected: 0x64b5f6 + }, + scene: { background: 0x222222, ambientIntensity: 0.4, directionalIntensity: 1.0 } + }, + scandinavian: { + name: 'Scandi', + swatch: '#f0ece4', + colors: { + wall: { exterior: 0xf0ece4, interior: 0xfaf6f0 }, + floor: { tile: 0xe8ddd0, hardwood: 0xd4b88c }, + ceiling: 0xffffff, + door: 0xc4a87a, + window: 0xc0ddf0, + windowFrame: 0xb0b0b0, + grid: 0xd8d8d8, + selected: 0x5b9bd5 + }, + scene: { background: 0xf8f6f2, ambientIntensity: 0.65, directionalIntensity: 0.7 } + } +}; + +/** + * ThemeManager — applies visual themes by mutating COLORS and re-rendering. + */ +export class ThemeManager { + constructor(renderer) { + this.renderer = renderer; + this.currentTheme = 'default'; + } + + applyTheme(themeId) { + const theme = THEMES[themeId]; + if (!theme) return; + this.currentTheme = themeId; + + // Mutate the shared COLORS object + Object.assign(COLORS.wall, theme.colors.wall); + Object.assign(COLORS.floor, theme.colors.floor); + COLORS.ceiling = theme.colors.ceiling; + COLORS.door = theme.colors.door; + COLORS.window = theme.colors.window; + COLORS.windowFrame = theme.colors.windowFrame; + COLORS.grid = theme.colors.grid; + COLORS.selected = theme.colors.selected; + + // Update scene background and lights + this.renderer.scene.background.setHex(theme.scene.background); + this.renderer.scene.traverse(child => { + if (child.isAmbientLight) child.intensity = theme.scene.ambientIntensity; + if (child.isDirectionalLight) child.intensity = theme.scene.directionalIntensity; + }); + + // Clear cached materials/geometry and re-render to pick up new colors + this.renderer._clearFloor(); + this.renderer.showFloor(this.renderer.currentFloor); + } + + getThemes() { + return Object.entries(THEMES).map(([id, t]) => ({ + id, + name: t.name, + swatch: t.swatch + })); + } +}