Files
house-design/src/catalog.js

234 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* CatalogPanel — left sidebar for browsing furniture catalog.
*
* Shows categories, search, and item cards. Clicking an item
* adds it to the center of the selected room via DesignState.
*/
export class CatalogPanel {
constructor(container, { renderer, state, interaction }) {
this.container = container;
this.renderer = renderer;
this.state = state;
this.interaction = interaction;
this.selectedCategory = 'all';
this.searchQuery = '';
this.selectedRoomId = null;
this._build();
this._bindEvents();
}
// ---- Build DOM ----
_build() {
this.container.innerHTML = '';
this.container.className = 'catalog-panel';
// Search
const searchWrap = document.createElement('div');
searchWrap.className = 'catalog-search';
this._searchInput = document.createElement('input');
this._searchInput.type = 'text';
this._searchInput.placeholder = 'Search furniture...';
this._searchInput.className = 'catalog-search-input';
searchWrap.appendChild(this._searchInput);
this.container.appendChild(searchWrap);
// Categories
this._categoryBar = document.createElement('div');
this._categoryBar.className = 'catalog-categories';
this.container.appendChild(this._categoryBar);
// Items list
this._itemList = document.createElement('div');
this._itemList.className = 'catalog-items';
this.container.appendChild(this._itemList);
this._renderCategories();
this._renderItems();
}
_renderCategories() {
const catalog = this.renderer.catalogData;
if (!catalog) return;
this._categoryBar.innerHTML = '';
const categories = ['all', ...catalog.categories];
const LABELS = {
all: 'All',
seating: 'Seating',
tables: 'Tables',
storage: 'Storage',
beds: 'Beds',
bathroom: 'Bath',
kitchen: 'Kitchen',
office: 'Office',
lighting: 'Lighting',
decor: 'Decor'
};
for (const cat of categories) {
const btn = document.createElement('button');
btn.className = 'catalog-cat-btn' + (cat === this.selectedCategory ? ' active' : '');
btn.textContent = LABELS[cat] || cat;
btn.dataset.category = cat;
btn.addEventListener('click', () => {
this.selectedCategory = cat;
this._renderCategories();
this._renderItems();
});
this._categoryBar.appendChild(btn);
}
}
_renderItems() {
const catalog = this.renderer.catalogData;
if (!catalog) {
this._itemList.innerHTML = '<div class="catalog-empty">No catalog loaded</div>';
return;
}
let items = catalog.items;
// Filter by category
if (this.selectedCategory !== 'all') {
items = items.filter(it => it.category === this.selectedCategory);
}
// Filter by search
if (this.searchQuery) {
const q = this.searchQuery.toLowerCase();
items = items.filter(it =>
it.name.toLowerCase().includes(q) ||
it.id.toLowerCase().includes(q) ||
it.category.toLowerCase().includes(q)
);
}
this._itemList.innerHTML = '';
if (items.length === 0) {
this._itemList.innerHTML = '<div class="catalog-empty">No items found</div>';
return;
}
for (const item of items) {
const card = this._createItemCard(item);
this._itemList.appendChild(card);
}
}
_createItemCard(item) {
const card = document.createElement('div');
card.className = 'catalog-item';
card.dataset.catalogId = item.id;
// Color swatch from first part
const color = item.mesh?.parts?.[0]?.color || '#888';
const dims = item.dimensions;
const dimStr = `${dims.width}×${dims.depth}×${dims.height}m`;
card.innerHTML =
`<div class="catalog-item-swatch" style="background:${color}"></div>` +
`<div class="catalog-item-info">` +
`<div class="catalog-item-name">${item.name}</div>` +
`<div class="catalog-item-dims">${dimStr}</div>` +
`</div>` +
`<button class="catalog-item-add" title="Add to room">+</button>`;
card.querySelector('.catalog-item-add').addEventListener('click', (e) => {
e.stopPropagation();
this._placeItem(item);
});
card.addEventListener('click', () => {
this._placeItem(item);
});
return card;
}
// ---- Place item ----
_placeItem(catalogItem) {
// Determine target room
const roomId = this._getTargetRoom(catalogItem);
if (!roomId) return;
// Get room center for initial placement
const floor = this.renderer.houseData.floors[this.renderer.currentFloor];
const room = floor?.rooms.find(r => r.id === roomId);
if (!room) return;
// Place at room center (local coords)
const cx = room.dimensions.width / 2;
const cz = room.dimensions.length / 2;
const placement = {
catalogId: catalogItem.id,
position: { x: cx, z: cz },
rotation: 0,
wallMounted: false
};
// InteractionManager handles the re-render via furniture-add event
this.state.addFurniture(roomId, placement);
}
_getTargetRoom(catalogItem) {
// Use currently selected room from interaction manager or sidebar
if (this.selectedRoomId) {
return this.selectedRoomId;
}
// If interaction has a room selected (via furniture selection)
if (this.interaction.selectedRoomId) {
return this.interaction.selectedRoomId;
}
// Try to find a matching room on the current floor
const rooms = this.renderer.getRooms();
if (rooms.length === 0) return null;
// If catalog item has room hints, try to match
if (catalogItem.rooms && catalogItem.rooms.length > 0) {
for (const room of rooms) {
// Room ids contain the room type (eg "eg-wohnzimmer")
for (const hint of catalogItem.rooms) {
if (room.id.includes(hint)) return room.id;
}
}
}
// Fallback: first room on the floor
return rooms[0].id;
}
// ---- Events ----
_bindEvents() {
this._searchInput.addEventListener('input', () => {
this.searchQuery = this._searchInput.value.trim();
this._renderItems();
});
// Track room selection from main sidebar
this.renderer.container.addEventListener('roomclick', (e) => {
this.selectedRoomId = e.detail.roomId;
});
}
/** Called externally when a room is selected in the main sidebar. */
setSelectedRoom(roomId) {
this.selectedRoomId = roomId;
}
/** Refresh the item list (e.g., after floor change). */
refresh() {
this._renderItems();
}
}