diff --git a/src/catalog.js b/src/catalog.js new file mode 100644 index 0000000..0347933 --- /dev/null +++ b/src/catalog.js @@ -0,0 +1,233 @@ +/** + * 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 = '
No catalog loaded
'; + 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 = '
No items found
'; + 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 = + `
` + + `
` + + `
${item.name}
` + + `
${dimStr}
` + + `
` + + ``; + + 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(); + } +} diff --git a/src/index.html b/src/index.html index 5921281..442721b 100644 --- a/src/index.html +++ b/src/index.html @@ -263,6 +263,7 @@ import { InteractionManager } from './interaction.js'; import { ThemeManager } from './themes.js'; import { ExportManager } from './export.js'; + import { CatalogPanel } from './catalog.js'; const viewer = document.getElementById('viewer'); const houseRenderer = new HouseRenderer(viewer); @@ -272,6 +273,7 @@ let interaction = null; let themeManager = null; let exportManager = null; + let catalogPanel = null; houseRenderer.loadHouse('../data/sample-house.json').then(async (house) => { document.getElementById('house-name').textContent = house.name; @@ -294,6 +296,11 @@ themeManager = new ThemeManager(houseRenderer); exportManager = new ExportManager(houseRenderer, designState); + catalogPanel = new CatalogPanel(document.getElementById('catalog-panel'), { + renderer: houseRenderer, + state: designState, + interaction + }); buildFloorButtons(); buildRoomList(); buildThemeButtons(); @@ -316,6 +323,7 @@ btn.classList.add('active'); buildRoomList(); selectedRoom = null; + if (catalogPanel) catalogPanel.setSelectedRoom(null); }); container.appendChild(btn); } @@ -340,6 +348,7 @@ document.querySelectorAll('.room-item').forEach(el => { el.classList.toggle('active', el.dataset.roomId === roomId); }); + if (catalogPanel) catalogPanel.setSelectedRoom(roomId); const room = houseRenderer.getRooms().find(r => r.id === roomId); if (room) { document.getElementById('info').textContent = `${room.name} (${room.nameEN}) — ${room.area} m²`; diff --git a/src/interaction.js b/src/interaction.js index a91ff4d..1e1d518 100644 --- a/src/interaction.js +++ b/src/interaction.js @@ -349,9 +349,9 @@ export class InteractionManager { this._syncMeshFromState(_detail.roomId, _detail.index); } - if (type === 'furniture-remove') { - // Mesh was already removed by state; re-render the floor - this.clearSelection(); + if (type === 'furniture-add' || type === 'furniture-remove') { + // Re-render the floor to reflect added/removed furniture + if (type === 'furniture-remove') this.clearSelection(); this.renderer.designData = this.state.design; this.renderer._clearFloor(); const floor = this.renderer.houseData.floors[this.renderer.currentFloor];