234 lines
6.4 KiB
JavaScript
234 lines
6.4 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
}
|