cabinet-configurator

تایید شده

Cabinet Editor web application development skill. Use when working on Cabinet Editor codebase - a 2D/3D furniture configurator built with Canvas API and Three.js. Covers architecture (Panel and Drawer classes, connections system, virtual panels), coordinate systems, panel/drawer movement logic, cabinet dimension changes (width/height/depth/base), 3D rendering with rank-based depth, ribs system, and drawer box calculations. Essential for debugging, adding features, or understanding how panels/drawers interact with cabinet sizing.

@majiayu000
MIT۱۴۰۴/۱۲/۳
(0)
۸۲
۳
۴

نصب مهارت

مهارت‌ها کدهای شخص ثالث از مخازن عمومی GitHub هستند. SkillHub الگوهای مخرب شناخته‌شده را اسکن می‌کند اما نمی‌تواند امنیت را تضمین کند. قبل از نصب، کد منبع را بررسی کنید.

نصب سراسری (سطح کاربر):

npx skillhub install majiayu000/claude-skill-registry/cabinet-configurator

نصب در پروژه فعلی:

npx skillhub install majiayu000/claude-skill-registry/cabinet-configurator --project

مسیر پیشنهادی: ~/.claude/skills/cabinet-configurator/

محتوای SKILL.md

---
name: cabinet-configurator
description: Cabinet Editor web application development skill. Use when working on Cabinet Editor codebase - a 2D/3D furniture configurator built with Canvas API and Three.js. Covers architecture (Panel and Drawer classes, connections system, virtual panels), coordinate systems, panel/drawer movement logic, cabinet dimension changes (width/height/depth/base), 3D rendering with rank-based depth, ribs system, and drawer box calculations. Essential for debugging, adding features, or understanding how panels/drawers interact with cabinet sizing.
---

# Cabinet Configurator Skill

Development guide for Cabinet Editor - a web-based furniture configurator with 2D Canvas editing and 3D Three.js visualization.

## Project Setup

**Location:** `C:\Users\admin\Desktop\OMS\cabinet-editor`

**CRITICAL: Always use filesystem MCP tools for file operations**
- Use `filesystem:read_text_file`, `filesystem:edit_file`, `filesystem:write_file`
- NEVER use bash/powershell commands like `cat`, `sed`, `type`, etc. for reading/editing files
- Reason: Prevents encoding issues (UTF-8 vs CP1251/CP866), handles line endings correctly, provides proper error handling
- The filesystem tools have access to the project directory and handle Windows paths correctly

## Architecture Overview

### Core Classes

**App** (`js/App.js`)
- Main application controller
- Manages `this.cabinet` (width, height, depth, base)
- Stores `this.panels` Map of Panel instances
- Handles interaction (dragging, mode selection)
- Manages history (undo/redo)

**Panel** (`js/Panel.js`)
- Represents shelves and dividers
- Properties:
  - `type`: 'shelf' | 'divider'
  - `id`: unique identifier
  - `position`: { x?, y? } - center point (one coordinate per panel)
  - `bounds`: { startX, endX } for shelves, { startY, endY } for dividers
  - `connections`: { left?, right?, top?, bottom? } - references to adjacent Panel objects
  - `ribs`: array of rib objects (shelves only)
  - `isHorizontal`: true for shelves, false for dividers

**Drawer** (`js/Drawer.js`)
- Represents pull-out drawer boxes
- Properties:
  - `type`: 'drawer'
  - `id`: unique identifier
  - `connections`: { bottomShelf, topShelf, leftDivider, rightDivider } - Panel/virtual panel references
  - `volume`: calculated 3D bounding box { x: {start, end}, y: {start, end}, z: {start, end} }
  - `boxLength`: selected standard box size (270, 350, 450, 550mm)
  - `parts`: calculated drawer components (front, leftSide, rightSide, back, bottom)

**Viewer3D** (`js/Viewer3D.js`)
- Three.js 3D visualization
- Manages scene, camera, renderer, controls
- Builds cabinet structure and panel meshes

### Panel System

**Shelves (horizontal panels)**
- `position.y`: Y coordinate of shelf center
- `bounds.startX`, `bounds.endX`: left and right edges
- `connections.left`/`right`: dividers that bound the shelf horizontally
- `connections.top`/`bottom`: used by dividers that terminate at this shelf

