diff --git a/DESIGN-interactive-features.md b/DESIGN-interactive-features.md new file mode 100644 index 0000000..6218055 --- /dev/null +++ b/DESIGN-interactive-features.md @@ -0,0 +1,985 @@ +# Interactive Features Design — Phase 2 + +**Task:** t-05700 +**Date:** 2026-02-07 +**Role:** Inventor + +## Overview + +Four interactive feature areas to transform the viewer into an editor: + +1. **Drag-and-drop furniture** — move, rotate, place from catalog +2. **Room editing** — resize rooms, edit wall openings +3. **Style themes** — switchable color/material palettes +4. **Export** — save designs, screenshot, share + +Design principle: **Enhance the existing vanilla JS + Three.js stack**. No framework rewrite. Each feature is an independent module that plugs into `HouseRenderer`. + +--- + +## Architecture Strategy + +### Module Structure + +``` +src/ + renderer.js (existing — 3D core) + index.html (existing — entry point) + interaction.js (NEW — drag/drop, selection, gizmos) + room-editor.js (NEW — room resize, wall editing) + themes.js (NEW — style theme system) + export.js (NEW — save/export functionality) + ui-panels.js (NEW — sidebar panels, catalog browser, property inspector) +``` + +Each module exports a class that receives the `HouseRenderer` instance and extends it via composition (not inheritance). This keeps renderer.js stable while adding capabilities. + +```javascript +// Pattern for all modules: +export class InteractionManager { + constructor(renderer) { + this.renderer = renderer; + // Hook into renderer's scene, camera, controls + } + dispose() { /* cleanup */ } +} +``` + +### State Management + +Currently state lives in scattered instance variables. For interactive editing, we need a lightweight state layer: + +```javascript +// src/state.js — simple observable state +export class DesignState { + constructor(initialDesign) { + this._state = structuredClone(initialDesign); + this._listeners = new Set(); + this._undoStack = []; + this._redoStack = []; + } + + // Read + get design() { return this._state; } + getRoomDesign(roomId) { ... } + getFurniture(roomId, index) { ... } + + // Write (all mutations go through here) + updateFurniture(roomId, index, changes) { + this._pushUndo(); + Object.assign(this._state.rooms[...].furniture[index], changes); + this._notify('furniture-update', { roomId, index }); + } + + moveFurniture(roomId, index, newPosition) { ... } + rotateFurniture(roomId, index, degrees) { ... } + addFurniture(roomId, catalogId, position, rotation) { ... } + removeFurniture(roomId, index) { ... } + + // Undo/Redo + undo() { ... } + redo() { ... } + + // Observers + onChange(listener) { this._listeners.add(listener); } + _notify(type, detail) { for (const fn of this._listeners) fn(type, detail); } + _pushUndo() { this._undoStack.push(structuredClone(this._state)); this._redoStack = []; } +} +``` + +This is intentionally minimal — no Redux, no Zustand, just a class with undo/redo. + +--- + +## Feature 1: Drag-and-Drop Furniture + +### Interaction Modes + +The viewer operates in one of these modes: + +| Mode | Behavior | Activation | +|------|----------|------------| +| **View** | Current behavior — orbit, zoom, click rooms | Default | +| **Select** | Click furniture to select, show properties | Toggle button | +| **Move** | Drag selected furniture on floor plane | Select + drag | +| **Rotate** | Rotate selected furniture around Y axis | R key or gizmo | +| **Place** | Drag new item from catalog into scene | Catalog click | + +### Selection System + +```javascript +class InteractionManager { + constructor(renderer) { + this.renderer = renderer; + this.selectedObject = null; + this.mode = 'view'; // view | select | move | rotate | place + this.dragPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // floor plane + this.dragOffset = new THREE.Vector3(); + this._ghostMesh = null; // preview during placement + + // Event listeners + this.renderer.renderer.domElement.addEventListener('pointerdown', e => this._onPointerDown(e)); + this.renderer.renderer.domElement.addEventListener('pointermove', e => this._onPointerMove(e)); + this.renderer.renderer.domElement.addEventListener('pointerup', e => this._onPointerUp(e)); + window.addEventListener('keydown', e => this._onKeyDown(e)); + } +} +``` + +### Furniture Selection + +When user clicks on a furniture object: + +1. Raycast from camera through click point +2. Walk hit object up to find `userData.isFurniture` group +3. Apply selection highlight (outline or emissive tint) +4. Show property panel in sidebar +5. Enable move/rotate controls + +**Selection visual:** Outline effect using a slightly scaled-up wireframe clone with a distinct color (e.g., cyan). This avoids modifying original materials. + +```javascript +_selectFurniture(meshGroup) { + this._clearSelection(); + this.selectedObject = meshGroup; + + // Create outline by cloning with wireframe material + const outline = meshGroup.clone(); + outline.traverse(child => { + if (child.isMesh) { + child.material = this._outlineMaterial; // cyan wireframe, slightly larger scale + child.scale.multiplyScalar(1.02); + } + }); + outline.userData._isOutline = true; + meshGroup.add(outline); +} +``` + +### Drag-to-Move + +When user drags a selected furniture piece: + +1. On `pointerdown`: if hitting selected object, enter move mode +2. Disable OrbitControls (prevent camera rotation during drag) +3. Raycast pointer against floor plane (Y=0) each frame +4. Apply offset so object doesn't jump to cursor +5. Optionally snap to grid (0.1m or 0.25m increments) +6. Constrain within room bounds +7. On `pointerup`: commit new position to DesignState, re-enable OrbitControls + +```javascript +_onPointerMove(event) { + if (this.mode !== 'move' || !this.selectedObject) return; + + const mouse = this._getNDC(event); + this.renderer.raycaster.setFromCamera(mouse, this.renderer.camera); + const intersection = new THREE.Vector3(); + this.renderer.raycaster.ray.intersectPlane(this.dragPlane, intersection); + + if (intersection) { + // Apply grid snapping + if (this.snapEnabled) { + intersection.x = Math.round(intersection.x / this.snapSize) * this.snapSize; + intersection.z = Math.round(intersection.z / this.snapSize) * this.snapSize; + } + + // Apply room bounds constraint + const room = this._getContainingRoom(intersection); + if (room) { + intersection.x = clamp(intersection.x, room.bounds.minX + padding, room.bounds.maxX - padding); + intersection.z = clamp(intersection.z, room.bounds.minZ + padding, room.bounds.maxZ - padding); + } + + this.selectedObject.position.copy(intersection.sub(this.dragOffset)); + } +} +``` + +### Rotation + +Two mechanisms: + +1. **Quick rotate:** Press R key to rotate 90 degrees +2. **Gizmo:** Circular handle at base of selected furniture, drag to rotate freely + +Quick rotate is simplest to implement first: + +```javascript +_onKeyDown(event) { + if (event.key === 'r' && this.selectedObject) { + this.selectedObject.rotation.y -= Math.PI / 2; + this.state.rotateFurniture(roomId, index, -90); + } + if (event.key === 'Delete' && this.selectedObject) { + this.state.removeFurniture(roomId, index); + this._removeFromScene(this.selectedObject); + this._clearSelection(); + } + if (event.key === 'Escape') { + this._clearSelection(); + this.mode = 'view'; + } +} +``` + +### Catalog Drag-to-Place + +Adding new furniture from the catalog sidebar: + +1. User clicks a catalog item in sidebar +2. Create a ghost (semi-transparent) mesh from catalog definition +3. Ghost follows cursor, projected onto floor plane +4. Show green/red tint for valid/invalid placement +5. Click to place — add to DesignState and create real mesh +6. Right-click or Escape to cancel + +**Alternative (simpler):** Click catalog item, then click on room floor to place at that point. No drag-from-sidebar needed. This avoids the complexity of HTML-to-3D coordinate mapping. + +**Recommendation:** Start with click-to-place, add drag-from-sidebar later. + +### Room Boundary Detection + +For constraining furniture to rooms, build bounding boxes from room data: + +```javascript +_buildRoomBounds() { + const floor = this.renderer.houseData.floors[this.renderer.currentFloor]; + this.roomBounds = new Map(); + for (const room of floor.rooms) { + this.roomBounds.set(room.id, { + minX: room.position.x, + maxX: room.position.x + room.dimensions.width, + minZ: room.position.y, + maxZ: room.position.y + room.dimensions.length + }); + } +} +``` + +### Collision Detection (Optional, Phase 2) + +Basic AABB overlap check to prevent furniture stacking: + +```javascript +_checkCollision(candidate, exclude) { + const box1 = new THREE.Box3().setFromObject(candidate); + for (const [key, mesh] of this.renderer.furnitureMeshes) { + if (mesh === exclude) continue; + const box2 = new THREE.Box3().setFromObject(mesh); + if (box1.intersectsBox(box2)) return true; + } + return false; +} +``` + +### Implementation Priority + +| Step | What | Complexity | Depends On | +|------|------|------------|------------| +| 1 | Click-to-select furniture | Low | Existing raycaster | +| 2 | Selection outline visual | Low | Step 1 | +| 3 | Property panel in sidebar | Low | Step 1 | +| 4 | Drag-to-move on floor plane | Medium | Step 1, OrbitControls toggle | +| 5 | Grid snapping | Low | Step 4 | +| 6 | Room bounds constraint | Low | Step 4 | +| 7 | R key rotation | Low | Step 1 | +| 8 | Delete key removal | Low | Step 1, DesignState | +| 9 | Catalog click-to-place | Medium | DesignState | +| 10 | Ghost preview during placement | Medium | Step 9 | +| 11 | Undo/redo | Medium | DesignState | +| 12 | Collision detection | Medium | Step 4 | + +--- + +## Feature 2: Room Editing + +### Scope + +Room editing is more complex than furniture interaction. Recommended phased approach: + +**Phase 2a (Do first):** Edit room properties only — name, flooring type, wall colors +**Phase 2b (Later):** Resize rooms — drag walls to change dimensions +**Phase 2c (Future):** Add/remove rooms, edit doors/windows + +### Phase 2a: Room Property Editing + +When a room is selected, show editable properties in sidebar: + +``` +Room: Wohnzimmer +──────────────── +Name: [Wohnzimmer ] +Type: [Living Room ▾] +Flooring: [Hardwood ▾] [Custom color: #b5894e] +Size: 4.5m × 5.5m (24.8 m²) +Ceiling: 2.6m + +Walls: + North: [Interior ▾] — Door to Esszimmer + South: [Exterior ▾] — Window (1.8m) + East: [Interior ▾] — Door to Flur + West: [Exterior ▾] — Patio door (2.0m) +``` + +Changes update both DesignState and the 3D scene in real-time. + +### Phase 2b: Room Resize + +Allow dragging room edges to resize: + +1. When room is selected, show drag handles on each wall midpoint +2. Dragging a handle moves that wall, changing room width or length +3. Adjacent rooms may need to adjust (constraint system) +4. Minimum room size: 1.5m × 1.5m +5. Grid snap: 0.25m increments + +**Wall drag handles:** Small sphere or cube meshes placed at wall midpoints, highlighted on hover. + +```javascript +_createResizeHandles(room) { + const handles = []; + const walls = ['north', 'south', 'east', 'west']; + for (const wall of walls) { + const handle = new THREE.Mesh( + new THREE.SphereGeometry(0.1), + new THREE.MeshStandardMaterial({ color: 0x4a90d9 }) + ); + handle.userData = { isHandle: true, wall, roomId: room.id }; + // Position at wall midpoint + handles.push(handle); + } + return handles; +} +``` + +**Constraint challenge:** When room A's east wall moves, room B's west wall (if adjacent) should also move. This requires understanding room adjacency from door connections. The house data already has `connectsTo` fields on doors — use these to build an adjacency graph. + +### Phase 2c: Door/Window Editing (Future) + +- Click wall to add/remove doors or windows +- Drag door/window along wall to reposition +- Change door/window types from property panel +- This modifies `sample-house.json` structure + +**Recommendation:** Defer this to a future phase. It's architecturally complex (wall segmentation recalculation, connectivity validation) and the current house data is well-defined. + +### Implementation Priority + +| Step | What | Complexity | +|------|------|------------| +| 1 | Room property panel (read-only) | Low | +| 2 | Editable flooring type | Low | +| 3 | Editable room name | Low | +| 4 | Wall color customization | Medium | +| 5 | Room resize handles | High | +| 6 | Adjacent room constraint system | High | +| 7 | Door/window editing | Very High | + +--- + +## Feature 3: Style Themes + +### Concept + +Predefined color/material palettes that restyle the entire house. Themes override the `COLORS` object and material properties. + +### Theme Data Structure + +```javascript +// src/themes.js +export const THEMES = { + default: { + name: 'Standard', + colors: { + wall: { exterior: 0xe8e0d4, interior: 0xf5f0eb }, + floor: { tile: 0xc8beb0, hardwood: 0xb5894e }, + ceiling: 0xfaf8f5, + door: 0x8b6914, + window: 0x87ceeb, + windowFrame: 0xd0d0d0, + grid: 0xcccccc, + selected: 0x4a90d9 + }, + materials: { + wallRoughness: 0.9, + floorRoughness: 0.8, + doorRoughness: 0.6 + }, + scene: { + background: 0xf0f0f0, + ambientIntensity: 0.6, + directionalIntensity: 0.8 + } + }, + + modern: { + name: 'Modern Minimal', + colors: { + wall: { exterior: 0xf5f5f5, interior: 0xffffff }, + floor: { tile: 0xe0e0e0, hardwood: 0xc4a882 }, + ceiling: 0xffffff, + door: 0x333333, + window: 0xa8d4f0, + windowFrame: 0x666666, + grid: 0xe0e0e0, + selected: 0x2196f3 + }, + materials: { wallRoughness: 0.3, floorRoughness: 0.4, doorRoughness: 0.2 }, + scene: { background: 0xfafafa, ambientIntensity: 0.7, directionalIntensity: 0.6 } + }, + + warm: { + name: 'Warm Rustic', + colors: { + wall: { exterior: 0xddd0b8, interior: 0xf0e8d8 }, + floor: { tile: 0xb8a890, hardwood: 0x9b6b3a }, + ceiling: 0xf5efe5, + door: 0x6b4423, + window: 0x8bc4e0, + windowFrame: 0x8b7355, + grid: 0xc8b8a0, + selected: 0xd48b2c + }, + materials: { wallRoughness: 0.95, floorRoughness: 0.9, doorRoughness: 0.8 }, + scene: { background: 0xf5efe5, ambientIntensity: 0.5, directionalIntensity: 0.9 } + }, + + dark: { + name: 'Dark Mode', + colors: { + wall: { exterior: 0x3a3a3a, interior: 0x4a4a4a }, + floor: { tile: 0x2a2a2a, hardwood: 0x5a4030 }, + ceiling: 0x333333, + door: 0x5a4030, + window: 0x4080b0, + windowFrame: 0x555555, + grid: 0x444444, + selected: 0x64b5f6 + }, + materials: { wallRoughness: 0.7, floorRoughness: 0.6, doorRoughness: 0.5 }, + scene: { background: 0x222222, ambientIntensity: 0.4, directionalIntensity: 1.0 } + }, + + scandinavian: { + name: 'Scandinavian', + colors: { + wall: { exterior: 0xf0ece4, interior: 0xfaf6f0 }, + floor: { tile: 0xe8ddd0, hardwood: 0xd4b88c }, + ceiling: 0xffffff, + door: 0xc4a87a, + window: 0xc0ddf0, + windowFrame: 0xb0b0b0, + grid: 0xd8d8d8, + selected: 0x5b9bd5 + }, + materials: { wallRoughness: 0.5, floorRoughness: 0.6, doorRoughness: 0.4 }, + scene: { background: 0xf8f6f2, ambientIntensity: 0.65, directionalIntensity: 0.7 } + } +}; +``` + +### Theme Application + +```javascript +export class ThemeManager { + constructor(renderer) { + this.renderer = renderer; + this.currentTheme = 'default'; + } + + applyTheme(themeId) { + const theme = THEMES[themeId]; + if (!theme) return; + this.currentTheme = themeId; + + // Update COLORS object (renderer reads from this) + 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; + + // Update scene + this.renderer.scene.background.setHex(theme.scene.background); + + // Clear material cache (forces recreation with new colors) + // Then re-render current floor + this.renderer._clearFloor(); + this.renderer.showFloor(this.renderer.currentFloor); + } + + getThemes() { + return Object.entries(THEMES).map(([id, t]) => ({ id, name: t.name })); + } +} +``` + +**Key insight:** The renderer already uses a `COLORS` constant and caches materials keyed by color. Changing `COLORS` and clearing the cache forces a full re-render with new colors. This is clean and requires minimal renderer changes — just make `COLORS` mutable (change `const` to `let`, or use an object that can be mutated). + +### Renderer Change Required + +The `COLORS` constant in renderer.js needs to be accessible for mutation. Two options: + +**Option A (minimal):** Export `COLORS` and let ThemeManager mutate it directly. +```javascript +export const COLORS = { ... }; // already an object, properties are mutable +``` + +**Option B (cleaner):** Add a `setColors(newColors)` method to HouseRenderer. + +Recommend Option A for simplicity — `COLORS` is already a mutable object. + +### UI: Theme Selector + +Add a theme dropdown or button row to the sidebar: + +```html +

