# 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