**Dividers (vertical panels)**
- `position.x`: X coordinate of divider center  
- `bounds.startY`, `bounds.endY`: bottom and top edges
- `connections.bottom`/`top`: shelves that bound the divider vertically
- `connections.left`/`right`: used by shelves that terminate at this divider

**Virtual Panels**
Drawers can connect to "virtual panels" representing cabinet boundaries:
- `type: 'left'` or `'right'`: virtual side panels at cabinet edges
- `type: 'bottom'` or `'top'`: virtual horizontal panels at cabinet base/roof
- Virtual panels don't exist in `app.panels` Map but behave like real panels for drawer connections
- Enable drawers to span full width/height of cabinet

## Coordinate System

**Canvas coordinates (2D):**
- Origin (0,0) at bottom-left
- X increases right (0 to cabinet.width)
- Y increases up (0 to cabinet.height)
- Cabinet structure:
  - Left side: x = CONFIG.DSP/2 (8mm)
  - Right side: x = cabinet.width - CONFIG.DSP/2
  - Bottom (plinth top): y = cabinet.base
  - Top (roof bottom): y = cabinet.height - CONFIG.DSP

**3D coordinates:**
- X/Y match 2D canvas
- Z is depth: 0 (back) to cabinet.depth (front)
- Panels recede based on `rank`: depth = (cabinet.depth - 3) - rank

**Key measurements:**
- `CONFIG.DSP`: 16mm (panel thickness for ДСП)
- `CONFIG.HDF`: 3mm (back panel thickness for ХДФ)
- `CONFIG.MIN_GAP`: 150mm (minimum spacing between panels)
- `CONFIG.MIN_SIZE`: 200mm (minimum panel dimension)
- `cabinet.base`: plinth height (min 60mm)
- `cabinet.width`: total cabinet width (400-3000mm)
- `cabinet.height`: total cabinet height
- `cabinet.depth`: total cabinet depth (300-800mm, adjustable in 1mm increments)

## Movement Logic

### Movable Cabinet Boundaries

Cabinet boundaries (sides, bottom, roof) can be moved in "move" mode by detecting them in `findPanelAt()`:

**Left/Right Sides** (`moveSide` method):
- Changes `cabinet.width`
- Left side: shifts all panels right when expanding
- Right side: only changes width
- Limits: MIN_CABINET_WIDTH (400mm) to MAX_CABINET_WIDTH (3000mm)
- Cannot pass through dividers (MIN_GAP spacing)

**Bottom** (`moveHorizontalSide` with `isBottom`):
- Changes `cabinet.base` (plinth height, min 60mm)
- Dividers WITHOUT `connections.bottom`: `bounds.startY = cabinet.base` (stretch/shrink)
- Dividers WITH `connections.bottom`: no change (stay with shelf)
- Shelves: remain at absolute Y coordinates (do not move)
- Updates `position.y` for affected dividers

**Roof** (`moveHorizontalSide` with `!isBottom`):
- Changes `cabinet.height` (total height)
- Cannot pass through shelves (stops at highest shelf + MIN_GAP)
- Dividers WITHOUT `connections.top`: stretch `bounds.endY` to new height
- Updates `position.y` for stretched dividers

### Panel Movement

**Shelves:**
- Move vertically (change `position.y`)
- `bounds.startX`/`endX` determined by `connections.left`/`right`
- Connected dividers update their `bounds.startY` or `bounds.endY`

**Dividers:**
- Move horizontally (change `position.x`)
- `bounds.startY`/`endY` determined by `connections.bottom`/`top`
- Connected shelves update their `bounds.startX` or `bounds.endX`

**Important:** After changing `bounds`, always update `position`:
```javascript
// For dividers
panel.position.y = (panel.bounds.startY + panel.bounds.endY) / 2;

// For shelves  
panel.position.x = (panel.bounds.startX + panel.bounds.endX) / 2;
```

### Update Pattern

When moving panels that affect others:
1. Update the moved panel's position/bounds
2. Call `updateConnectedPanels(movedPanel)` to update connected panels
3. Update ribs: `panel.updateRibs(this.panels, this.cabinet.width)` for affected shelves
4. Call `updateMesh(this, panel)` for 3D updates
5. Call `render2D(this)` and `renderAll3D(this)` to redraw

