Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
lejay
2026-10-07 17:13:03 +02:00
co-authored by Cursor
parent 6c0d7e47b8
commit 30921aade6
75 changed files with 15618 additions and 1300 deletions
+173 -22
View File
@@ -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;