v2.1
Ship configurator updates with catalog DB included for server deploy. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -8,7 +8,10 @@
|
||||
import {
|
||||
PANEL, GROOVE, SYSTEM32, SYSTEM32_SEPARATORS, HINGE, CONNECTORS,
|
||||
getHingeCountForDoor, getHingeCupPositions, getDoorHingeSide, HANDLE,
|
||||
EDGE_BANDING, REINFORCEMENT, REINFORCEMENT_MACHINING, PLINTH, SHELF_MACHINING
|
||||
EDGE_BANDING, REINFORCEMENT, REINFORCEMENT_MACHINING, PLINTH, SHELF_MACHINING,
|
||||
resolveHorizontalSpan, validateHorizontalSpans, getSeparatorVerticalSegments,
|
||||
hasSideHabillage, hasSidePlinthReturn, getAxiloPlinthReturnLengthMm,
|
||||
getAxiloPlinthFrontSideDeductionMm
|
||||
} from '../../shared/manufacturing.js';
|
||||
|
||||
// Constants for drawing
|
||||
@@ -36,6 +39,8 @@ const SHARED_STYLES = `
|
||||
.renfort { fill: #c9a96e; stroke: #333; stroke-width: 0.5; opacity: 0.7; }
|
||||
.detail-label { font-family: Arial, sans-serif; font-size: 7px; fill: #666; }
|
||||
.fh-groove { fill: none; stroke: #0d9488; stroke-width: 1; stroke-dasharray: 3, 2; }
|
||||
.horizontal-span { fill: none; stroke: #b45309; stroke-width: 2; }
|
||||
.horizontal-span-label { font-family: Arial, sans-serif; font-size: 8px; font-weight: bold; fill: #92400e; }
|
||||
`;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
@@ -140,6 +145,19 @@ function generateDimensionLine(x, y, length, label, direction = 'horizontal') {
|
||||
export function generateAssemblyPlan(config, modules = []) {
|
||||
const { widthMm, heightMm, plinthHeightMm, doorType } = config;
|
||||
const bodyHeight = heightMm - plinthHeightMm;
|
||||
const planModules = modules.length > 0 ? modules : (Array.isArray(config.modules) ? config.modules : []);
|
||||
const spanConfig = { ...config, modules: planModules };
|
||||
const spanIssues =
|
||||
Array.isArray(config.horizontalSpans) && config.horizontalSpans.length
|
||||
? validateHorizontalSpans(spanConfig)
|
||||
: [];
|
||||
const invalidSpanIds = new Set(
|
||||
spanIssues.map((issue) => issue.spanId).filter((id) => id != null).map(String)
|
||||
);
|
||||
const resolvedSpans = (Array.isArray(config.horizontalSpans) ? config.horizontalSpans : [])
|
||||
.filter((span) => span?.id != null && !invalidSpanIds.has(String(span.id)))
|
||||
.map((span) => resolveHorizontalSpan(span, spanConfig))
|
||||
.filter(Boolean);
|
||||
const S = DRAWING_SCALE;
|
||||
|
||||
const svgWidth = widthMm * S + MARGIN * 2;
|
||||
@@ -154,19 +172,49 @@ export function generateAssemblyPlan(config, modules = []) {
|
||||
const oX = MARGIN;
|
||||
const oY = MARGIN;
|
||||
|
||||
// Plinth
|
||||
const habLeft = hasSideHabillage(config, 'left');
|
||||
const habRight = hasSideHabillage(config, 'right');
|
||||
const returnLeft = hasSidePlinthReturn(config, 'left');
|
||||
const returnRight = hasSidePlinthReturn(config, 'right');
|
||||
const habT = PANEL.structure * S;
|
||||
const isNiche = config?.installationType === 'niche';
|
||||
const leftDeductMm = isNiche ? 0 : getAxiloPlinthFrontSideDeductionMm(config, 'left');
|
||||
const rightDeductMm = isNiche ? 0 : getAxiloPlinthFrontSideDeductionMm(config, 'right');
|
||||
const sideSetbackMm = PLINTH.frontSetbackMm;
|
||||
|
||||
// Plinth — shortened where side cladding or a recessed plinth return runs to the floor
|
||||
const plinthY = oY + bodyHeight * S;
|
||||
svg += `<rect x="${oX}" y="${plinthY}" width="${widthMm * S}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
const plinthX = oX + leftDeductMm * S;
|
||||
const plinthW = (widthMm - leftDeductMm - rightDeductMm) * S;
|
||||
svg += `<rect x="${plinthX}" y="${plinthY}" width="${plinthW}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
|
||||
// Body
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${widthMm * S}" height="${bodyHeight * S}" class="body" />`;
|
||||
if (habLeft) {
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${habT}" height="${heightMm * S}" class="body" />`;
|
||||
} else if (returnLeft) {
|
||||
svg += `<rect x="${oX + sideSetbackMm * S}" y="${plinthY}" width="${habT}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
}
|
||||
if (habRight) {
|
||||
svg += `<rect x="${oX + widthMm * S - habT}" y="${oY}" width="${habT}" height="${heightMm * S}" class="body" />`;
|
||||
} else if (returnRight) {
|
||||
svg += `<rect x="${oX + widthMm * S - (sideSetbackMm + PANEL.structure) * S}" y="${plinthY}" width="${habT}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
}
|
||||
|
||||
// Module separators
|
||||
if (modules.length > 0) {
|
||||
if (planModules.length > 0) {
|
||||
let currentX = oX;
|
||||
for (let i = 0; i < modules.length - 1; i++) {
|
||||
currentX += modules[i].widthMm * S;
|
||||
svg += `<line x1="${currentX}" y1="${oY}" x2="${currentX}" y2="${plinthY}" class="separator" />`;
|
||||
for (let i = 0; i < planModules.length - 1; i++) {
|
||||
currentX += planModules[i].widthMm * S;
|
||||
if (resolvedSpans.length === 0) {
|
||||
svg += `<line x1="${currentX}" y1="${oY}" x2="${currentX}" y2="${plinthY}" class="separator" />`;
|
||||
} else {
|
||||
for (const segment of getSeparatorVerticalSegments(spanConfig, i)) {
|
||||
const y1 = plinthY - segment.topMm * S;
|
||||
const y2 = plinthY - segment.bottomMm * S;
|
||||
svg += `<line x1="${currentX}" y1="${y1}" x2="${currentX}" y2="${y2}" class="separator" />`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,9 +230,9 @@ export function generateAssemblyPlan(config, modules = []) {
|
||||
svg += `<rect x="${oX + i * panelWidth * 0.95 + 5}" y="${oY + 5}" width="${panelWidth * 0.9}" height="${(bodyHeight - 10) * S}" class="door" />`;
|
||||
}
|
||||
} else {
|
||||
const blockCount = modules.length > 0 ? modules.length : (config.blocks?.length || 2);
|
||||
const blockCount = planModules.length > 0 ? planModules.length : (config.blocks?.length || 2);
|
||||
let currentX = oX;
|
||||
const items = modules.length > 0 ? modules : (config.blocks || []);
|
||||
const items = planModules.length > 0 ? planModules : (config.blocks || []);
|
||||
const hw = config.hardware || {};
|
||||
const fullHeightHandle = hw.handleType === 'full_height';
|
||||
const widthDeductionMm = fullHeightHandle ? HANDLE.fullHeightWidthDeductionMm : 0;
|
||||
@@ -193,24 +241,116 @@ export function generateAssemblyPlan(config, modules = []) {
|
||||
|
||||
for (let i = 0; i < blockCount; i++) {
|
||||
const bw = items[i]?.widthMm || (widthMm / blockCount);
|
||||
const doorWmm = Math.max(100, Math.floor(widthMm / blockCount) - 4 - widthDeductionMm);
|
||||
const modulePx = bw * S;
|
||||
const doorWmm = Math.max(
|
||||
100,
|
||||
Math.floor(resolvedSpans.length === 0 ? widthMm / blockCount : bw) -
|
||||
4 -
|
||||
widthDeductionMm
|
||||
);
|
||||
const doorWPx = doorWmm * S;
|
||||
const doorX = currentX + (modulePx - doorWPx) / 2;
|
||||
const doorY = oY + gapMm * S;
|
||||
|
||||
svg += `<rect x="${doorX}" y="${doorY}" width="${doorWPx}" height="${doorHmm * S}" class="door" />`;
|
||||
|
||||
const hingeSide = getDoorHingeSide(i, blockCount, hw);
|
||||
svg += drawHingeMarkers(doorX, doorY, doorWmm, doorHmm, S, hingeSide);
|
||||
if (fullHeightHandle) {
|
||||
svg += drawFullHeightHandleGrooveElevation(doorX, doorY, doorWmm, doorHmm, S, hingeSide);
|
||||
|
||||
if (resolvedSpans.length === 0 || planModules.length === 0) {
|
||||
const doorY = oY + gapMm * S;
|
||||
svg += `<rect x="${doorX}" y="${doorY}" width="${doorWPx}" height="${doorHmm * S}" class="door" />`;
|
||||
svg += drawHingeMarkers(doorX, doorY, doorWmm, doorHmm, S, hingeSide);
|
||||
if (fullHeightHandle) {
|
||||
svg += drawFullHeightHandleGrooveElevation(doorX, doorY, doorWmm, doorHmm, S, hingeSide);
|
||||
}
|
||||
} else {
|
||||
const module = planModules[i] || {};
|
||||
let heights = Array.isArray(module.blockHeightsMm)
|
||||
? module.blockHeightsMm.map(Number).filter((height) => Number.isFinite(height) && height > 0)
|
||||
: [];
|
||||
if (!heights.length) {
|
||||
const stacked = module.stacked === true ||
|
||||
(module.stacked == null && config.stackEnabled === true);
|
||||
const bottom = Number(module.stackBottomHeightMm ?? config.stackBottomHeightMm);
|
||||
heights = stacked && bottom > 0 && bottom < bodyHeight
|
||||
? [bottom, bodyHeight - bottom]
|
||||
: [bodyHeight];
|
||||
}
|
||||
const blockDoors = Array.isArray(module.blockDoors) ? module.blockDoors : null;
|
||||
let bottomMm = 0;
|
||||
heights.forEach((height, blockIndex) => {
|
||||
const topMm = bottomMm + height;
|
||||
const covered = resolvedSpans.some((span) =>
|
||||
span.moduleIndexes.includes(i) &&
|
||||
bottomMm >= span.bottomMm - 0.5 &&
|
||||
topMm <= span.topMm + 0.5
|
||||
);
|
||||
const hasDoor =
|
||||
blockDoors && typeof blockDoors[blockIndex] === 'boolean'
|
||||
? blockDoors[blockIndex]
|
||||
: true;
|
||||
if (hasDoor && !covered) {
|
||||
const segmentDoorHeightMm = Math.max(80, height - 4);
|
||||
const doorY = plinthY - topMm * S + gapMm * S;
|
||||
svg += `<rect x="${doorX}" y="${doorY}" width="${doorWPx}" height="${segmentDoorHeightMm * S}" class="door" />`;
|
||||
svg += drawHingeMarkers(
|
||||
doorX, doorY, doorWmm, segmentDoorHeightMm, S, hingeSide
|
||||
);
|
||||
if (fullHeightHandle) {
|
||||
svg += drawFullHeightHandleGrooveElevation(
|
||||
doorX, doorY, doorWmm, segmentDoorHeightMm, S, hingeSide
|
||||
);
|
||||
}
|
||||
}
|
||||
bottomMm = topMm;
|
||||
});
|
||||
}
|
||||
|
||||
currentX += modulePx;
|
||||
}
|
||||
}
|
||||
|
||||
for (const span of resolvedSpans) {
|
||||
const x1 = oX + span.leftMm * S;
|
||||
const x2 = oX + span.rightMm * S;
|
||||
const yBottom = plinthY - span.bottomMm * S;
|
||||
const yTop = plinthY - span.topMm * S;
|
||||
svg += `<line x1="${x1}" y1="${yBottom}" x2="${x2}" y2="${yBottom}" class="horizontal-span" />`;
|
||||
svg += `<line x1="${x1}" y1="${yTop}" x2="${x2}" y2="${yTop}" class="horizontal-span" />`;
|
||||
svg += `<text x="${(x1 + x2) / 2}" y="${(yBottom + yTop) / 2}" class="horizontal-span-label" text-anchor="middle">Portée horizontale — colonnes ${span.moduleIndexes.map((index) => index + 1).join('–')}</text>`;
|
||||
|
||||
const doorMode = span.span?.doorMode || 'none';
|
||||
if (doorType !== 'sliding' && doorMode !== 'none') {
|
||||
const hw = config.hardware || {};
|
||||
const fullHeightHandle = hw.handleType === 'full_height';
|
||||
const widthDeductionMm = fullHeightHandle ? HANDLE.fullHeightWidthDeductionMm : 0;
|
||||
const doorHeightMm = Math.max(120, span.topMm - span.bottomMm - 4);
|
||||
const doorY = yTop + 2 * S;
|
||||
const defs = doorMode === 'single'
|
||||
? [{ leftMm: span.leftMm, widthMm: span.widthMm }]
|
||||
: [
|
||||
{
|
||||
leftMm: span.leftMm,
|
||||
widthMm: span.widthMm / 2,
|
||||
hingeSide: 'left'
|
||||
},
|
||||
{
|
||||
leftMm: span.leftMm + span.widthMm / 2,
|
||||
widthMm: span.widthMm / 2,
|
||||
hingeSide: 'right'
|
||||
}
|
||||
];
|
||||
defs.forEach((door, doorIndex) => {
|
||||
const doorWidthMm = Math.max(100, Math.floor(door.widthMm) - 4 - widthDeductionMm);
|
||||
const doorX = oX + door.leftMm * S + (door.widthMm - doorWidthMm) * S / 2;
|
||||
const hingeSide = door.hingeSide || getDoorHingeSide(doorIndex, defs.length, hw);
|
||||
svg += `<rect x="${doorX}" y="${doorY}" width="${doorWidthMm * S}" height="${doorHeightMm * S}" class="door" data-horizontal-span-door="${doorMode}" data-hinge-side="${hingeSide}" />`;
|
||||
svg += drawHingeMarkers(doorX, doorY, doorWidthMm, doorHeightMm, S, hingeSide);
|
||||
if (fullHeightHandle) {
|
||||
svg += drawFullHeightHandleGrooveElevation(
|
||||
doorX, doorY, doorWidthMm, doorHeightMm, S, hingeSide
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reinforcements (Forteresse 16 mm):
|
||||
// width = armoire width - 2×19 (entre côtés),
|
||||
// one at top + one at bottom (offset 19 mm),
|
||||
@@ -257,12 +397,23 @@ export function generateSidePlan(config) {
|
||||
const oX = MARGIN;
|
||||
const oY = MARGIN;
|
||||
|
||||
// Plinth
|
||||
const plinthY = oY + bodyHeight * S;
|
||||
svg += `<rect x="${oX}" y="${plinthY}" width="${depthMm * S}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
const habLeft = hasSideHabillage(config, 'left');
|
||||
const returnLeft = hasSidePlinthReturn(config, 'left');
|
||||
|
||||
// Body
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${depthMm * S}" height="${bodyHeight * S}" class="body" />`;
|
||||
const plinthY = oY + bodyHeight * S;
|
||||
const plinthSetbackMm = PLINTH.frontSetbackMm;
|
||||
if (habLeft) {
|
||||
// Habillage gauche jusqu'au sol : pas de plinthe visible de ce côté.
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${depthMm * S}" height="${heightMm * S}" class="body" />`;
|
||||
} else {
|
||||
if (returnLeft) {
|
||||
const returnLenMm = getAxiloPlinthReturnLengthMm(depthMm);
|
||||
svg += `<rect x="${oX + plinthSetbackMm * S}" y="${plinthY}" width="${returnLenMm * S}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
} else {
|
||||
svg += `<rect x="${oX + plinthSetbackMm * S}" y="${plinthY}" width="${PLINTH.panelThicknessMm * S}" height="${plinthHeightMm * S}" class="plinth" />`;
|
||||
}
|
||||
svg += `<rect x="${oX}" y="${oY}" width="${depthMm * S}" height="${bodyHeight * S}" class="body" />`;
|
||||
}
|
||||
|
||||
// Back panel (8mm) shown as a thin rectangle at the back
|
||||
const backX = oX + (depthMm - PANEL.back) * S;
|
||||
|
||||
Reference in New Issue
Block a user