cabinet-configurator

Pass

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
MIT2/22/2026
(0)
82
3
3

Install Skill

Skills are third-party code from public GitHub repositories. SkillHub scans for known malicious patterns but cannot guarantee safety. Review the source code before installing.

Install globally (user-level):

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

Install in current project:

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

Suggested path: ~/.claude/skills/cabinet-configurator/

SKILL.md Content

---
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();
```