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:
171
src/catalog.js
171
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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user