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._itemList.className = 'catalog-items';
|
||||||
this.container.appendChild(this._itemList);
|
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._renderCategories();
|
||||||
this._renderItems();
|
this._renderItems();
|
||||||
}
|
}
|
||||||
@@ -207,6 +220,164 @@ export class CatalogPanel {
|
|||||||
return rooms[0].id;
|
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 ----
|
// ---- Events ----
|
||||||
|
|
||||||
_bindEvents() {
|
_bindEvents() {
|
||||||
|
|||||||
@@ -207,6 +207,103 @@
|
|||||||
font-size: 12px;
|
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 {
|
.export-section {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user