When moving cabinet boundaries:
1. Update `cabinet.width`/`height`/`base`
2. Update affected panel bounds and positions
3. Call `updateCalc()` to recalculate derived dimensions
4. Update ribs for all shelves
5. Call `updateCanvas()` if canvas scaling changed
6. Call `viewer3D.rebuildCabinet()` to rebuild 3D structure
7. Call `render2D(this)` and `renderAll3D(this)`

## 3D Rendering

### Rank System

Panels have a `rank` that determines their Z-depth (recess from front):

```javascript
calculatePanelRank(panel) {
  // Fixed ranks
  if (panel.type === 'back') return -1;  // ХДФ back
  if (panel.type === 'left' || panel.type === 'right') return 0;  // Sides
  if (panel.type === 'bottom' || panel.type === 'top') return 1;  // Floor/ceiling
  
  // Dynamic rank = max(parent ranks) + 1
  let maxRank = 0;
  for (let parent of Object.values(panel.connections)) {
    if (parent?.type) {
      maxRank = Math.max(maxRank, this.calculatePanelRank(parent));
    }
  }
  return maxRank + 1;
}
```

**3D depth calculation:**
```javascript
const rank = app.calculatePanelRank(panel);
const depth = (cabinet.depth - 3) - rank;  // Recess from front
```

### Cabinet Structure (3D)

Built by `Viewer3D.rebuildCabinet()`:
- Left/right sides: 16mm thick, full height, depth - 3mm
- Bottom/top: between sides, 16mm thick, depth - 4mm
- Back (ХДФ): 3mm thick, behind everything
- Front/back plinth: 16mm thick, below base height

## Drawer System

Drawers are pull-out boxes defined by 4 boundary panels (real or virtual).

### Drawer Structure

**Connections:**
- `bottomShelf`: lower boundary (shelf or virtual 'bottom')
- `topShelf`: upper boundary (shelf or virtual 'top')
- `leftDivider`: left boundary (divider or virtual 'left')
- `rightDivider`: right boundary (divider or virtual 'right')

**Volume Calculation** (`calculateVolume`):
```javascript
// Find minimum depth among connected panels (based on rank)
const depths = [
  (cabinet.depth - CONFIG.HDF) - calculatePanelRank(bottomShelf),
  (cabinet.depth - CONFIG.HDF) - calculatePanelRank(topShelf),
  (cabinet.depth - CONFIG.HDF) - calculatePanelRank(leftDivider),
  (cabinet.depth - CONFIG.HDF) - calculatePanelRank(rightDivider)
];
const minDepth = Math.min(...depths);

// For virtual panels, use cabinet dimensions directly
const leftEdge = leftDivider.type === 'left' 
  ? CONFIG.DSP 
  : leftDivider.position.x + CONFIG.DSP;

const volume = {
  x: { start: leftEdge, end: rightEdge },
  y: { start: bottomEdge, end: topEdge },
  z: { start: CONFIG.DSP, end: minDepth - 2 }  // 16mm offset from back for rib + 2mm clearance from panel
};
```

**Box Length Selection:**
- Standard sizes: 270, 350, 450, 550mm (from `CONFIG.DRAWER.SIZES`)
- Selected based on available depth: `volDepth + CONFIG.DSP`
- If no suitable size, drawer creation fails

**Parts Calculation** (`calculateParts`):
Drawer consists of 5 components with precise dimensions and Z-coordinates:

1. **Front panel** (facade)
   - Width: `volWidth - 4mm` (2mm gaps on sides)
   - Height: `volHeight - 30mm` (26mm gap on top, 4mm on bottom)
   - Depth: 16mm (CONFIG.DSP)
   - Position Z: `frontZ = vol.z.end`

2. **Left/Right sides**
   - Height: `volHeight - 56mm`
   - Depth: `boxLength - 26mm`
   - Thickness: 16mm
   - Z range: `[sidesZ1, sidesZ2]` where `sidesZ2 = frontZ - 16`

3. **Back panel**
   - Width: `volWidth - 42mm`
   - Height: `volHeight - 68mm`
   - Positioned at: `backZ = sidesZ1 + CONFIG.DRAWER.BACK_OFFSET`

4. **Bottom panel**
   - Width: `volWidth - 42mm`
   - Depth: `boxLength - 44mm`
   - Z range: `[bottomZ1, bottomZ2]` starting at `sidesZ1 + 16 + BOTTOM_OFFSET`

