Add custom furniture creator form to catalog panel

Adds a "Create Custom Furniture" button at the bottom of the catalog
sidebar that expands into a form with fields for name, dimensions
(W/D/H in meters), color picker, and category selector. Submitting
creates a simple box-geometry catalog item and adds it to the
in-memory catalog for immediate placement into rooms.
This commit is contained in:
m
2026-02-07 12:45:09 +01:00
parent d35b61648e
commit 53ee0fc1ec
2 changed files with 268 additions and 0 deletions

View File

@@ -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() {

View File

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