Adds HouseEditor panel to the right sidebar with: - Edit house name and description - Adjust building footprint dimensions - Add/remove floors with ceiling height control - Add/remove rooms with name, type, and dimensions - Edit selected room: name, type, dimensions, position, flooring - Save house as JSON template - Create new empty house from scratch
682 lines
21 KiB
JavaScript
682 lines
21 KiB
JavaScript
/**
|
|
* HouseEditor — UI panel for house customization.
|
|
*
|
|
* Provides controls to create/edit houses, add/remove rooms,
|
|
* adjust dimensions, manage floors, and save as templates.
|
|
*/
|
|
export class HouseEditor {
|
|
constructor(container, { renderer, onHouseChanged }) {
|
|
this.container = container;
|
|
this.renderer = renderer;
|
|
this.onHouseChanged = onHouseChanged || (() => {});
|
|
this._editing = false;
|
|
this._selectedRoomId = null;
|
|
this.render();
|
|
}
|
|
|
|
get houseData() {
|
|
return this.renderer.houseData;
|
|
}
|
|
|
|
render() {
|
|
this.container.innerHTML = '';
|
|
|
|
if (!this.houseData) {
|
|
this.container.innerHTML = '<p style="color:#999;font-size:12px;">No house loaded</p>';
|
|
return;
|
|
}
|
|
|
|
// Toggle button
|
|
const toggleBtn = document.createElement('button');
|
|
toggleBtn.className = 'he-toggle-btn';
|
|
toggleBtn.textContent = this._editing ? 'Close Editor' : 'Edit House';
|
|
toggleBtn.addEventListener('click', () => {
|
|
this._editing = !this._editing;
|
|
this.render();
|
|
});
|
|
this.container.appendChild(toggleBtn);
|
|
|
|
if (!this._editing) return;
|
|
|
|
// House metadata section
|
|
this._renderMetadataSection();
|
|
|
|
// Building section
|
|
this._renderBuildingSection();
|
|
|
|
// Floor management
|
|
this._renderFloorSection();
|
|
|
|
// Room list with editing
|
|
this._renderRoomSection();
|
|
|
|
// Room editor (if a room is selected)
|
|
if (this._selectedRoomId) {
|
|
this._renderRoomEditor();
|
|
}
|
|
|
|
// Save as template
|
|
this._renderSaveSection();
|
|
}
|
|
|
|
setSelectedRoom(roomId) {
|
|
this._selectedRoomId = roomId;
|
|
if (this._editing) {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
// ---- Metadata Section ----
|
|
|
|
_renderMetadataSection() {
|
|
const section = this._createSection('House Info');
|
|
|
|
const nameRow = this._createFieldRow('Name');
|
|
const nameInput = document.createElement('input');
|
|
nameInput.className = 'he-input';
|
|
nameInput.value = this.houseData.name || '';
|
|
nameInput.addEventListener('change', () => {
|
|
this.houseData.name = nameInput.value;
|
|
this.onHouseChanged('name');
|
|
});
|
|
nameRow.appendChild(nameInput);
|
|
section.appendChild(nameRow);
|
|
|
|
const descRow = this._createFieldRow('Description');
|
|
const descInput = document.createElement('input');
|
|
descInput.className = 'he-input';
|
|
descInput.value = this.houseData.description || '';
|
|
descInput.addEventListener('change', () => {
|
|
this.houseData.description = descInput.value;
|
|
this.onHouseChanged('description');
|
|
});
|
|
descRow.appendChild(descInput);
|
|
section.appendChild(descRow);
|
|
|
|
this.container.appendChild(section);
|
|
}
|
|
|
|
// ---- Building Section ----
|
|
|
|
_renderBuildingSection() {
|
|
const section = this._createSection('Building');
|
|
const building = this.houseData.building || {};
|
|
const footprint = building.footprint || {};
|
|
|
|
const widthRow = this._createFieldRow('Width (m)');
|
|
const widthInput = this._createNumberInput(footprint.width || 12, 4, 30, 0.5, (val) => {
|
|
if (!this.houseData.building) this.houseData.building = {};
|
|
if (!this.houseData.building.footprint) this.houseData.building.footprint = {};
|
|
this.houseData.building.footprint.width = val;
|
|
this.onHouseChanged('building');
|
|
});
|
|
widthRow.appendChild(widthInput);
|
|
section.appendChild(widthRow);
|
|
|
|
const depthRow = this._createFieldRow('Depth (m)');
|
|
const depthInput = this._createNumberInput(footprint.depth || 10, 4, 30, 0.5, (val) => {
|
|
if (!this.houseData.building) this.houseData.building = {};
|
|
if (!this.houseData.building.footprint) this.houseData.building.footprint = {};
|
|
this.houseData.building.footprint.depth = val;
|
|
this.onHouseChanged('building');
|
|
});
|
|
depthRow.appendChild(depthInput);
|
|
section.appendChild(depthRow);
|
|
|
|
this.container.appendChild(section);
|
|
}
|
|
|
|
// ---- Floor Section ----
|
|
|
|
_renderFloorSection() {
|
|
const section = this._createSection('Floors');
|
|
|
|
const floors = this.houseData.floors || [];
|
|
for (let i = 0; i < floors.length; i++) {
|
|
const floor = floors[i];
|
|
const row = document.createElement('div');
|
|
row.className = 'he-floor-row';
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'he-floor-label';
|
|
label.textContent = `${floor.name} (${floor.rooms.length} rooms)`;
|
|
row.appendChild(label);
|
|
|
|
// Ceiling height
|
|
const heightInput = this._createNumberInput(floor.ceilingHeight || 2.5, 2.2, 4.0, 0.1, (val) => {
|
|
floor.ceilingHeight = val;
|
|
this._rebuildFloor();
|
|
});
|
|
heightInput.title = 'Ceiling height';
|
|
heightInput.style.width = '55px';
|
|
row.appendChild(heightInput);
|
|
|
|
// Remove floor button (only if more than 1 floor)
|
|
if (floors.length > 1) {
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.className = 'he-icon-btn he-icon-btn-danger';
|
|
removeBtn.textContent = '\u00d7';
|
|
removeBtn.title = 'Remove floor';
|
|
removeBtn.addEventListener('click', () => this._removeFloor(i));
|
|
row.appendChild(removeBtn);
|
|
}
|
|
|
|
section.appendChild(row);
|
|
}
|
|
|
|
const addBtn = document.createElement('button');
|
|
addBtn.className = 'he-add-btn';
|
|
addBtn.textContent = '+ Add Floor';
|
|
addBtn.addEventListener('click', () => this._addFloor());
|
|
section.appendChild(addBtn);
|
|
|
|
this.container.appendChild(section);
|
|
}
|
|
|
|
// ---- Room Section ----
|
|
|
|
_renderRoomSection() {
|
|
const section = this._createSection('Rooms');
|
|
|
|
const floor = this.houseData.floors[this.renderer.currentFloor];
|
|
if (!floor) return;
|
|
|
|
for (const room of floor.rooms) {
|
|
const row = document.createElement('div');
|
|
row.className = 'he-room-row' + (this._selectedRoomId === room.id ? ' active' : '');
|
|
|
|
const info = document.createElement('span');
|
|
info.className = 'he-room-info';
|
|
info.textContent = `${room.name} (${room.dimensions.width}\u00d7${room.dimensions.length}m)`;
|
|
info.addEventListener('click', () => {
|
|
this._selectedRoomId = room.id;
|
|
this.renderer.focusRoom(room.id);
|
|
this.render();
|
|
});
|
|
row.appendChild(info);
|
|
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.className = 'he-icon-btn he-icon-btn-danger';
|
|
removeBtn.textContent = '\u00d7';
|
|
removeBtn.title = 'Remove room';
|
|
removeBtn.addEventListener('click', () => this._removeRoom(room.id));
|
|
row.appendChild(removeBtn);
|
|
|
|
section.appendChild(row);
|
|
}
|
|
|
|
const addBtn = document.createElement('button');
|
|
addBtn.className = 'he-add-btn';
|
|
addBtn.textContent = '+ Add Room';
|
|
addBtn.addEventListener('click', () => this._showAddRoomForm(section, addBtn));
|
|
section.appendChild(addBtn);
|
|
|
|
this.container.appendChild(section);
|
|
}
|
|
|
|
// ---- Room Editor ----
|
|
|
|
_renderRoomEditor() {
|
|
const floor = this.houseData.floors[this.renderer.currentFloor];
|
|
if (!floor) return;
|
|
const room = floor.rooms.find(r => r.id === this._selectedRoomId);
|
|
if (!room) return;
|
|
|
|
const section = this._createSection(`Edit: ${room.name}`);
|
|
|
|
// Name
|
|
const nameRow = this._createFieldRow('Name');
|
|
const nameInput = document.createElement('input');
|
|
nameInput.className = 'he-input';
|
|
nameInput.value = room.name;
|
|
nameInput.addEventListener('change', () => {
|
|
room.name = nameInput.value;
|
|
this._rebuildFloor();
|
|
});
|
|
nameRow.appendChild(nameInput);
|
|
section.appendChild(nameRow);
|
|
|
|
// English name
|
|
const nameEnRow = this._createFieldRow('Name (EN)');
|
|
const nameEnInput = document.createElement('input');
|
|
nameEnInput.className = 'he-input';
|
|
nameEnInput.value = room.nameEN || '';
|
|
nameEnInput.addEventListener('change', () => {
|
|
room.nameEN = nameEnInput.value;
|
|
this._rebuildFloor();
|
|
});
|
|
nameEnRow.appendChild(nameEnInput);
|
|
section.appendChild(nameEnRow);
|
|
|
|
// Type
|
|
const typeRow = this._createFieldRow('Type');
|
|
const typeSelect = document.createElement('select');
|
|
typeSelect.className = 'he-input';
|
|
const roomTypes = [
|
|
'living', 'kitchen', 'dining', 'bedroom', 'bathroom',
|
|
'hallway', 'office', 'utility', 'storage', 'garage'
|
|
];
|
|
for (const t of roomTypes) {
|
|
const opt = document.createElement('option');
|
|
opt.value = t;
|
|
opt.textContent = t.charAt(0).toUpperCase() + t.slice(1);
|
|
if (room.type === t) opt.selected = true;
|
|
typeSelect.appendChild(opt);
|
|
}
|
|
typeSelect.addEventListener('change', () => {
|
|
room.type = typeSelect.value;
|
|
});
|
|
typeRow.appendChild(typeSelect);
|
|
section.appendChild(typeRow);
|
|
|
|
// Dimensions
|
|
const dimsLabel = document.createElement('div');
|
|
dimsLabel.className = 'he-field-label';
|
|
dimsLabel.textContent = 'Dimensions';
|
|
section.appendChild(dimsLabel);
|
|
|
|
const dimsRow = document.createElement('div');
|
|
dimsRow.className = 'he-dims-row';
|
|
|
|
const wLabel = document.createElement('label');
|
|
wLabel.className = 'he-dim-label';
|
|
wLabel.textContent = 'W';
|
|
dimsRow.appendChild(wLabel);
|
|
const wInput = this._createNumberInput(room.dimensions.width, 1, 15, 0.25, (val) => {
|
|
room.dimensions.width = val;
|
|
this._rebuildFloor();
|
|
});
|
|
dimsRow.appendChild(wInput);
|
|
|
|
const lLabel = document.createElement('label');
|
|
lLabel.className = 'he-dim-label';
|
|
lLabel.textContent = 'L';
|
|
dimsRow.appendChild(lLabel);
|
|
const lInput = this._createNumberInput(room.dimensions.length, 1, 15, 0.25, (val) => {
|
|
room.dimensions.length = val;
|
|
this._rebuildFloor();
|
|
});
|
|
dimsRow.appendChild(lInput);
|
|
|
|
section.appendChild(dimsRow);
|
|
|
|
// Position
|
|
const posLabel = document.createElement('div');
|
|
posLabel.className = 'he-field-label';
|
|
posLabel.textContent = 'Position';
|
|
section.appendChild(posLabel);
|
|
|
|
const posRow = document.createElement('div');
|
|
posRow.className = 'he-dims-row';
|
|
|
|
const xLabel = document.createElement('label');
|
|
xLabel.className = 'he-dim-label';
|
|
xLabel.textContent = 'X';
|
|
posRow.appendChild(xLabel);
|
|
const xInput = this._createNumberInput(room.position.x, 0, 30, 0.25, (val) => {
|
|
room.position.x = val;
|
|
this._rebuildFloor();
|
|
});
|
|
posRow.appendChild(xInput);
|
|
|
|
const yLabel = document.createElement('label');
|
|
yLabel.className = 'he-dim-label';
|
|
yLabel.textContent = 'Y';
|
|
posRow.appendChild(yLabel);
|
|
const yInput = this._createNumberInput(room.position.y, 0, 30, 0.25, (val) => {
|
|
room.position.y = val;
|
|
this._rebuildFloor();
|
|
});
|
|
posRow.appendChild(yInput);
|
|
|
|
section.appendChild(posRow);
|
|
|
|
// Flooring
|
|
const flooringRow = this._createFieldRow('Flooring');
|
|
const flooringSelect = document.createElement('select');
|
|
flooringSelect.className = 'he-input';
|
|
for (const f of ['hardwood', 'tile']) {
|
|
const opt = document.createElement('option');
|
|
opt.value = f;
|
|
opt.textContent = f.charAt(0).toUpperCase() + f.slice(1);
|
|
if (room.flooring === f) opt.selected = true;
|
|
flooringSelect.appendChild(opt);
|
|
}
|
|
flooringSelect.addEventListener('change', () => {
|
|
room.flooring = flooringSelect.value;
|
|
this._rebuildFloor();
|
|
});
|
|
flooringRow.appendChild(flooringSelect);
|
|
section.appendChild(flooringRow);
|
|
|
|
this.container.appendChild(section);
|
|
}
|
|
|
|
// ---- Save Section ----
|
|
|
|
_renderSaveSection() {
|
|
const section = this._createSection('Template');
|
|
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.className = 'he-save-btn';
|
|
saveBtn.textContent = 'Save as House Template';
|
|
saveBtn.addEventListener('click', () => this._saveAsTemplate());
|
|
section.appendChild(saveBtn);
|
|
|
|
const newBtn = document.createElement('button');
|
|
newBtn.className = 'he-add-btn';
|
|
newBtn.style.marginTop = '6px';
|
|
newBtn.textContent = 'New Empty House';
|
|
newBtn.addEventListener('click', () => this._createNewHouse());
|
|
section.appendChild(newBtn);
|
|
|
|
this.container.appendChild(section);
|
|
}
|
|
|
|
// ---- Actions ----
|
|
|
|
_addFloor() {
|
|
const floors = this.houseData.floors;
|
|
const level = floors.length;
|
|
const id = `floor-${level}`;
|
|
floors.push({
|
|
id,
|
|
name: `Floor ${level}`,
|
|
nameEN: `Floor ${level}`,
|
|
level,
|
|
ceilingHeight: 2.5,
|
|
rooms: []
|
|
});
|
|
this.onHouseChanged('floors');
|
|
this.render();
|
|
}
|
|
|
|
_removeFloor(index) {
|
|
this.houseData.floors.splice(index, 1);
|
|
// Re-index levels
|
|
this.houseData.floors.forEach((f, i) => { f.level = i; });
|
|
// If current floor was removed, switch to last available
|
|
if (this.renderer.currentFloor >= this.houseData.floors.length) {
|
|
this.renderer.showFloor(this.houseData.floors.length - 1);
|
|
} else {
|
|
this._rebuildFloor();
|
|
}
|
|
this.onHouseChanged('floors');
|
|
this.render();
|
|
}
|
|
|
|
_showAddRoomForm(section, addBtn) {
|
|
// Replace button with form
|
|
addBtn.style.display = 'none';
|
|
|
|
const form = document.createElement('div');
|
|
form.className = 'he-add-room-form';
|
|
|
|
const nameInput = document.createElement('input');
|
|
nameInput.className = 'he-input';
|
|
nameInput.placeholder = 'Room name';
|
|
form.appendChild(nameInput);
|
|
|
|
const typeSelect = document.createElement('select');
|
|
typeSelect.className = 'he-input';
|
|
const roomTypes = [
|
|
'living', 'kitchen', 'dining', 'bedroom', 'bathroom',
|
|
'hallway', 'office', 'utility', 'storage', 'garage'
|
|
];
|
|
for (const t of roomTypes) {
|
|
const opt = document.createElement('option');
|
|
opt.value = t;
|
|
opt.textContent = t.charAt(0).toUpperCase() + t.slice(1);
|
|
typeSelect.appendChild(opt);
|
|
}
|
|
form.appendChild(typeSelect);
|
|
|
|
const dimsRow = document.createElement('div');
|
|
dimsRow.className = 'he-dims-row';
|
|
const wInput = document.createElement('input');
|
|
wInput.className = 'he-input';
|
|
wInput.type = 'number';
|
|
wInput.value = '4';
|
|
wInput.min = '1';
|
|
wInput.max = '15';
|
|
wInput.step = '0.25';
|
|
wInput.placeholder = 'Width';
|
|
wInput.style.flex = '1';
|
|
dimsRow.appendChild(wInput);
|
|
|
|
const xSpan = document.createElement('span');
|
|
xSpan.textContent = '\u00d7';
|
|
xSpan.style.padding = '0 4px';
|
|
xSpan.style.color = '#999';
|
|
dimsRow.appendChild(xSpan);
|
|
|
|
const lInput = document.createElement('input');
|
|
lInput.className = 'he-input';
|
|
lInput.type = 'number';
|
|
lInput.value = '3';
|
|
lInput.min = '1';
|
|
lInput.max = '15';
|
|
lInput.step = '0.25';
|
|
lInput.placeholder = 'Length';
|
|
lInput.style.flex = '1';
|
|
dimsRow.appendChild(lInput);
|
|
form.appendChild(dimsRow);
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'he-form-actions';
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.className = 'he-cancel-btn';
|
|
cancelBtn.textContent = 'Cancel';
|
|
cancelBtn.addEventListener('click', () => {
|
|
form.remove();
|
|
addBtn.style.display = '';
|
|
});
|
|
actions.appendChild(cancelBtn);
|
|
|
|
const submitBtn = document.createElement('button');
|
|
submitBtn.className = 'he-submit-btn';
|
|
submitBtn.textContent = 'Add';
|
|
submitBtn.addEventListener('click', () => {
|
|
const name = nameInput.value.trim();
|
|
if (!name) { nameInput.focus(); return; }
|
|
this._addRoom({
|
|
name,
|
|
type: typeSelect.value,
|
|
width: parseFloat(wInput.value) || 4,
|
|
length: parseFloat(lInput.value) || 3
|
|
});
|
|
});
|
|
actions.appendChild(submitBtn);
|
|
form.appendChild(actions);
|
|
|
|
section.appendChild(form);
|
|
nameInput.focus();
|
|
}
|
|
|
|
_addRoom({ name, type, width, length }) {
|
|
const floor = this.houseData.floors[this.renderer.currentFloor];
|
|
if (!floor) return;
|
|
|
|
// Auto-position: find rightmost edge of existing rooms
|
|
let maxX = 0;
|
|
for (const r of floor.rooms) {
|
|
const edge = r.position.x + r.dimensions.width;
|
|
if (edge > maxX) maxX = edge;
|
|
}
|
|
|
|
const id = `${floor.id}-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${Date.now().toString(36).slice(-4)}`;
|
|
const room = {
|
|
id,
|
|
name,
|
|
nameEN: name,
|
|
type,
|
|
position: { x: maxX + 0.24, y: 0 },
|
|
dimensions: { width, length },
|
|
flooring: (type === 'bathroom' || type === 'kitchen' || type === 'utility') ? 'tile' : 'hardwood',
|
|
walls: {
|
|
south: { type: 'exterior' },
|
|
north: { type: 'exterior' },
|
|
west: { type: 'interior' },
|
|
east: { type: 'exterior' }
|
|
}
|
|
};
|
|
|
|
floor.rooms.push(room);
|
|
this._rebuildFloor();
|
|
this._selectedRoomId = room.id;
|
|
this.onHouseChanged('rooms');
|
|
this.render();
|
|
}
|
|
|
|
_removeRoom(roomId) {
|
|
const floor = this.houseData.floors[this.renderer.currentFloor];
|
|
if (!floor) return;
|
|
|
|
const idx = floor.rooms.findIndex(r => r.id === roomId);
|
|
if (idx === -1) return;
|
|
|
|
floor.rooms.splice(idx, 1);
|
|
|
|
if (this._selectedRoomId === roomId) {
|
|
this._selectedRoomId = null;
|
|
}
|
|
|
|
this._rebuildFloor();
|
|
this.onHouseChanged('rooms');
|
|
this.render();
|
|
}
|
|
|
|
_saveAsTemplate() {
|
|
const data = structuredClone(this.houseData);
|
|
data.savedAt = new Date().toISOString();
|
|
const json = JSON.stringify(data, null, 2);
|
|
const blob = new Blob([json], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${(data.name || 'house').replace(/\s+/g, '-').toLowerCase()}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
_createNewHouse() {
|
|
const newHouse = {
|
|
name: 'New House',
|
|
description: '',
|
|
units: 'meters',
|
|
building: {
|
|
footprint: { width: 10, depth: 8 },
|
|
wallThickness: 0.24,
|
|
roofType: 'gable'
|
|
},
|
|
floors: [
|
|
{
|
|
id: 'floor-0',
|
|
name: 'Ground Floor',
|
|
nameEN: 'Ground Floor',
|
|
level: 0,
|
|
ceilingHeight: 2.6,
|
|
rooms: [
|
|
{
|
|
id: 'floor-0-hallway',
|
|
name: 'Hallway',
|
|
nameEN: 'Hallway',
|
|
type: 'hallway',
|
|
position: { x: 3.5, y: 0 },
|
|
dimensions: { width: 2.0, length: 8.0 },
|
|
flooring: 'tile',
|
|
walls: {
|
|
south: { type: 'exterior', doors: [{ id: 'entry', type: 'entry', position: 0.3, width: 1.1, height: 2.2, connectsTo: 'exterior' }] },
|
|
north: { type: 'exterior' },
|
|
west: { type: 'interior' },
|
|
east: { type: 'interior' }
|
|
}
|
|
},
|
|
{
|
|
id: 'floor-0-living',
|
|
name: 'Living Room',
|
|
nameEN: 'Living Room',
|
|
type: 'living',
|
|
position: { x: 0, y: 3.0 },
|
|
dimensions: { width: 3.5, length: 5.0 },
|
|
flooring: 'hardwood',
|
|
walls: {
|
|
south: { type: 'interior' },
|
|
north: { type: 'exterior', windows: [{ id: 'lr-w1', type: 'casement', position: 0.5, width: 1.4, height: 1.4, sillHeight: 0.6 }] },
|
|
west: { type: 'exterior', windows: [{ id: 'lr-w2', type: 'casement', position: 1.5, width: 1.2, height: 1.4, sillHeight: 0.6 }] },
|
|
east: { type: 'interior' }
|
|
}
|
|
},
|
|
{
|
|
id: 'floor-0-kitchen',
|
|
name: 'Kitchen',
|
|
nameEN: 'Kitchen',
|
|
type: 'kitchen',
|
|
position: { x: 0, y: 0 },
|
|
dimensions: { width: 3.5, length: 3.0 },
|
|
flooring: 'tile',
|
|
walls: {
|
|
south: { type: 'exterior', windows: [{ id: 'k-w1', type: 'casement', position: 1.0, width: 1.2, height: 1.2, sillHeight: 0.9 }] },
|
|
north: { type: 'interior' },
|
|
west: { type: 'exterior' },
|
|
east: { type: 'interior' }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
this.renderer.houseData = newHouse;
|
|
this.renderer.showFloor(0);
|
|
this._selectedRoomId = null;
|
|
this.onHouseChanged('new');
|
|
this.render();
|
|
}
|
|
|
|
_rebuildFloor() {
|
|
this.renderer.showFloor(this.renderer.currentFloor);
|
|
this.onHouseChanged('rebuild');
|
|
}
|
|
|
|
// ---- UI Helpers ----
|
|
|
|
_createSection(title) {
|
|
const section = document.createElement('div');
|
|
section.className = 'he-section';
|
|
const h = document.createElement('div');
|
|
h.className = 'he-section-title';
|
|
h.textContent = title;
|
|
section.appendChild(h);
|
|
return section;
|
|
}
|
|
|
|
_createFieldRow(label) {
|
|
const row = document.createElement('div');
|
|
row.className = 'he-field-row';
|
|
const lbl = document.createElement('label');
|
|
lbl.className = 'he-field-label';
|
|
lbl.textContent = label;
|
|
row.appendChild(lbl);
|
|
return row;
|
|
}
|
|
|
|
_createNumberInput(value, min, max, step, onChange) {
|
|
const input = document.createElement('input');
|
|
input.className = 'he-input he-num-input';
|
|
input.type = 'number';
|
|
input.value = value;
|
|
input.min = min;
|
|
input.max = max;
|
|
input.step = step;
|
|
input.addEventListener('change', () => {
|
|
const val = parseFloat(input.value);
|
|
if (!isNaN(val) && val >= min && val <= max) {
|
|
onChange(val);
|
|
}
|
|
});
|
|
return input;
|
|
}
|
|
}
|