**Drawer Config Constants** (from `CONFIG.DRAWER`):
```javascript
DRAWER: {
  SIZES: [270, 320, 370, 420, 470, 520, 570, 620],  // Стандартные длины коробов (8 размеров)
  MIN_WIDTH: 150,    // Минимальная ширина ящика (как MIN_GAP)
  MAX_WIDTH: 1200,   // Максимальная ширина ящика
  MIN_HEIGHT: 80,    // Минимальная высота ящика
  MAX_HEIGHT: 400,   // Максимальная высота ящика
  GAP_FRONT: 2,      // Зазоры фасада
  GAP_TOP: 28,
  GAP_BOTTOM: 2,
  SIDE_OFFSET_X: 5,  // Отступы боковин
  SIDE_OFFSET_Y: 17,
  INNER_OFFSET: 21,  // Отступ задней стенки/дна
  BACK_OFFSET: 2,
  BOTTOM_OFFSET: 2
}
```

### Drawer Lifecycle

**Adding a drawer** (`addDrawer`):
1. Find 4 boundary panels at click position (or use virtual panels)
2. Create Drawer instance with connections
3. Call `drawer.calculateParts(app)` - returns false if volume too small
4. Add to `app.drawers` Map
5. Call `updateDrawerMeshes(app, drawer)` for 3D
6. Save history and render

**Updating drawers:**
Drawers must be recalculated when connected panels move or cabinet dimensions change:
```javascript
// After panel movement
for (let drawer of app.drawers.values()) {
  if (drawer.connections includes movedPanel) {
    drawer.updateParts(app);
    updateDrawerMeshes(app, drawer);
  }
}

// After cabinet dimension change (width/height/depth/base)
for (let drawer of app.drawers.values()) {
  drawer.updateParts(app);
  updateDrawerMeshes(app, drawer);
}
```

**Deleting drawers:**
- Delete when any connected panel is deleted
- Can be deleted individually in delete mode
- Use `removeDrawerMeshes(app, drawer)` before removing from Map

**Mirroring:**
When mirroring cabinet content:
```javascript
// Swap left/right divider connections
const tempLeft = drawer.connections.leftDivider;
drawer.connections.leftDivider = drawer.connections.rightDivider;
drawer.connections.rightDivider = tempLeft;

// Update virtual panel types if present
if (leftDivider?.type === 'right') leftDivider.type = 'left';
if (rightDivider?.type === 'left') rightDivider.type = 'right';

drawer.updateParts(app);
```

### 3D Rendering

**Drawer meshes** (from `render3D.js`):
- Each drawer creates 5 separate meshes (front, sides, back, bottom)
- Stored in `app.mesh3D` with keys: `${drawer.id}-front`, `${drawer.id}-leftSide`, etc.
- Material: orange color (`0xff9800`) to distinguish from panels
- Box geometry with precise dimensions from `drawer.parts`

**Update pattern:**
```javascript
import { updateDrawerMeshes, removeDrawerMeshes } from './modules/render3D.js';

// After drawer modification
updateDrawerMeshes(app, drawer);  // Removes old meshes, creates new ones

// Before deletion
removeDrawerMeshes(app, drawer);  // Cleans up all 5 meshes
```

**Global export** (for HTML inline scripts):
```javascript
// In main.js
import { updateDrawerMeshes } from './modules/render3D.js';
window.updateDrawerMeshes = updateDrawerMeshes;
```

## Ribs System

Ribs (ребра жесткости) are vertical supports under shelves, preventing sagging.

**When added:**
- Shelves longer than threshold need ribs
- Thresholds: 800mm (no ribs), 1000mm (1 rib), 1200mm (2 ribs)

**Calculation** (`Panel.updateRibs()`):
```javascript
updateRibs(allPanels, cabinetWidth) {
  const shelfWidth = this.bounds.endX - this.bounds.startX;
  
  // Find dividers that cross this shelf
  const crossingDividers = Array.from(allPanels.values())
    .filter(p => !p.isHorizontal && 
                 p.bounds.startY <= this.position.y &&
                 p.bounds.endY >= this.position.y &&
                 p.position.x > this.bounds.startX &&
                 p.position.x < this.bounds.endX)
    .map(p => p.position.x)
    .sort((a, b) => a - b);
  
  // Calculate segments between dividers
  const points = [
    this.bounds.startX,
    ...crossingDividers,
    this.bounds.endX
  ];
  
  // Add ribs to segments that need them
  this.ribs = [];
  for (let i = 0; i < points.length - 1; i++) {
    const segmentStart = points[i] + (crossingDividers.includes(points[i]) ? CONFIG.DSP : 0);
    const segmentEnd = points[i + 1];
    const segmentWidth = segmentEnd - segmentStart;
    
    const ribsNeeded = calculateRibsForSegment(segmentWidth);
    if (ribsNeeded > 0) {
      // Distribute ribs evenly in segment
      for (let j = 0; j < ribsNeeded; j++) {
        const ribX = segmentStart + (segmentWidth / (ribsNeeded + 1)) * (j + 1);
        this.ribs.push({ startX: ribX, endX: ribX + CONFIG.DSP });
      }
    }
  }
}
```

