diff --git a/src/export.js b/src/export.js new file mode 100644 index 0000000..1de114b --- /dev/null +++ b/src/export.js @@ -0,0 +1,124 @@ +import * as THREE from 'three'; + +/** + * ExportManager — JSON save/load and PNG screenshot export. + * + * Receives HouseRenderer + DesignState, provides download/upload + * functionality for design files and viewport screenshots. + */ +export class ExportManager { + constructor(renderer, state) { + this.renderer = renderer; + this.state = state; + } + + /** Export current design state as a downloadable JSON file. */ + exportDesignJSON() { + const data = { + ...this.state.toJSON(), + exportedAt: new Date().toISOString() + }; + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + this._downloadBlob(blob, `${data.name || 'design'}.json`); + } + + /** Open a file picker and load a design JSON file. */ + importDesignJSON() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json,application/json'; + input.addEventListener('change', () => { + const file = input.files[0]; + if (!file) return; + this._loadDesignFile(file); + }); + input.click(); + } + + /** Load a design from a File object. */ + async _loadDesignFile(file) { + try { + const text = await file.text(); + const data = JSON.parse(text); + + // Basic validation: must have rooms array + if (!data.rooms || !Array.isArray(data.rooms)) { + throw new Error('Invalid design file: missing rooms array'); + } + + this.state.loadDesign(data); + this.renderer.designData = this.state.design; + + // Re-render current floor with new design + this.renderer._clearFloor(); + const floor = this.renderer.houseData.floors[this.renderer.currentFloor]; + if (floor) { + for (const room of floor.rooms) { + this.renderer._renderRoom(room, floor.ceilingHeight); + } + } + this.renderer._placeFurnitureForFloor(); + + this.renderer.container.dispatchEvent(new CustomEvent('designloaded', { + detail: { name: data.name || file.name } + })); + } catch (err) { + console.error('Failed to load design:', err); + this.renderer.container.dispatchEvent(new CustomEvent('loaderror', { + detail: { source: 'importDesign', error: err.message } + })); + } + } + + /** + * Capture the current 3D view as a PNG and download it. + * Temporarily resizes the renderer for high-res output. + */ + exportScreenshot(width = 1920, height = 1080) { + const r = this.renderer.renderer; + + // Save current size + const prevSize = new THREE.Vector2(); + r.getSize(prevSize); + const prevPixelRatio = r.getPixelRatio(); + + // Resize for capture + r.setPixelRatio(1); + r.setSize(width, height); + this.renderer.camera.aspect = width / height; + this.renderer.camera.updateProjectionMatrix(); + + // Render one frame + r.render(this.renderer.scene, this.renderer.camera); + + // Grab the image + const dataURL = r.domElement.toDataURL('image/png'); + + // Restore original size + r.setPixelRatio(prevPixelRatio); + r.setSize(prevSize.x, prevSize.y); + this.renderer.camera.aspect = prevSize.x / prevSize.y; + this.renderer.camera.updateProjectionMatrix(); + + this._downloadDataURL(dataURL, 'house-design.png'); + } + + // ---- Internal helpers ---- + + _downloadBlob(blob, filename) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + _downloadDataURL(dataURL, filename) { + const a = document.createElement('a'); + a.href = dataURL; + a.download = filename; + a.click(); + } +} diff --git a/src/index.html b/src/index.html index ae04e49..5921281 100644 --- a/src/index.html +++ b/src/index.html @@ -77,7 +77,7 @@ #info { position: fixed; bottom: 16px; - left: 16px; + left: 260px; background: rgba(0,0,0,0.7); color: #fff; padding: 8px 14px; @@ -85,11 +85,152 @@ font-size: 13px; z-index: 10; } + + /* Catalog panel (left sidebar) */ + #catalog-panel { + position: fixed; + top: 0; + left: 0; + width: 250px; + height: 100vh; + background: rgba(255, 255, 255, 0.95); + border-right: 1px solid #ddd; + z-index: 10; + display: flex; + flex-direction: column; + } + .catalog-panel h3 { + font-size: 13px; + padding: 12px 12px 0; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .catalog-search { + padding: 12px 12px 8px; + } + .catalog-search-input { + width: 100%; + padding: 7px 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 13px; + outline: none; + } + .catalog-search-input:focus { + border-color: #4a90d9; + } + .catalog-categories { + padding: 0 12px 8px; + display: flex; + flex-wrap: wrap; + gap: 4px; + } + .catalog-cat-btn { + padding: 3px 8px; + border: 1px solid #ccc; + border-radius: 3px; + background: #fff; + cursor: pointer; + font-size: 11px; + } + .catalog-cat-btn.active { + background: #4a90d9; + color: #fff; + border-color: #4a90d9; + } + .catalog-items { + flex: 1; + overflow-y: auto; + padding: 0 12px 12px; + } + .catalog-item { + display: flex; + align-items: center; + padding: 8px; + margin: 2px 0; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + } + .catalog-item:hover { + background: #e8f0fe; + } + .catalog-item-swatch { + width: 32px; + height: 32px; + border-radius: 4px; + flex-shrink: 0; + border: 1px solid rgba(0,0,0,0.1); + } + .catalog-item-info { + flex: 1; + margin-left: 8px; + min-width: 0; + } + .catalog-item-name { + font-size: 12px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .catalog-item-dims { + font-size: 10px; + color: #888; + margin-top: 2px; + } + .catalog-item-add { + width: 24px; + height: 24px; + border: 1px solid #ccc; + border-radius: 3px; + background: #fff; + cursor: pointer; + font-size: 14px; + font-weight: bold; + color: #4a90d9; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + .catalog-item-add:hover { + background: #4a90d9; + color: #fff; + border-color: #4a90d9; + } + .catalog-empty { + text-align: center; + color: #999; + padding: 20px 0; + font-size: 12px; + } + + .export-section { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid #ddd; + } + .export-btn { + display: inline-block; + padding: 6px 12px; + margin: 3px 4px 3px 0; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + cursor: pointer; + font-size: 12px; + } + .export-btn:hover { background: #f0f0f0; } + .export-btn:active { background: #e0e0e0; }
+ +