Add research and interactive features design docs

- RESEARCH.md: Tech stack analysis (Three.js, SVG, hybrid approaches)
- DESIGN-interactive-features.md: Phase 2 design with 5 sprints, 14 tasks
This commit is contained in:
m
2026-02-07 12:31:08 +01:00
parent 32eaf70635
commit ab3e8fd03c
2 changed files with 1241 additions and 0 deletions

View File

@@ -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
<h3>Theme</h3>
<div id="theme-buttons">
<!-- Generated: one button per theme -->
<button class="theme-btn active" data-theme="default">
<span class="theme-swatch" style="background: #e8e0d4"></span>
Standard
</button>
...
</div>
```
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

256
RESEARCH.md Normal file
View File

@@ -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