Theme

+
+ + + ... +
+``` + +Each theme button shows a small color swatch preview. + +### Per-Room Color Override (Stretch) + +Allow individual rooms to override theme colors: + +```javascript +// In DesignState +roomOverrides: { + "eg-wohnzimmer": { + floorColor: "#a0522d", + wallColor: "#f0e0d0" + } +} +``` + +The renderer checks for overrides before falling back to theme defaults. + +### Implementation Priority + +| Step | What | Complexity | +|------|------|------------| +| 1 | Define 5 theme presets | Low | +| 2 | Theme application (clear cache + re-render) | Low | +| 3 | Theme selector UI | Low | +| 4 | Smooth transition (fade between themes) | Medium | +| 5 | Per-room color overrides | Medium | +| 6 | Custom theme builder | High | + +--- + +## Feature 4: Export + +### Export Formats + +| Format | What | Use Case | +|--------|------|----------| +| **JSON** | Design state (furniture placements) | Save/load, share | +| **PNG** | Screenshot of current view | Quick sharing | +| **PDF** | 2D floor plan with furniture | Printing | +| **glTF** | 3D model of entire house | Use in other 3D apps | + +### JSON Save/Load + +```javascript +export class ExportManager { + constructor(renderer, state) { + this.renderer = renderer; + this.state = state; + } + + // Save design to JSON + exportDesignJSON() { + const data = { + ...this.state.design, + exportedAt: new Date().toISOString(), + theme: this.themeManager?.currentTheme || 'default' + }; + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + this._download(blob, `${data.name || 'design'}.json`); + } + + // Load design from JSON + async importDesignJSON(file) { + const text = await file.text(); + const data = JSON.parse(text); + this.state.loadDesign(data); + this.renderer._clearFloor(); + this.renderer.designData = data; + this.renderer._placeFurnitureForFloor(); + } +} +``` + +### PNG Screenshot + +Three.js makes this straightforward: + +```javascript +exportScreenshot(width = 1920, height = 1080) { + // Temporarily resize renderer for high-res capture + const prevSize = new THREE.Vector2(); + this.renderer.renderer.getSize(prevSize); + + this.renderer.renderer.setSize(width, height); + this.renderer.camera.aspect = width / height; + this.renderer.camera.updateProjectionMatrix(); + this.renderer.renderer.render(this.renderer.scene, this.renderer.camera); + + const dataURL = this.renderer.renderer.domElement.toDataURL('image/png'); + + // Restore original size + this.renderer.renderer.setSize(prevSize.x, prevSize.y); + this.renderer.camera.aspect = prevSize.x / prevSize.y; + this.renderer.camera.updateProjectionMatrix(); + + this._downloadDataURL(dataURL, 'house-design.png'); +} +``` + +**Enhancement:** Option to hide UI overlays (sidebar, info bar) during capture, or render to an offscreen canvas. + +### PDF Floor Plan (Phase 2) + +Generate a 2D top-down floor plan suitable for printing: + +1. Create a 2D canvas (or use jsPDF) +2. Draw rooms as rectangles with labels +3. Draw furniture as simplified top-down shapes +4. Add dimension lines and measurements +5. Include legend with furniture names + +This is a separate rendering pipeline from the 3D view. Consider using the `canvas` API directly or a library like `jspdf` + `html2canvas`. + +```javascript +exportFloorPlanPDF() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const scale = 50; // 50px per meter + + const floor = this.renderer.houseData.floors[this.renderer.currentFloor]; + // ... draw rooms, furniture, dimensions, legend + // Convert to PDF using jsPDF or similar +} +``` + +### glTF Export (Phase 3) + +Three.js has a built-in `GLTFExporter`: + +```javascript +import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js'; + +exportGLTF() { + const exporter = new GLTFExporter(); + exporter.parse(this.renderer.scene, (gltf) => { + const blob = new Blob([JSON.stringify(gltf)], { type: 'application/json' }); + this._download(blob, 'house-design.gltf'); + }, { binary: false }); + + // Or binary .glb: + exporter.parse(this.renderer.scene, (buffer) => { + const blob = new Blob([buffer], { type: 'application/octet-stream' }); + this._download(blob, 'house-design.glb'); + }, { binary: true }); +} +``` + +### Auto-Save + +Save design state to `localStorage` periodically: + +```javascript +// Every 30 seconds, save to localStorage +setInterval(() => { + localStorage.setItem('house-design-autosave', JSON.stringify(this.state.design)); +}, 30000); + +// On load, offer to restore +const saved = localStorage.getItem('house-design-autosave'); +if (saved) { + // Show "Restore previous session?" prompt +} +``` + +### Implementation Priority + +| Step | What | Complexity | +|------|------|------------| +| 1 | JSON export (download) | Low | +| 2 | JSON import (file picker) | Low | +| 3 | PNG screenshot | Low | +| 4 | Auto-save to localStorage | Low | +| 5 | 2D floor plan PDF | High | +| 6 | glTF/GLB export | Medium | +| 7 | Share via URL (encode state) | Medium | + +--- + +## UI Design + +### Updated Sidebar Layout + +``` +┌─────────────────────────────┐ +│ ☰ Musterhaus │ +├─────────────────────────────┤ +│ [View] [Edit] [Place] │ ← Mode buttons +├─────────────────────────────┤ +│ FLOORS │ +│ [EG] [OG] │ +├─────────────────────────────┤ +│ ROOMS │ +│ ▸ Flur 18.0 m² │ +│ ▸ Wohnzimmer 24.8 m² │ ← Expandable to show furniture +│ ▸ Küche 15.0 m² │ +│ ... │ +├─────────────────────────────┤ +│ CATALOG 🔍 search │ ← Only visible in Place mode +│ ┌──────┐ ┌──────┐ │ +│ │ Sofa │ │Chair │ │ +│ └──────┘ └──────┘ │ +│ ┌──────┐ ┌──────┐ │ +│ │Table │ │ Bed │ │ +│ └──────┘ └──────┘ │ +├─────────────────────────────┤ +│ PROPERTIES │ ← Only visible when item selected +│ Sofa 3-Sitzer │ +│ Position: (2.1, 3.4) │ +│ Rotation: 180° │ +│ [Rotate 90°] [Delete] │ +├─────────────────────────────┤ +│ THEME │ +│ [Standard] [Modern] [Warm] │ +│ [Dark] [Scandinavian] │ +├─────────────────────────────┤ +│ [💾 Save] [📷 Screenshot] │ ← Always visible +│ [📂 Load] [📤 Export 3D] │ +└─────────────────────────────┘ +``` + +### Toolbar (Top) + +For quick access to common actions: + +``` +┌─────────────────────────────────────────────────────┐ +│ [Undo] [Redo] | [Snap: 0.25m ▾] | [Grid ✓] [Labels ✓] │ +└─────────────────────────────────────────────────────┘ +``` + +### Keyboard Shortcuts + +| Key | Action | +|-----|--------| +| `Escape` | Cancel current action, deselect | +| `Delete` / `Backspace` | Remove selected furniture | +| `R` | Rotate selected 90° clockwise | +| `Shift+R` | Rotate selected 90° counter-clockwise | +| `Ctrl+Z` | Undo | +| `Ctrl+Shift+Z` | Redo | +| `Ctrl+S` | Save design JSON | +| `G` | Toggle grid | +| `L` | Toggle room labels | +| `1`-`9` | Quick-select room by index | + +--- + +## Required Renderer Changes + +To support these features, `HouseRenderer` needs a few additions: + +### 1. Make COLORS Exportable and Mutable + +```javascript +// Change from const to let, or export the object +export const COLORS = { ... }; // Already works — object properties are mutable +``` + +### 2. Add Furniture Click Detection + +Extend `_onClick` to distinguish room clicks from furniture clicks: + +```javascript +_onClick(event) { + // ... existing raycaster setup ... + for (const hit of intersects) { + let obj = hit.object; + // Check furniture first (more specific) + while (obj && !obj.userData.isFurniture && !obj.userData.roomId) { + obj = obj.parent; + } + if (obj?.userData.isFurniture) { + this.container.dispatchEvent(new CustomEvent('furnitureclick', { + detail: { ...obj.userData, mesh: obj, point: hit.point } + })); + return; + } + if (obj?.userData.roomId) { + // existing room click behavior + } + } +} +``` + +### 3. Add OrbitControls Toggle + +```javascript +setControlsEnabled(enabled) { + this.controls.enabled = enabled; +} +``` + +### 4. Expose Scene for External Modules + +The renderer already exposes `this.scene`, `this.camera`, `this.raycaster` as public properties. No changes needed — external modules can access these directly. + +### 5. Floor-switch Event + +```javascript +showFloor(index) { + // ... existing code ... + this.container.dispatchEvent(new CustomEvent('floorchange', { + detail: { index, floor: this.houseData.floors[index] } + })); +} +``` + +--- + +## Trade-off Analysis + +### Framework vs. Vanilla JS + +**Decision: Stay vanilla JS.** + +Pros of staying vanilla: +- No build step needed (works with static file server) +- No dependency management +- Project already works well this way +- Interactive features can be added as ES6 modules + +Cons: +- No reactive UI updates (must manually sync DOM) +- No component system for sidebar panels +- State management is DIY + +Mitigation: Keep UI simple. Use custom events for cross-module communication. The sidebar can be built with template literals and direct DOM manipulation — it doesn't need React for this complexity level. + +### 2D vs. 3D Interaction + +**Decision: 3D interaction (existing stack).** + +The RESEARCH.md recommended a 2D Konva.js approach, but the project already has a working 3D viewer with raycasting. Adding drag-and-drop to 3D is harder than 2D, but: + +1. We already have raycasting and hit detection +2. Floor plane projection for drag is straightforward +3. Users already understand the 3D orbit camera +4. A 2D top-down view can be added later as an alternative mode + +### Undo/Redo Approach + +**Decision: Full state snapshots with structuredClone.** + +Alternative: Command pattern (store individual operations). More memory-efficient but harder to implement correctly. + +For a house design with ~50 furniture items, the design JSON is ~10-20KB. Storing 50 undo snapshots = ~1MB. That's fine. Simplicity wins. + +### Collision Detection + +**Decision: Defer to Phase 2.** + +AABB collision detection is useful but not critical for MVP. Users can visually avoid overlaps. Implement only if users find placement confusing without it. + +--- + +## Implementation Roadmap + +### Sprint 1: Foundation (3-4 tasks) + +1. **DesignState class** — observable state with undo/redo +2. **Renderer events** — `furnitureclick`, `floorchange`, controls toggle +3. **InteractionManager skeleton** — mode system, keyboard shortcuts +4. **Selection visual** — outline on selected furniture + +### Sprint 2: Drag & Drop (3-4 tasks) + +5. **Drag-to-move** — floor plane projection, OrbitControls toggle +6. **Grid snapping** — configurable snap size +7. **Room bounds constraint** — keep furniture in rooms +8. **Rotate & delete** — R key, Delete key + +### Sprint 3: Catalog & Placement (2-3 tasks) + +9. **Catalog sidebar panel** — browsable, filterable +10. **Click-to-place** — new furniture from catalog +11. **Ghost preview** — semi-transparent placement preview + +### Sprint 4: Themes & Export (3-4 tasks) + +12. **Theme system** — 5 presets, apply/switch +13. **Theme selector UI** +14. **JSON save/load** +15. **PNG screenshot** + +### Sprint 5: Room Editing & Polish (3-4 tasks) + +16. **Room property panel** — read-only info +17. **Editable flooring/name** +18. **Auto-save to localStorage** +19. **Toolbar with undo/redo buttons** + +### Future Sprints + +20. Room resize handles +21. 2D floor plan export (PDF) +22. glTF 3D export +23. Collision detection +24. Per-room color overrides +25. Door/window editing + +--- + +## Task Breakdown for mai + +These are the tasks to create for implementation: + +``` +1. "Create DesignState class with undo/redo" (coder) +2. "Add furniture click events and controls toggle to renderer" (coder) +3. "Build InteractionManager with mode system and keyboard shortcuts" (coder) +4. "Implement furniture selection with outline visual" (coder) +5. "Add drag-to-move furniture on floor plane" (coder) +6. "Add grid snapping and room bounds constraint" (coder) +7. "Build catalog sidebar panel with categories and search" (coder) +8. "Implement click-to-place new furniture from catalog" (coder) +9. "Create theme system with 5 presets" (coder) +10. "Add theme selector UI to sidebar" (coder) +11. "Implement JSON save/load and PNG screenshot export" (coder) +12. "Add room property panel and editable properties" (coder) +13. "Add toolbar with undo/redo, grid toggle, snap settings" (coder) +14. "Implement auto-save to localStorage" (coder) +``` + +--- + +## Summary + +This design adds full interactivity to the house viewer while respecting the existing architecture: + +- **No framework rewrite** — vanilla JS modules that compose with HouseRenderer +- **No new dependencies** — everything built with Three.js and browser APIs +- **Incremental delivery** — each sprint produces a usable improvement +- **Clean separation** — interaction, themes, export, and UI are independent modules +- **Undo/redo from day 1** — critical for any editor +- **14 implementation tasks**, roughly 5 sprints diff --git a/RESEARCH.md b/RESEARCH.md new file mode 100644 index 0000000..04167ef --- /dev/null +++ b/RESEARCH.md @@ -0,0 +1,256 @@ +# Interior Design Visualization - Tech Stack Research + +**Task:** t-fb166 +**Date:** 2026-02-07 +**Researcher:** bohr + +## Executive Summary + +**Recommended approach: 2D-first with Konva.js (react-konva), Vite+React app, with optional 3D preview via React Three Fiber later.** + +This is the simplest viable path to a working room planner prototype. Start with 2D canvas-based floor plan editing, add 3D viewing as a second phase. + +--- + +## Options Evaluated + +### Option A: Pure 3D with React Three Fiber (R3F) + +**Stack:** Vite + React + @react-three/fiber + @react-three/drei + +**How it works:** Everything rendered in a WebGL 3D scene. Room walls are 3D meshes, furniture are 3D models (GLTF/GLB), camera orbits around the scene. + +**Pros:** +- Visually impressive - realistic lighting, shadows, materials +- R3F is mature (huge community, active maintenance, excellent docs) +- drei provides orbit controls, drag controls, environment maps out of the box +- Good ecosystem of 3D furniture models (Sketchfab, etc.) + +**Cons:** +- **Hardest to build** - 3D drag-and-drop is complex (raycasting, plane projection) +- Precise measurement/placement is difficult in 3D +- Requires 3D model assets for all furniture (heavy, complex pipeline) +- Performance concerns with many objects + shadows +- Steep learning curve for 3D math (vectors, quaternions, raycasting) +- Drawing room outlines in 3D is unintuitive for users + +**Complexity: HIGH** | **Time to prototype: 3-4 weeks** + +**Key repos:** +- [threejs-3d-room-designer](https://github.com/CodeHole7/threejs-3d-room-designer) - React + Three.js room planner +- [threejs-room-planner](https://github.com/nickorzha/threejs-room-planner) - Similar approach + +--- + +### Option B: Hybrid 2D Editor + 3D Preview (react-planner style) + +**Stack:** Vite + React + SVG (2D) + Three.js (3D preview) + +**How it works:** Users draw/edit floor plans in a 2D SVG view, then toggle to a 3D preview. The same data model drives both views. + +**Pros:** +- Best UX - 2D for precise editing, 3D for visualization +- Proven concept (react-planner has 1.4k GitHub stars) +- 2D editing is simpler and more intuitive for users +- 3D adds wow factor for presentations + +**Cons:** +- Must maintain two rendering paths (SVG + Three.js) +- react-planner itself is **outdated** (React 16, Redux, Immutable.js - needs modernization) +- More code to maintain than either pure approach +- SVG can become slow with very complex plans + +**Complexity: MEDIUM-HIGH** | **Time to prototype: 2-3 weeks** + +**Key repos:** +- [react-planner](https://github.com/cvdlab/react-planner) - 1.4k stars, SVG 2D + Three.js 3D, but old React stack +- [arcada](https://github.com/mehanix/arcada) - React + Pixi.js, 196 stars, includes backend + +--- + +### Option C: 2D Canvas with Konva.js (RECOMMENDED) + +**Stack:** Vite + React + react-konva + +**How it works:** Room plans rendered on HTML5 Canvas via Konva.js. Rooms are drawn as polygons/rectangles, furniture items are draggable shapes/images. Top-down 2D view. + +**Pros:** +- **Simplest to build** - Konva handles drag-drop, transforms, snapping natively +- react-konva integrates cleanly with React component model +- Canvas performs well with hundreds of objects +- Built-in: drag-and-drop, resize handles, rotation, snapping, grouping, layers +- Easy to add grid snapping, measurements, labels +- Export to PNG/PDF straightforward +- Familiar 2D interaction model for users (like Google Slides) +- Can add 3D preview later using same data model + +**Cons:** +- No 3D visualization (initially) +- Less visually impressive than 3D +- Canvas text rendering less crisp than SVG (mitigated by high-DPI) + +**Complexity: LOW-MEDIUM** | **Time to prototype: 1-2 weeks** + +**Alternative: Fabric.js** - Similar capabilities, also canvas-based, slightly different API. Konva has better React integration via react-konva. + +--- + +### Option D: 2D SVG with React + +**Stack:** Vite + React + raw SVG (or svg.js) + +**How it works:** Floor plans rendered as SVG elements directly in React. Furniture items are SVG groups with drag behavior. + +**Pros:** +- SVG is resolution-independent (crisp at all zoom levels) +- React manages SVG elements naturally (they're just DOM nodes) +- Good for precise measurements and labels +- Easy to style with CSS + +**Cons:** +- Performance degrades with many elements (SVG is DOM-based) +- Custom drag-and-drop implementation needed (no built-in like Konva) +- Transform handles, snapping, rotation all manual work +- More boilerplate than Konva for interactive features + +**Complexity: MEDIUM** | **Time to prototype: 2 weeks** + +--- + +## Comparison Matrix + +| Criteria | A: Pure 3D (R3F) | B: Hybrid 2D+3D | C: 2D Konva (rec) | D: 2D SVG | +|-----------------------|-------------------|------------------|---------------------|-----------| +| Time to prototype | 3-4 weeks | 2-3 weeks | **1-2 weeks** | 2 weeks | +| Visual impact | **Highest** | High | Medium | Medium | +| Ease of furniture add | Hard (3D models) | Medium | **Easy (images)** | Easy | +| Drag-and-drop | Complex | Medium | **Built-in** | Manual | +| Precise placement | Hard | Good | **Good** | Good | +| Performance | Medium | Medium | **Good** | Medium | +| Upgrade path to 3D | N/A | Built-in | **Add later** | Add later | +| Learning curve | Steep | Medium | **Low** | Low | +| Mobile support | Limited | Limited | **Good (touch)** | Good | + +--- + +## Recommended Architecture + +### Phase 1: 2D Room Planner (MVP) + +``` +Tech Stack: +- Vite + React + TypeScript +- react-konva (2D canvas rendering) +- Zustand (lightweight state management) +- Tailwind CSS (UI styling) +- shadcn/ui (UI components) + +Features: +- Draw room outlines (rectangles, L-shapes) +- Grid-based canvas with snap-to-grid +- Furniture catalog sidebar (chair, table, bed, sofa, etc.) +- Drag furniture from catalog onto room +- Move, rotate, resize placed furniture +- Room dimensions and measurements +- Export as PNG image +- Save/load room plans (JSON) +``` + +### Phase 2: Enhanced Features + +``` +- Upload room image as background trace layer +- More furniture with realistic top-down SVG/PNG icons +- Multiple rooms / full floor plans +- Wall thickness and door/window placement +- Measurement labels and area calculation +- Color/material swatches for floors and walls +``` + +### Phase 3: 3D Preview (Optional) + +``` +Add-on stack: +- @react-three/fiber + @react-three/drei +- Same room data model → 3D scene generation +- Orbit camera view of the room +- Basic lighting and materials +``` + +--- + +## Key Libraries + +| Library | Purpose | npm Weekly Downloads | Maturity | +|---------|---------|---------------------|----------| +| react-konva | React bindings for Konva canvas | ~100k+ | Stable, active | +| konva | 2D canvas framework | ~200k+ | Stable, v9+ | +| zustand | State management | ~3M+ | Very active | +| @react-three/fiber | React Three.js (Phase 3) | ~500k+ | Very active | +| @react-three/drei | R3F helpers (Phase 3) | ~500k+ | Very active | + +--- + +## Existing Projects Worth Studying + +1. **[react-planner](https://github.com/cvdlab/react-planner)** - Best reference for hybrid 2D/3D architecture. Study its data model even if not using the code directly (it's outdated React 16). + +2. **[arcada](https://github.com/mehanix/arcada)** - React + Pixi.js floor planner. Good reference for feature set and UX patterns. Has a [bachelor's thesis document](https://github.com/mehanix/arcada/blob/master/docs/) explaining the architecture. + +3. **[Floorplan.js](https://floorplanjs.org/)** - Commercial-ish but has good UX patterns to study. + +4. **Sweet Home 3D** - Java desktop app, but gold standard for feature set reference. Shows what users expect from a room planner. + +--- + +## Data Model Sketch + +```typescript +interface RoomPlan { + id: string; + name: string; + rooms: Room[]; + furniture: FurnitureItem[]; +} + +interface Room { + id: string; + points: Point[]; // polygon vertices + label: string; + floorColor: string; +} + +interface FurnitureItem { + id: string; + catalogId: string; // reference to catalog + x: number; + y: number; + rotation: number; // degrees + scaleX: number; + scaleY: number; +} + +interface CatalogEntry { + id: string; + name: string; + category: string; + width: number; // real-world cm + height: number; + icon: string; // SVG/PNG path for 2D view + model3d?: string; // GLTF path for future 3D view +} +``` + +This data model works for both 2D rendering (Konva) and future 3D rendering (R3F), enabling a clean upgrade path. + +--- + +## Conclusion + +**Start with Option C (2D Konva.js)** because: +1. Fastest path to a working prototype +2. Konva handles 90% of the interactive features we need out of the box +3. Clean data model that separates room geometry from rendering +4. 3D can be added as a second view of the same data later +5. Modern stack (Vite + React + TS + Zustand) with excellent DX +6. Low risk - all well-maintained, well-documented libraries