xiaohongshu
xiaohongshu
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, c...
Full skill instructions
Development guide for Cabinet Editor - a web-based furniture configurator with 2D Canvas editing and 3D Three.js visualization.
Location: C:\Users\admin\Desktop\OMS\cabinet-editor
CRITICAL: Always use filesystem MCP tools for file operations
filesystem:read_text_file, filesystem:edit_file, filesystem:write_filecat, sed, type, etc. for reading/editing filesApp (js/App.js)
this.cabinet (width, height, depth, base)this.panels Map of Panel instancesPanel (js/Panel.js)
type: 'shelf' | 'divider'id: unique identifierposition: { x?, y? } - center point (one coordinate per panel)bounds: { startX, endX } for shelves, { startY, endY } for dividersconnections: { left?, right?, top?, bottom? } - references to adjacent Panel objectsribs: array of rib objects (shelves only)isHorizontal: true for shelves, false for dividersDrawer (js/Drawer.js)
type: 'drawer'id: unique identifierconnections: { bottomShelf, topShelf, leftDivider, rightDivider } - Panel/virtual panel referencesvolume: 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)
Shelves (horizontal panels)
position.y: Y coordinate of shelf centerbounds.startX, bounds.endX: left and right edgesconnections.left/right: dividers that bound the shelf horizontallyconnections.top/bottom: used by dividers that terminate at this shelfDividers (vertical panels)
position.x: X coordinate of divider centerbounds.startY, bounds.endY: bottom and top edgesconnections.bottom/top: shelves that bound the divider verticallyconnections.left/right: used by shelves that terminate at this dividerVirtual Panels Drawers can connect to "virtual panels" representing cabinet boundaries:
type: 'left' or 'right': virtual side panels at cabinet edgestype: 'bottom' or 'top': virtual horizontal panels at cabinet base/roofapp.panels Map but behave like real panels for drawer connectionsCanvas coordinates (2D):
3D coordinates:
rank: depth = (cabinet.depth - 3) - rankKey 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 heightcabinet.depth: total cabinet depth (300-800mm, adjustable in 1mm increments)Cabinet boundaries (sides, bottom, roof) can be moved in "move" mode by detecting them in findPanelAt():
Left/Right Sides (moveSide method):
cabinet.widthBottom (moveHorizontalSide with isBottom):
cabinet.base (plinth height, min 60mm)connections.bottom: bounds.startY = cabinet.base (stretch/shrink)connections.bottom: no change (stay with shelf)position.y for affected dividersRoof (moveHorizontalSide with !isBottom):
cabinet.height (total height)connections.top: stretch bounds.endY to new heightposition.y for stretched dividersShelves:
position.y)bounds.startX/endX determined by connections.left/rightbounds.startY or bounds.endYDividers:
position.x)bounds.startY/endY determined by connections.bottom/topbounds.startX or bounds.endXImportant: 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;
When moving panels that affect others:
updateConnectedPanels(movedPanel) to update connected panelspanel.updateRibs(this.panels, this.cabinet.width) for affected shelvesupdateMesh(this, panel) for 3D updatesrender2D(this) and renderAll3D(this) to redrawWhen moving cabinet boundaries:
cabinet.width/height/baseupdateCalc() to recalculate derived dimensionsupdateCanvas() if canvas scaling changedviewer3D.rebuildCabinet() to rebuild 3D structurerender2D(this) and renderAll3D(this)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
Built by Viewer3D.rebuildCabinet():
Drawers are pull-out boxes defined by 4 boundary panels (real or virtual).
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:
CONFIG.DRAWER.SIZES)volDepth + CONFIG.DSPParts Calculation (calculateParts):
Drawer consists of 5 components with precise dimensions and Z-coordinates:
Front panel (facade)
volWidth - 4mm (2mm gaps on sides)volHeight - 30mm (26mm gap on top, 4mm on bottom)frontZ = vol.z.endLeft/Right sides
volHeight - 56mmboxLength - 26mm[sidesZ1, sidesZ2] where sidesZ2 = frontZ - 16Back panel
volWidth - 42mmvolHeight - 68mmbackZ = sidesZ1 + CONFIG.DRAWER.BACK_OFFSETBottom panel
volWidth - 42mmboxLength - 44mm[bottomZ1, bottomZ2] starting at sidesZ1 + 16 + BOTTOM_OFFSETDrawer 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
}
Adding a drawer (addDrawer):
drawer.calculateParts(app) - returns false if volume too smallapp.drawers MapupdateDrawerMeshes(app, drawer) for 3DUpdating 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:
removeDrawerMeshes(app, drawer) before removing from MapMirroring: 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);
Drawer meshes (from render3D.js):
app.mesh3D with keys: ${drawer.id}-front, ${drawer.id}-leftSide, etc.0xff9800) to distinguish from panelsdrawer.partsUpdate 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 (ребра жесткости) are vertical supports under shelves, preventing sagging.
When added:
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:
y = shelf.position.y - 100For detailed code examples, see references/examples.md.
app.panels Mappanel.updateRibs() if shelfapp.saveHistory()render2D(app) and renderAll3D(app)connectionsremoveMesh(app, panel) for eachapp.panelsapp.saveHistory()render2D(app) and renderAll3D(app)app.cabinet.width/height/depth/baseapp.updateCalc()app.updateCanvas()app.viewer3D.rebuildCabinet()renderAll3D(app)new Drawer(id, connections)drawer.calculateParts(app) - check return valueapp.drawers MapupdateDrawerMeshes(app, drawer) for 3Dcabinet-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
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();
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience