Compare commits
2 Commits
d35b61648e
...
cf0fe586eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf0fe586eb | ||
|
|
53ee0fc1ec |
352
RESEARCH-ikea-data.md
Normal file
352
RESEARCH-ikea-data.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# IKEA Furniture Data Import — Research Report
|
||||||
|
|
||||||
|
**Task:** t-05dd7
|
||||||
|
**Date:** 2026-02-07
|
||||||
|
**Researcher:** researcher role
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**There is no official public IKEA API for furniture data.** However, there are multiple viable paths to get IKEA product dimensions and 3D models into our room planner. The recommended approach is a **tiered strategy**: start with a curated hand-built catalog of common IKEA items using known dimensions, with optional GLB model loading for enhanced visuals later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. IKEA APIs (Official)
|
||||||
|
|
||||||
|
### IKEA Has No Public Developer API
|
||||||
|
|
||||||
|
IKEA does not offer a public REST/GraphQL API for developers. There is no developer portal, no API keys, no official documentation.
|
||||||
|
|
||||||
|
### Undocumented Internal Endpoints
|
||||||
|
|
||||||
|
IKEA's website uses internal APIs that have been partially reverse-engineered:
|
||||||
|
|
||||||
|
| Endpoint | Purpose | Format |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| `sik.search.blue.cdtapps.com/{country}/{lang}/search-result-page?q={query}&size=24&types=PRODUCT` | Product search | JSON |
|
||||||
|
| `www.ikea.com/{country}/{lang}/products/{itemNo[5:]}/{itemNo}.json` | Product detail (PIP) | JSON |
|
||||||
|
| `api.ingka.ikea.com/cia/availabilities/{country}/{lang}?itemNos={itemNo}` | Stock/availability | JSON |
|
||||||
|
| `www.ikea.com/{region}/{country}/iows/catalog/availability/{itemNo}` | Legacy availability (deprecated Dec 2021) | XML |
|
||||||
|
| `web-api.ikea.com/{country}/{lang}/rotera/data/exists/{itemNo}/` | 3D model existence check | JSON |
|
||||||
|
| `web-api.ikea.com/{country}/{lang}/rotera/data/model/{itemNo}/` | 3D model metadata + CDN URL | JSON |
|
||||||
|
|
||||||
|
The **Rotera API** is most relevant for us — it returns a `modelUrl` field pointing to the actual GLB file on IKEA's CDN. This is what the Blender IKEA Browser add-on uses internally.
|
||||||
|
|
||||||
|
**IKEA Kreativ**: Their AI-powered room planner at `ikea.com/us/en/home-design/` uses AWS infrastructure (API Gateway at `ddh79d7xh5.execute-api.us-east-1.amazonaws.com`). No public API.
|
||||||
|
|
||||||
|
**Status**: All endpoints are undocumented, unsupported, and may break at any time. Rate limiting and anti-bot measures exist. The IOWS endpoints are deprecated.
|
||||||
|
|
||||||
|
**Sources**: https://sbmueller.github.io/posts/ikea/ | Postman workspace: https://www.postman.com/galactic-shuttle-566922/ikea/overview
|
||||||
|
|
||||||
|
### Archived Python Client
|
||||||
|
|
||||||
|
**vrslev/ikea-api-client** (GitHub, 1k+ stars) — Python client for IKEA internal APIs supporting cart, search, stock, item info, and 3D models. **Archived October 2024** — no longer maintained. MIT license.
|
||||||
|
|
||||||
|
- Supported: stock lookup, product search, item specs, 3D model retrieval (`RoteraItem` endpoint)
|
||||||
|
- Install: `pip install ikea-api[httpx]`
|
||||||
|
- **Not recommended** for new projects since archived
|
||||||
|
|
||||||
|
**Source**: https://github.com/vrslev/ikea-api-client
|
||||||
|
|
||||||
|
### npm: ikea-availability-checker (Active)
|
||||||
|
|
||||||
|
**Ephigenia/ikea-availability-checker** — Node.js package for checking IKEA stock across 40+ countries and 400+ stores. Actively maintained.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install ikea-availability-checker
|
||||||
|
# CLI: ikea-availability-checker stock --country us 30457903
|
||||||
|
```
|
||||||
|
|
||||||
|
Not directly useful for dimensions/models but proves Node.js access to IKEA endpoints is feasible.
|
||||||
|
|
||||||
|
**Source**: https://github.com/Ephigenia/ikea-availability-checker
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Open Datasets with Dimensions
|
||||||
|
|
||||||
|
### Best: Hugging Face — IKEA US CommerceTXT (30,511 products)
|
||||||
|
|
||||||
|
- **Records**: 30,511 products across 632 categories
|
||||||
|
- **Fields**: Name, SKU, Price, Dimensions (Width/Height/Depth), Materials, Category, Images, URLs
|
||||||
|
- **Format**: CommerceTXT v1.0.1 (text-based, parseable) + Parquet (56.5 MB)
|
||||||
|
- **License**: CC0 1.0 (Public Domain) — fully usable
|
||||||
|
- **Date**: July 15, 2025
|
||||||
|
- **Dimensions**: Yes, in the `@SPECS` section (e.g., `Width: 12", Height: 16"`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datasets import load_dataset
|
||||||
|
dataset = load_dataset("tsazan/ikea-us-commercetxt")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source**: https://huggingface.co/datasets/tsazan/ikea-us-commercetxt
|
||||||
|
|
||||||
|
### Good: Kaggle — IKEA SA Furniture (2,962 items)
|
||||||
|
|
||||||
|
- **Records**: 2,962 items
|
||||||
|
- **Fields**: name, category, price, width (cm), height (cm), depth (cm), designer, description
|
||||||
|
- **Format**: CSV
|
||||||
|
- **License**: Kaggle dataset terms
|
||||||
|
- **Date**: April 2020 (somewhat dated)
|
||||||
|
- **Dimensions**: Yes — width, height, depth in centimeters
|
||||||
|
|
||||||
|
**Source**: https://www.kaggle.com/datasets/ahmedkallam/ikea-sa-furniture-web-scraping
|
||||||
|
|
||||||
|
### Good: GitHub — IKEA Dataset (12,600+ items)
|
||||||
|
|
||||||
|
- **Records**: 12,600+ images with furniture dimensions
|
||||||
|
- **Fields**: Name, dimensions (width/length/height cm), material
|
||||||
|
- **Format**: ZIP archives + Python scraping script
|
||||||
|
- **Organization**: By room (bedroom, bathroom, kitchen, living room, etc.)
|
||||||
|
- **Date**: January 2020
|
||||||
|
|
||||||
|
**Source**: https://github.com/valexande/IKEA-Dataset
|
||||||
|
|
||||||
|
### Reference: Dimensions.com — IKEA Collection
|
||||||
|
|
||||||
|
- **Content**: Dozens of IKEA products with precise dimensions + technical drawings
|
||||||
|
- **Formats**: DWG, SVG, JPG (2D) + 3DM, OBJ, SKP (3D models)
|
||||||
|
- **Coverage**: KALLAX, HEMNES, BESTÅ, BEKANT, and many more series
|
||||||
|
- **Dual units**: Both inches and centimeters
|
||||||
|
- **Includes**: Weight capacities
|
||||||
|
- **Note**: Good for manual reference but not easily machine-parseable without scraping
|
||||||
|
|
||||||
|
**Source**: https://www.dimensions.com/collection/ikea-furniture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. IKEA 3D Models
|
||||||
|
|
||||||
|
### Web-Sourced GLB Models (Best Path for Three.js)
|
||||||
|
|
||||||
|
IKEA product pages with a "View in 3D" button serve simplified, Draco-compressed GLB (binary glTF) files. These are:
|
||||||
|
- **Format**: GLB (binary glTF 2.0) — native Three.js format
|
||||||
|
- **Quality**: "Lowest quality versions" — simplified from manufacturing models for web delivery
|
||||||
|
- **Properties**: Scaled in metric, basic materials applied, KHR_Texture_Transform extension
|
||||||
|
- **Compatibility**: Direct import into Three.js via `GLTFLoader` + `DRACOLoader`
|
||||||
|
|
||||||
|
#### Tools to Download IKEA GLB Models
|
||||||
|
|
||||||
|
| Tool | Type | Stars | Status | License |
|
||||||
|
|------|------|-------|--------|---------|
|
||||||
|
| [IKEA 3D Model Download Button](https://github.com/apinanaivot/IKEA-3D-Model-Download-Button) | Tampermonkey userscript | 941 | Active (Jan 2026) | — |
|
||||||
|
| [IKEA 3D Model Batch Downloader](https://github.com/apinanaivot/IKEA-3d-model-batch-downloader) | Python (Selenium+BS4) | — | Active | GPL-3.0 |
|
||||||
|
| [IKEA Browser for Blender](https://extensions.blender.org/add-ons/ikea-browser/) | Blender addon | 95k downloads | Active (v0.4.0, Oct 2025) | GPL-3.0 |
|
||||||
|
| [Chrome Extension](https://chromewebstore.google.com/detail/ikea-3d-models-downloader/kihdhjecfjagoplfnnpdbbnhkjhnlfeg) | Chrome extension | — | Active | — |
|
||||||
|
|
||||||
|
The **batch downloader** workflow:
|
||||||
|
1. Give it an IKEA category URL (e.g., `https://www.ikea.com/fi/fi/cat/chairs-fu002/`)
|
||||||
|
2. Scrapes product links → extracts 3D model URLs → downloads GLB files
|
||||||
|
3. Stores metadata in SQLite database
|
||||||
|
4. Deduplication built-in
|
||||||
|
|
||||||
|
**Note**: Not all IKEA products have 3D models. Only items with the "View in 3D" button are available.
|
||||||
|
|
||||||
|
### Official IKEA 3D Assembly Dataset (GitHub)
|
||||||
|
|
||||||
|
- **Source**: https://github.com/IKEA/IKEA3DAssemblyDataset
|
||||||
|
- **Items**: Only 5 products (LACK, EKET, BEKVÄM, DALFRED)
|
||||||
|
- **Formats**: GLB/glTF + OBJ + PDF assembly instructions
|
||||||
|
- **License**: CC BY-NC-SA 4.0 — **non-commercial only, research purposes**
|
||||||
|
- **Verdict**: Too few items and restrictive license. Not suitable for our use.
|
||||||
|
|
||||||
|
### Sweet Home 3D IKEA Libraries
|
||||||
|
|
||||||
|
- **180 models**: https://3deshop.blogscopia.com/180-ikea-models-for-sweethome3d/
|
||||||
|
- **342 models (bundle)**: https://3deshop.blogscopia.com/ikea-bundle-342-models/
|
||||||
|
- **Format**: SH3F (Sweet Home 3D format) — would need conversion
|
||||||
|
- **Quality**: Community-created, based on real IKEA products
|
||||||
|
- **Note**: Format is proprietary to Sweet Home 3D, would require extraction/conversion
|
||||||
|
|
||||||
|
### Third-Party Model Sources
|
||||||
|
|
||||||
|
- **Sketchfab**: Search "IKEA" — many free community models, various licenses
|
||||||
|
- **TurboSquid**: Free IKEA models available
|
||||||
|
- **Clara.io**: Some IKEA models
|
||||||
|
- **Trimble 3D Warehouse**: IKEA models available (SketchUp format)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Web Scraping Feasibility
|
||||||
|
|
||||||
|
### Existing Scrapers
|
||||||
|
|
||||||
|
| Project | Stack | Notes |
|
||||||
|
|---------|-------|-------|
|
||||||
|
| [IKEA Scraper](https://github.com/Abdelrahman-Hekal/IKEA_Scraper) | Python | Full scraper for ikea.com |
|
||||||
|
| [ikea-webscraper](https://github.com/gamladz/ikea-webscraper) | Selenium | Product info extraction |
|
||||||
|
| [IKEA-project](https://github.com/furkansenn/IKEA-project) | Selenium | Product scraping |
|
||||||
|
| [ikea-scraper](https://github.com/bonzi/ikea-scraper) | — | IKEA data extraction |
|
||||||
|
|
||||||
|
### Technical Challenges
|
||||||
|
|
||||||
|
- IKEA uses heavy client-side rendering (React/Next.js) requiring browser automation (Selenium)
|
||||||
|
- Anti-bot protections: CAPTCHAs, rate limiting, IP blocking
|
||||||
|
- Dimension data is embedded in product pages in varying formats
|
||||||
|
- Product IDs are 8-digit numbers but URL structure varies by locale
|
||||||
|
|
||||||
|
### Legal Considerations
|
||||||
|
|
||||||
|
- IKEA's Terms of Service prohibit automated scraping
|
||||||
|
- IKEA actively protects trademarks and trade dress under US Trademark Act
|
||||||
|
- For personal/non-commercial home planning use, tools like the Blender extension operate in a gray area
|
||||||
|
- The HuggingFace CommerceTXT dataset (CC0) is the safest legal path for product data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Standard IKEA Dimensions Reference
|
||||||
|
|
||||||
|
Manually verified dimensions for the most common IKEA product lines:
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
| Product | Width (cm) | Depth (cm) | Height (cm) |
|
||||||
|
|---------|-----------|-----------|------------|
|
||||||
|
| KALLAX 1×4 | 42 | 39 | 147 |
|
||||||
|
| KALLAX 2×2 | 77 | 39 | 77 |
|
||||||
|
| KALLAX 2×4 | 77 | 39 | 147 |
|
||||||
|
| KALLAX 4×4 | 147 | 39 | 147 |
|
||||||
|
| BILLY bookcase (standard) | 80 | 28 | 202 |
|
||||||
|
| BILLY bookcase (narrow) | 40 | 28 | 202 |
|
||||||
|
| BILLY bookcase (short) | 80 | 28 | 106 |
|
||||||
|
| HEMNES 6-drawer dresser | 108 | 50 | 131 |
|
||||||
|
| HEMNES 3-drawer dresser | 108 | 50 | 96 |
|
||||||
|
| HEMNES bookcase | 90 | 37 | 197 |
|
||||||
|
| BESTÅ TV bench | 120/180 | 42 | 38 |
|
||||||
|
| MALM 6-drawer dresser | 80 | 48 | 123 |
|
||||||
|
| MALM 4-drawer dresser | 80 | 48 | 100 |
|
||||||
|
| PAX wardrobe (standard) | 100/150/200 | 58 | 201/236 |
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
| Product | Width (cm) | Depth (cm) | Height (cm) |
|
||||||
|
|---------|-----------|-----------|------------|
|
||||||
|
| LACK side table | 55 | 55 | 45 |
|
||||||
|
| LACK coffee table | 90 | 55 | 45 |
|
||||||
|
| LACK TV bench | 90 | 26 | 45 |
|
||||||
|
| LISABO desk | 118 | 45 | 74 |
|
||||||
|
| BEKANT desk (rect) | 120/140/160 | 80 | 65-85 |
|
||||||
|
| LINNMON/ALEX desk | 150 | 75 | 73 |
|
||||||
|
| MELLTORP dining table | 125 | 75 | 74 |
|
||||||
|
| EKEDALEN ext. table | 120-180 | 80 | 75 |
|
||||||
|
|
||||||
|
### Seating
|
||||||
|
|
||||||
|
| Product | Width (cm) | Depth (cm) | Height (cm) |
|
||||||
|
|---------|-----------|-----------|------------|
|
||||||
|
| POÄNG armchair | 68 | 82 | 100 |
|
||||||
|
| STRANDMON wing chair | 82 | 96 | 101 |
|
||||||
|
| KLIPPAN 2-seat sofa | 180 | 88 | 66 |
|
||||||
|
| EKTORP 3-seat sofa | 218 | 88 | 88 |
|
||||||
|
| KIVIK 3-seat sofa | 228 | 95 | 83 |
|
||||||
|
| MARKUS office chair | 62 | 60 | 129-140 |
|
||||||
|
|
||||||
|
### Beds
|
||||||
|
|
||||||
|
| Product | Width (cm) | Depth (cm) | Height (cm) |
|
||||||
|
|---------|-----------|-----------|------------|
|
||||||
|
| MALM bed (queen, 160) | 160 | 209 | 92 (headboard) |
|
||||||
|
| MALM bed (king, 180) | 180 | 209 | 92 (headboard) |
|
||||||
|
| MALM bed (single, 90) | 90 | 209 | 92 (headboard) |
|
||||||
|
| HEMNES bed (queen) | 163 | 211 | 66 (headboard 112) |
|
||||||
|
| KURA reversible bed | 99 | 209 | 116 |
|
||||||
|
| SUNDVIK child bed | 80 | 167 | 83 |
|
||||||
|
|
||||||
|
### Kitchen
|
||||||
|
|
||||||
|
| Product | Standard Width (cm) | Depth (cm) | Height (cm) |
|
||||||
|
|---------|-----------|-----------|------------|
|
||||||
|
| METOD base cabinet | 60/80 | 60 | 80 |
|
||||||
|
| METOD wall cabinet | 60/80 | 37 | 60/80/100 |
|
||||||
|
| METOD tall cabinet | 60 | 60 | 200/220 |
|
||||||
|
| KNOXHULT base cabinet | 120/180 | 61 | 85 |
|
||||||
|
| VADHOLMA kitchen island | 126 | 79 | 90 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Recommendations for Our Project
|
||||||
|
|
||||||
|
### Recommended Approach: Tiered Strategy
|
||||||
|
|
||||||
|
#### Tier 1: Curated IKEA Catalog (Immediate)
|
||||||
|
|
||||||
|
Extend `data/furniture-catalog.json` with IKEA-specific items:
|
||||||
|
- Add 30-50 most popular IKEA products with verified dimensions
|
||||||
|
- Use our existing procedural mesh format (box geometry parts)
|
||||||
|
- Add `ikeaId` field (8-digit product number) for future linking
|
||||||
|
- Add `ikeaSeries` field (KALLAX, BILLY, MALM, etc.)
|
||||||
|
- Add `ikeaUrl` field for reference
|
||||||
|
|
||||||
|
**Schema extension**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "ikea-kallax-2x4",
|
||||||
|
"name": "KALLAX Regal 2×4",
|
||||||
|
"ikeaId": "80275887",
|
||||||
|
"ikeaSeries": "KALLAX",
|
||||||
|
"ikeaUrl": "https://www.ikea.com/de/de/p/kallax-regal-weiss-80275887/",
|
||||||
|
"category": "storage",
|
||||||
|
"rooms": ["wohnzimmer", "arbeitszimmer", "kinderzimmer"],
|
||||||
|
"dimensions": { "width": 0.77, "depth": 0.39, "height": 1.47 },
|
||||||
|
"variants": [
|
||||||
|
{ "color": "white", "hex": "#ffffff" },
|
||||||
|
{ "color": "black-brown", "hex": "#3c3028" },
|
||||||
|
{ "color": "white-stained-oak", "hex": "#d4c4a8" }
|
||||||
|
],
|
||||||
|
"mesh": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Effort**: ~2-3 hours to add 30 items manually from IKEA website
|
||||||
|
**Risk**: None — uses verified public dimensions
|
||||||
|
|
||||||
|
#### Tier 2: GLB Model Import (Enhancement)
|
||||||
|
|
||||||
|
Add optional GLB model loading to the renderer:
|
||||||
|
- Use Three.js `GLTFLoader` + `DRACOLoader` to load IKEA GLB files
|
||||||
|
- Users download GLB files themselves (personal use) via browser extension
|
||||||
|
- Store in local `models/` directory
|
||||||
|
- Fall back to procedural mesh if GLB not available
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In catalog entry:
|
||||||
|
{
|
||||||
|
"id": "ikea-kallax-2x4",
|
||||||
|
"model3d": "models/ikea/kallax-2x4.glb", // optional
|
||||||
|
"mesh": { ... } // fallback procedural geometry
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Effort**: ~4-6 hours for GLTFLoader integration
|
||||||
|
**Risk**: Low — GLB loading is standard Three.js functionality
|
||||||
|
|
||||||
|
#### Tier 3: Dataset Import Tool (Future)
|
||||||
|
|
||||||
|
Build a converter that parses the HuggingFace CommerceTXT dataset:
|
||||||
|
- Extract product dimensions from `@SPECS` section
|
||||||
|
- Map categories to our catalog format
|
||||||
|
- Auto-generate procedural meshes from dimensions
|
||||||
|
- Run offline as a data pipeline
|
||||||
|
|
||||||
|
**Effort**: ~1-2 days
|
||||||
|
**Risk**: Medium — dimension parsing from free-text specs varies in reliability
|
||||||
|
|
||||||
|
### What NOT to Do
|
||||||
|
|
||||||
|
1. **Don't scrape IKEA live** — Legal risk, fragile, unnecessary when datasets exist
|
||||||
|
2. **Don't depend on undocumented APIs** — They change without notice
|
||||||
|
3. **Don't bundle IKEA 3D models** — Trademark/IP issues. Let users provide their own
|
||||||
|
4. **Don't use the IKEA 3D Assembly Dataset for the app** — License is NC-SA research only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Key Sources
|
||||||
|
|
||||||
|
| Resource | URL | Best For |
|
||||||
|
|----------|-----|----------|
|
||||||
|
| HuggingFace IKEA US | https://huggingface.co/datasets/tsazan/ikea-us-commercetxt | Dimension data (CC0) |
|
||||||
|
| Kaggle IKEA SA | https://www.kaggle.com/datasets/ahmedkallam/ikea-sa-furniture-web-scraping | Structured CSV with dims |
|
||||||
|
| Dimensions.com IKEA | https://www.dimensions.com/collection/ikea-furniture | Reference dimensions + drawings |
|
||||||
|
| 3D Batch Downloader | https://github.com/apinanaivot/IKEA-3d-model-batch-downloader | Downloading GLB models |
|
||||||
|
| Blender IKEA Browser | https://extensions.blender.org/add-ons/ikea-browser/ | Model inspection/conversion |
|
||||||
|
| IKEA API Blog | https://sbmueller.github.io/posts/ikea/ | Understanding IKEA endpoints |
|
||||||
|
| Sweet Home 3D IKEA | https://3deshop.blogscopia.com/ikea-bundle-342-models/ | Pre-made 3D models (needs conversion) |
|
||||||
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