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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/cabinet-configurator/SKILL.mdCabinet 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_filefilesystem:write_file - NEVER use bash/powershell commands like
,cat
,sed
, etc. for reading/editing filestype - 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
(width, height, depth, base)this.cabinet - Stores
Map of Panel instancesthis.panels - Handles interaction (dragging, mode selection)
- Manages history (undo/redo)
Panel (
js/Panel.js)
- Represents shelves and dividers
- Properties:
: 'shelf' | 'divider'type
: unique identifierid
: { x?, y? } - center point (one coordinate per panel)position
: { startX, endX } for shelves, { startY, endY } for dividersbounds
: { left?, right?, top?, bottom? } - references to adjacent Panel objectsconnections
: array of rib objects (shelves only)ribs
: true for shelves, false for dividersisHorizontal
Drawer (
js/Drawer.js)
- Represents pull-out drawer boxes
- Properties:
: 'drawer'type
: unique identifierid
: { bottomShelf, topShelf, leftDivider, rightDivider } - Panel/virtual panel referencesconnections
: calculated 3D bounding box { x: {start, end}, y: {start, end}, z: {start, end} }volume
: selected standard box size (270, 350, 450, 550mm)boxLength
: calculated drawer components (front, leftSide, rightSide, back, bottom)parts
Viewer3D (
js/Viewer3D.js)
- Three.js 3D visualization
- Manages scene, camera, renderer, controls
- Builds cabinet structure and panel meshes
Panel System
Shelves (horizontal panels)
: Y coordinate of shelf centerposition.y
,bounds.startX
: left and right edgesbounds.endX
/connections.left
: dividers that bound the shelf horizontallyright
/connections.top
: used by dividers that terminate at this shelfbottom
Dividers (vertical panels)
: X coordinate of divider centerposition.x
,bounds.startY
: bottom and top edgesbounds.endY
/connections.bottom
: shelves that bound the divider verticallytop
/connections.left
: used by shelves that terminate at this dividerright
Virtual Panels Drawers can connect to "virtual panels" representing cabinet boundaries:
ortype: 'left'
: virtual side panels at cabinet edges'right'
ortype: 'bottom'
: virtual horizontal panels at cabinet base/roof'top'- Virtual panels don't exist in
Map but behave like real panels for drawer connectionsapp.panels - 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
: depth = (cabinet.depth - 3) - rankrank
Key measurements:
: 16mm (panel thickness for ДСП)CONFIG.DSP
: 3mm (back panel thickness for ХДФ)CONFIG.HDF
: 150mm (minimum spacing between panels)CONFIG.MIN_GAP
: 200mm (minimum panel dimension)CONFIG.MIN_SIZE
: plinth height (min 60mm)cabinet.base
: total cabinet width (400-3000mm)cabinet.width
: total cabinet heightcabinet.height
: total cabinet depth (300-800mm, adjustable in 1mm increments)cabinet.depth
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
(plinth height, min 60mm)cabinet.base - Dividers WITHOUT
:connections.bottom
(stretch/shrink)bounds.startY = cabinet.base - Dividers WITH
: no change (stay with shelf)connections.bottom - Shelves: remain at absolute Y coordinates (do not move)
- Updates
for affected dividersposition.y
Roof (
moveHorizontalSide with !isBottom):
- Changes
(total height)cabinet.height - Cannot pass through shelves (stops at highest shelf + MIN_GAP)
- Dividers WITHOUT
: stretchconnections.top
to new heightbounds.endY - Updates
for stretched dividersposition.y
Panel Movement
Shelves:
- Move vertically (change
)position.y
/bounds.startX
determined byendX
/connections.leftright- Connected dividers update their
orbounds.startYbounds.endY
Dividers:
- Move horizontally (change
)position.x
/bounds.startY
determined byendY
/connections.bottomtop- Connected shelves update their
orbounds.startXbounds.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:
- Update the moved panel's position/bounds
- Call
to update connected panelsupdateConnectedPanels(movedPanel) - Update ribs:
for affected shelvespanel.updateRibs(this.panels, this.cabinet.width) - Call
for 3D updatesupdateMesh(this, panel) - Call
andrender2D(this)
to redrawrenderAll3D(this)
When moving cabinet boundaries:
- Update
/cabinet.width
/heightbase - Update affected panel bounds and positions
- Call
to recalculate derived dimensionsupdateCalc() - Update ribs for all shelves
- Call
if canvas scaling changedupdateCanvas() - Call
to rebuild 3D structureviewer3D.rebuildCabinet() - Call
andrender2D(this)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:
: lower boundary (shelf or virtual 'bottom')bottomShelf
: upper boundary (shelf or virtual 'top')topShelf
: left boundary (divider or virtual 'left')leftDivider
: right boundary (divider or virtual 'right')rightDivider
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:
-
Front panel (facade)
- Width:
(2mm gaps on sides)volWidth - 4mm - Height:
(26mm gap on top, 4mm on bottom)volHeight - 30mm - Depth: 16mm (CONFIG.DSP)
- Position Z:
frontZ = vol.z.end
- Width:
-
Left/Right sides
- Height:
volHeight - 56mm - Depth:
boxLength - 26mm - Thickness: 16mm
- Z range:
where[sidesZ1, sidesZ2]sidesZ2 = frontZ - 16
- Height:
-
Back panel
- Width:
volWidth - 42mm - Height:
volHeight - 68mm - Positioned at:
backZ = sidesZ1 + CONFIG.DRAWER.BACK_OFFSET
- Width:
-
Bottom panel
- Width:
volWidth - 42mm - Depth:
boxLength - 44mm - Z range:
starting at[bottomZ1, bottomZ2]sidesZ1 + 16 + BOTTOM_OFFSET
- Width:
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):
- Find 4 boundary panels at click position (or use virtual panels)
- Create Drawer instance with connections
- Call
- returns false if volume too smalldrawer.calculateParts(app) - Add to
Mapapp.drawers - Call
for 3DupdateDrawerMeshes(app, drawer) - 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
before removing from MapremoveDrawerMeshes(app, drawer)
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
with keys:app.mesh3D
,${drawer.id}-front
, etc.${drawer.id}-leftSide - Material: orange color (
) to distinguish from panels0xff9800 - 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
- Create Panel instance with type, id, position, bounds, connections
- Add to
Mapapp.panels - Call
if shelfpanel.updateRibs() - Call
app.saveHistory() - Call
andrender2D(app)renderAll3D(app)
Deleting a panel
- Find dependent panels via
connections - Call
for eachremoveMesh(app, panel) - Remove from
app.panels - Recalculate bounds for affected panels
- Update ribs for remaining shelves
- Call
app.saveHistory() - Call
andrender2D(app)renderAll3D(app)
Changing cabinet dimensions
- Update
/app.cabinet.width
/height
/depthbase - Update panel bounds that depend on cabinet size
- Call
app.updateCalc() - Recalculate ALL drawers (they depend on cabinet dimensions via virtual panels)
- If width/height changed:
app.updateCanvas() - Rebuild 3D:
app.viewer3D.rebuildCabinet() - Update all panel meshes or call
renderAll3D(app)
Adding a drawer
- Click in drawer mode to select area
- Find 4 boundary panels (use virtual panels for cabinet edges)
- Create Drawer instance:
new Drawer(id, connections) - Calculate parts:
- check return valuedrawer.calculateParts(app) - Add to
Mapapp.drawers - Call
for 3DupdateDrawerMeshes(app, drawer) - 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();