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.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._renderItems();
|
||||
}
|
||||
@@ -207,6 +220,164 @@ export class CatalogPanel {
|
||||
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 ----
|
||||
|
||||
_bindEvents() {
|
||||
|
||||
@@ -207,6 +207,103 @@
|
||||
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 {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
|
||||
Reference in New Issue
Block a user