diff --git a/src/catalog.js b/src/catalog.js index 0347933..908baa8 100644 --- a/src/catalog.js +++ b/src/catalog.js @@ -45,6 +45,19 @@ export class CatalogPanel { this._itemList.className = 'catalog-items'; this.container.appendChild(this._itemList); + // Create custom button + this._createBtn = document.createElement('button'); + this._createBtn.className = 'catalog-create-btn'; + this._createBtn.textContent = '+ Create Custom Furniture'; + this._createBtn.addEventListener('click', () => this._showCreateForm()); + this.container.appendChild(this._createBtn); + + // Create form container (hidden initially) + this._createFormContainer = document.createElement('div'); + this._createFormContainer.className = 'catalog-create-form'; + this._createFormContainer.style.display = 'none'; + this.container.appendChild(this._createFormContainer); + this._renderCategories(); this._renderItems(); } @@ -207,6 +220,164 @@ export class CatalogPanel { return rooms[0].id; } + // ---- Custom furniture creator ---- + + _showCreateForm() { + this._createBtn.style.display = 'none'; + this._createFormContainer.style.display = ''; + this._buildCreateForm(); + } + + _hideCreateForm() { + this._createBtn.style.display = ''; + this._createFormContainer.style.display = 'none'; + this._createFormContainer.innerHTML = ''; + } + + _buildCreateForm() { + const catalog = this.renderer.catalogData; + const categories = catalog?.categories || []; + const f = this._createFormContainer; + f.innerHTML = ''; + + // Name + const nameLabel = document.createElement('label'); + nameLabel.textContent = 'Name'; + f.appendChild(nameLabel); + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.placeholder = 'e.g. Custom Shelf'; + f.appendChild(nameInput); + + // Dimensions + const dimLabel = document.createElement('label'); + dimLabel.textContent = 'Dimensions (meters)'; + f.appendChild(dimLabel); + const dimRow = document.createElement('div'); + dimRow.className = 'catalog-create-dims'; + + const makeDimField = (label, value) => { + const wrap = document.createElement('div'); + const lbl = document.createElement('label'); + lbl.textContent = label; + wrap.appendChild(lbl); + const inp = document.createElement('input'); + inp.type = 'number'; + inp.step = '0.05'; + inp.min = '0.05'; + inp.max = '10'; + inp.value = value; + wrap.appendChild(inp); + dimRow.appendChild(wrap); + return inp; + }; + + const widthInput = makeDimField('W', '0.8'); + const depthInput = makeDimField('D', '0.4'); + const heightInput = makeDimField('H', '0.8'); + f.appendChild(dimRow); + + // Color + const colorLabel = document.createElement('label'); + colorLabel.textContent = 'Color'; + f.appendChild(colorLabel); + const colorRow = document.createElement('div'); + colorRow.className = 'catalog-create-color-row'; + const colorPicker = document.createElement('input'); + colorPicker.type = 'color'; + colorPicker.value = '#8899aa'; + const colorText = document.createElement('input'); + colorText.type = 'text'; + colorText.value = '#8899aa'; + colorPicker.addEventListener('input', () => { colorText.value = colorPicker.value; }); + colorText.addEventListener('input', () => { + if (/^#[0-9a-f]{6}$/i.test(colorText.value)) colorPicker.value = colorText.value; + }); + colorRow.appendChild(colorPicker); + colorRow.appendChild(colorText); + f.appendChild(colorRow); + + // Category + const catLabel = document.createElement('label'); + catLabel.textContent = 'Category'; + f.appendChild(catLabel); + const catSelect = document.createElement('select'); + for (const cat of categories) { + const opt = document.createElement('option'); + opt.value = cat; + opt.textContent = cat.charAt(0).toUpperCase() + cat.slice(1); + catSelect.appendChild(opt); + } + f.appendChild(catSelect); + + // Actions + const actions = document.createElement('div'); + actions.className = 'catalog-create-actions'; + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'btn-cancel'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.addEventListener('click', () => this._hideCreateForm()); + const submitBtn = document.createElement('button'); + submitBtn.className = 'btn-submit'; + submitBtn.textContent = 'Add to Catalog'; + submitBtn.addEventListener('click', () => { + const name = nameInput.value.trim(); + if (!name) { nameInput.focus(); return; } + const w = parseFloat(widthInput.value) || 0.8; + const d = parseFloat(depthInput.value) || 0.4; + const h = parseFloat(heightInput.value) || 0.8; + const color = colorText.value || '#8899aa'; + const category = catSelect.value; + this._addCustomItem({ name, width: w, depth: d, height: h, color, category }); + this._hideCreateForm(); + }); + actions.appendChild(cancelBtn); + actions.appendChild(submitBtn); + f.appendChild(actions); + + nameInput.focus(); + } + + _addCustomItem({ name, width, depth, height, color, category }) { + const catalog = this.renderer.catalogData; + if (!catalog) return; + + // Generate unique id + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); + let id = `custom-${slug}`; + let n = 1; + while (this.renderer._catalogIndex.has(id)) { + id = `custom-${slug}-${++n}`; + } + + // Build a simple box mesh matching the catalog format + const item = { + id, + name, + category, + rooms: [], + dimensions: { width, depth, height }, + mesh: { + type: 'group', + parts: [ + { + name: 'body', + geometry: 'box', + size: [width, height, depth], + position: [0, height / 2, 0], + color + } + ] + } + }; + + catalog.items.push(item); + this.renderer._catalogIndex.set(id, item); + + // Refresh display + this._renderItems(); + } + // ---- Events ---- _bindEvents() { diff --git a/src/index.html b/src/index.html index 442721b..1c5a9ba 100644 --- a/src/index.html +++ b/src/index.html @@ -207,6 +207,103 @@ font-size: 12px; } + /* Custom furniture creator */ + .catalog-create-btn { + display: block; + width: calc(100% - 24px); + margin: 8px 12px; + padding: 8px; + border: 1px dashed #4a90d9; + border-radius: 4px; + background: transparent; + color: #4a90d9; + cursor: pointer; + font-size: 12px; + font-weight: 500; + } + .catalog-create-btn:hover { + background: #e8f0fe; + } + .catalog-create-form { + padding: 0 12px 12px; + border-top: 1px solid #eee; + margin-top: 4px; + } + .catalog-create-form label { + display: block; + font-size: 11px; + color: #666; + margin-top: 8px; + margin-bottom: 2px; + } + .catalog-create-form input, + .catalog-create-form select { + width: 100%; + padding: 5px 8px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + outline: none; + } + .catalog-create-form input:focus, + .catalog-create-form select:focus { + border-color: #4a90d9; + } + .catalog-create-dims { + display: flex; + gap: 6px; + } + .catalog-create-dims > div { + flex: 1; + } + .catalog-create-dims label { + margin-top: 0 !important; + } + .catalog-create-color-row { + display: flex; + align-items: center; + gap: 8px; + } + .catalog-create-color-row input[type="color"] { + width: 32px; + height: 28px; + padding: 1px; + border: 1px solid #ccc; + border-radius: 3px; + cursor: pointer; + flex-shrink: 0; + } + .catalog-create-color-row input[type="text"] { + flex: 1; + } + .catalog-create-actions { + display: flex; + gap: 6px; + margin-top: 10px; + } + .catalog-create-actions button { + flex: 1; + padding: 6px; + border: 1px solid #ccc; + border-radius: 3px; + font-size: 12px; + cursor: pointer; + } + .catalog-create-actions .btn-submit { + background: #4a90d9; + color: #fff; + border-color: #4a90d9; + } + .catalog-create-actions .btn-submit:hover { + background: #3a7bc8; + } + .catalog-create-actions .btn-cancel { + background: #fff; + } + .catalog-create-actions .btn-cancel:hover { + background: #f0f0f0; + } + .export-section { margin-top: 16px; padding-top: 12px;