Claude-skill-registry 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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cabinet-configurator" ~/.claude/skills/majiayu000-claude-skill-registry-cabinet-configurator && rm -rf "$T"
manifest: skills/data/cabinet-configurator/SKILL.md
source content

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
:

// 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):

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:

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
):

// 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
):

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:

// 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:

// 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:

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):

// 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()
):

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.

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:

// In browser console
window.app.panels.forEach(p => console.log(p.id, p.position, p.bounds, p.connections))

Check drawer state:

// 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:

// 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:

console.log(window.app.cabinet); // { width, height, depth, base }
console.log(window.app.calc);    // { innerWidth, innerDepth, workHeight }

Force 3D rebuild:

window.app.viewer3D.rebuildCabinet();
window.app.renderAll3D();