**3D rendering:**
- Ribs are 16mm wide, 100mm tall
- Positioned below shelf: `y = shelf.position.y - 100`
- Same depth as shelf (based on rank)

## Common Patterns

For detailed code examples, see [references/examples.md](references/examples.md).

### Adding a new panel
1. Create Panel instance with type, id, position, bounds, connections
2. Add to `app.panels` Map
3. Call `panel.updateRibs()` if shelf
4. Call `app.saveHistory()`
5. Call `render2D(app)` and `renderAll3D(app)`

### Deleting a panel
1. Find dependent panels via `connections`
2. Call `removeMesh(app, panel)` for each
3. Remove from `app.panels`
4. Recalculate bounds for affected panels
5. Update ribs for remaining shelves
6. Call `app.saveHistory()`
7. Call `render2D(app)` and `renderAll3D(app)`

### Changing cabinet dimensions
1. Update `app.cabinet.width`/`height`/`depth`/`base`
2. Update panel bounds that depend on cabinet size
3. Call `app.updateCalc()`
4. Recalculate ALL drawers (they depend on cabinet dimensions via virtual panels)
5. If width/height changed: `app.updateCanvas()`
6. Rebuild 3D: `app.viewer3D.rebuildCabinet()`
7. Update all panel meshes or call `renderAll3D(app)`

### Adding a drawer
1. Click in drawer mode to select area
2. Find 4 boundary panels (use virtual panels for cabinet edges)
3. Create Drawer instance: `new Drawer(id, connections)`
4. Calculate parts: `drawer.calculateParts(app)` - check return value
5. Add to `app.drawers` Map
6. Call `updateDrawerMeshes(app, drawer)` for 3D
7. Save history and render

## File Structure

```
cabinet-editor/
├── index.html           - UI structure, bottom sheets, event handlers
├── css/
│   └── main.css         - Styling
├── js/
│   ├── main.js          - Entry point, app initialization, global exports
│   ├── App.js           - Main application class
│   ├── Panel.js         - Panel class
│   ├── Drawer.js        - Drawer class
│   ├── Viewer3D.js      - 3D viewer class
│   ├── config.js        - Constants and configuration
│   ├── exportJSON.js    - Export functionality
│   └── modules/
│       ├── render2D.js       - Canvas 2D rendering
│       ├── render3D.js       - Three.js mesh management
│       ├── historyLogging.js - History UI
│       └── historyDebug.js   - History debugging tools
```

## Debugging Tips

**Check panel state:**
```javascript
// In browser console
window.app.panels.forEach(p => console.log(p.id, p.position, p.bounds, p.connections))
```

**Check drawer state:**
```javascript
// Inspect drawers
window.app.drawers.forEach(d => console.log(d.id, d.volume, d.boxLength, d.parts));

// Test drawer in specific area
const testDrawer = new Drawer('test', {
  bottomShelf: window.app.panels.get('shelf-0'),
  topShelf: window.app.panels.get('shelf-1'),
  leftDivider: { type: 'left', position: { x: 8 } },
  rightDivider: window.app.panels.get('divider-0')
});
testDrawer.calculateParts(window.app);
console.log(testDrawer);
```

**Verify connections:**
```javascript
// Check if connections are Panel objects, not IDs
const panel = window.app.panels.get('shelf-0');
console.log(panel.connections.left?.type); // Should be 'divider', not undefined
```

**Test cabinet dimensions:**
```javascript
console.log(window.app.cabinet); // { width, height, depth, base }
console.log(window.app.calc);    // { innerWidth, innerDepth, workHeight }
```

**Force 3D rebuild:**
```javascript
window.app.viewer3D.rebuildCabinet();
window.app.renderAll3D();
```