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
+268 -62
View File
@@ -12,12 +12,19 @@ import {
HINGE,
tourillonFaceDrillDepthMm,
DRAWER_REINFORCEMENT,
DRAWER_RENFORT_PANEL_IXCONNECT
DRAWER_RENFORT_PANEL_IXCONNECT,
getBackGrooveInnerEndInsetFromBackEdgeMm
} from './manufacturing.js';
/** Perçage ferrure en face — Ø8 mm (même diamètre que tourillon / Ixconnect face) */
export const CARCASS_FERRURE_FACE_DIAMETER_MM = 8;
/** Axe X du trou Inbus sur le panneau qui porte le long perçage Ixconnect en chant. */
function ixconnectInbusXFromEndMm({ endSide, widthMm, clampX }) {
const offset = Number(CONNECTORS.ixconnect.lockScrewInbus?.offsetFromAssemblyEdgeMm) || 25;
return clampX(endSide === 'left' ? offset : Number(widthMm) - offset);
}
/** Montants / séparateurs : rangées Ixconnect jonction dessus·fond — retrait depuis bords haut/bas (mm, origine Y = bas pièce en G-code). */
export const IXCONNECT_VERTICAL_JOINT_ROW_INSET_MM = 23;
@@ -32,6 +39,98 @@ export function mapJunctionXAcrossWidthMm(widthMm, xmm) {
return clamped;
}
/**
* Machining X is stored in finished-panel millimetres (origin = left). Saw blanks
* (`rawCutWidthMm`) are longer; that extra sits on +X, so right-edge features
* (montants, AXILO) look too far from the right until they are re-anchored.
* Features within `edgeBandMm` of the finished right edge keep the same inset
* on the workpiece; everything else stays left-origin.
*/
export function mapFinishedXOntoWorkpieceMm(xMm, finishedWidthMm, workpieceWidthMm, edgeBandMm = 120) {
const x = Number(xMm) || 0;
const fw = Math.max(0, Number(finishedWidthMm) || 0);
const ww = Math.max(0, Number(workpieceWidthMm) || 0);
if (ww < 0.5) return x;
if (fw < 0.5 || Math.abs(ww - fw) < 0.05) return Math.max(0, Math.min(ww, x));
const distRight = fw - x;
const distLeft = x;
if (distRight <= edgeBandMm && distRight <= distLeft + 0.05) {
return Math.max(0, Math.min(ww, ww - distRight));
}
return Math.max(0, Math.min(ww, x));
}
/** AXILO 78 Press-fit holes on the underside of a bottom panel. */
export function buildAxiloFootHoleSpecsMm(piece, widthMm, heightMm) {
const axilo = piece?.machining?.axiloFootPlates;
if ((piece?.elementType || '') !== 'fond' || !axilo?.centers?.length) return [];
const finishedW = Number(piece?.widthMm) > 0 ? Number(piece.widthMm) : widthMm;
const diameterMm = Number(axilo.holeDiameterMm) || 10;
const depthMm = Number(axilo.holeDepthMm) || 12;
const clampX = (v) => Math.max(0, Math.min(widthMm, Number(v) || 0));
const clampY = (v) => Math.max(0, Math.min(heightMm, Number(v) || 0));
const out = [];
for (const center of axilo.centers) {
for (const offset of axilo.holeOffsetsFromCenterMm || []) {
out.push({
xMm: clampX(
mapFinishedXOntoWorkpieceMm(
Number(center.xMm) + Number(offset.x || 0),
finishedW,
widthMm
)
),
yMm: clampY(Number(center.yMm) + Number(offset.y || 0)),
diameterMm,
depthMm,
type: 'axilo_press_fit',
face: 'back',
row: center.row
});
}
}
return out;
}
/** System 32 rows; the rear row clears the back-groove zone when present. */
export function buildSystem32HoleSpecsMm(piece, widthMm, heightMm) {
const system32 = piece?.machining?.system32;
if (!system32?.positions?.length) return [];
const finishedW = Number(piece?.widthMm) > 0 ? Number(piece.widthMm) : widthMm;
const clampX = (v) => Math.max(0, Math.min(widthMm, Number(v) || 0));
const clampY = (v) => Math.max(0, Math.min(heightMm, Number(v) || 0));
const mapX = (v) => clampX(mapFinishedXOntoWorkpieceMm(v, finishedW, widthMm));
const backClearanceMm = piece?.machining?.groove
? getBackGrooveInnerEndInsetFromBackEdgeMm()
: 0;
const xRows = [
mapX(37),
mapX(Math.max(0, finishedW - 37 - backClearanceMm))
];
const diameterMm = Number(system32.holeDiameter) || 5;
const depthMm = Number(system32.holeDepth || piece?.thicknessMm) || 12;
const toolMode = system32.throughHole ? 'through' : 'blind';
const out = [];
for (const pos of system32.positions) {
for (const xMm of xRows) {
out.push({
xMm,
yMm: clampY(pos),
diameterMm,
depthMm,
type: 'system32',
face: 'face',
toolMode
});
}
}
return out;
}
/** Même logique que `getCarcassDowelStationsForPlan` dans cuttingEngine.js */
export function getCarcassDowelStationsForPlan(cf, edgeLengthMm) {
const L = Math.max(1, Number(edgeLengthMm) || 0);
@@ -50,14 +149,22 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
const out = [];
const m = piece?.machining || {};
const thicknessMm = Math.max(1, Number(piece?.thicknessMm) || 19);
if (!['dessus', 'fond'].includes(piece.elementType || '')) return out;
const et = piece.elementType || '';
if (!['dessus', 'fond'].includes(et)) return out;
const clampX = (v) => Math.max(0, Math.min(widthMm, Number(v) || 0));
const clampY = (v) => Math.max(0, Math.min(heightMm, Number(v) || 0));
/** Largeur « caisse » (finie) pour repères jonctions — peut être < widthMm (brut CNC). */
const refW =
Number(piece?.widthMm) > 0 ? Math.min(widthMm, Number(piece.widthMm)) : widthMm;
const mapX = (xmm) => clampX(mapFinishedXOntoWorkpieceMm(xmm, refW, widthMm));
const edgeTolMm = 0.5;
/**
* Face d’entrée en épaisseur pour jonctions caisse (côtés / séparateurs) :
* — dessus : sous-face (intérieur caisse) → FACE=back (MPR von unten / 3D fromBottom)
* — fond : face supérieure (intérieur caisse) → FACE=face
*/
const carcassFace = et === 'dessus' ? 'back' : 'face';
const cf = m.carcassFasteners;
if (cf && (cf.dowelStations?.length || cf.positionsAlongJointMm?.length)) {
@@ -69,11 +176,15 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
const zFerr = Number(cf.ferrureDepthInThicknessMm ?? CARCASS_FASTENERS.ferrureDepthInThicknessMm);
const zFerrEdge = Number(cf.ferrureDepthAlongLengthMm ?? 46);
for (const xmm of jax) {
for (let junctionIndex = 0; junctionIndex < jax.length; junctionIndex++) {
const xmm = jax[junctionIndex];
const rawX = clampX(xmm);
const isEnd = rawX <= edgeTolMm || rawX >= refW - edgeTolMm;
const explicitFaceMode = cf.junctionFaceModes?.[junctionIndex];
const isEnd = explicitFaceMode
? explicitFaceMode === 'end'
: rawX <= edgeTolMm || rawX >= refW - edgeTolMm;
const endSide = rawX <= refW / 2 ? 'left' : 'right';
const x = clampX(mapJunctionXAcrossWidthMm(widthMm, xmm));
const x = mapX(mapJunctionXAcrossWidthMm(refW, xmm));
for (const st of stations) {
const along = st.holesAlongMm || [];
for (let hi = 0; hi < along.length; hi++) {
@@ -87,7 +198,7 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
diameterMm: dTour,
depthMm: zTour,
type: 'tourillon',
face: isEnd ? 'end' : 'face',
face: isEnd ? 'end' : carcassFace,
...(isEnd ? { end: endSide } : {})
});
} else {
@@ -98,32 +209,25 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
diameterMm: CARCASS_FERRURE_FACE_DIAMETER_MM,
depthMm: isEnd ? zFerrEdge : zFerr,
type: 'ixconnect',
face: isEnd ? 'end' : 'face',
face: isEnd ? 'end' : carcassFace,
...(isEnd ? { end: endSide } : {})
});
/* Trou Inbus : même règle que connecteur T–IX–T (un par Ixconnect ferrure). Sans ceci, les jeux côté rainure
* (souvent issus de ferrures caisse seules) n’ont pas de trou violet Inbus en plan / G-code / 3D. */
{
/* Le Ø6 d'accès appartient au même panneau que le long Ø8 percé en chant. */
if (isEnd) {
const lock = CONNECTORS.ixconnect.lockScrewInbus;
const dLock = Number(lock?.diameter) || 6;
const zLock = Number(lock?.depth) || 9;
const epsMm = 0.1;
let xInbus = clampX(x);
if (isEnd) {
const depthAlongMm = Math.min(zFerrEdge, widthMm * 0.48, 80);
xInbus =
endSide === 'left'
? depthAlongMm / 2 + epsMm
: widthMm - depthAlongMm / 2 - epsMm;
xInbus = clampX(xInbus);
}
out.push({
xMm: xInbus,
xMm: ixconnectInbusXFromEndMm({
endSide,
widthMm,
clampX
}),
yMm,
diameterMm: dLock,
depthMm: zLock,
type: 'ixconnect_inbus',
face: 'face'
face: carcassFace
});
}
}
@@ -134,7 +238,14 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
const ch = m.connectorHoles;
const joints = ch?.ixconnectJointsAlongDepth;
if (joints?.length) {
/* Quand les ferrures caisse (carcassFasteners) sont déjà posées, elles portent le trio
* T–IX–T + Inbus le long de chaque jonction. Réémettre ixconnectJointsAlongDepth
* (motif bord avant 20/50/80) doublonne le jeu avant — d’où deux trous au centre Ixconnect. */
const carcassOwnsHorizontalJoints =
Array.isArray(cf?.dowelStations) && cf.dowelStations.length > 0
? true
: Array.isArray(cf?.positionsAlongJointMm) && cf.positionsAlongJointMm.length > 0;
if (joints?.length && !carcassOwnsHorizontalJoints) {
const dTour = Number(ch.tourillonDiameter || CONNECTORS.tourillonDiameter || 8);
const zTour = tourillonFaceDrillDepthMm(thicknessMm);
const dFace = Number(ch.faceHole?.diameter || CONNECTORS.ixconnect.faceHole.diameter || 8);
@@ -148,14 +259,15 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
* clampX(axe droit) recule sous refW−ε et la jonction droite n’est plus « extrémité » → un seul Inbus. */
const axisNominal = Number(j.axisAcrossWidthMm) || 0;
const rawAcross = clampX(axisNominal);
const isEnd =
axisNominal <= edgeTolMm || axisNominal >= refW - edgeTolMm;
const isEnd = j.junctionFace
? j.junctionFace === 'end'
: axisNominal <= edgeTolMm || axisNominal >= refW - edgeTolMm;
const endSide = rawAcross <= refW / 2 ? 'left' : 'right';
const x = isEnd
? rawAcross <= edgeTolMm
? 0
: refW
: clampX(mapJunctionXAcrossWidthMm(widthMm, j.axisAcrossWidthMm));
: widthMm
: mapX(mapJunctionXAcrossWidthMm(refW, j.axisAcrossWidthMm));
const seq = [
{ axis: j.tourillon1Axis, d: dTour, z: isEnd ? zTourEdge : zTour, type: 'tourillon' },
{ axis: j.ixconnectAxis, d: isEnd ? dEdge : dFace, z: isEnd ? zEdge : zFace, type: 'ixconnect' },
@@ -170,37 +282,27 @@ export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
diameterMm: h.d,
depthMm: h.z,
type: h.type,
face: isEnd ? 'end' : 'face',
face: isEnd ? 'end' : carcassFace,
...(isEnd ? { end: endSide } : {})
});
}
/* Trou de serrage Inbus : un par Ixconnect — même (Y) que l’axe Ixconnect ; X aligné sur le centre du
* perçage tel qu’affiché en 3D (même formule que Cnc3DViewer pour FACE=end : min(prof chant, 48 % largeur, 80 mm) / 2).
* Sur jonction face : X = x du trio. Sur extrémité (montant) : entrée toujours face grande (perpendiculaire),
* mais X ne peut pas rester 0/refW sinon le cylindre est sur le bord au lieu du centre du violet sur chant. */
{
/* Le Ø6 d'accès appartient au même panneau que le long Ø8 percé en chant. */
if (isEnd) {
const lock = CONNECTORS.ixconnect.lockScrewInbus;
const dLock = Number(lock?.diameter) || 6;
const zLock = Number(lock?.depth) || 9;
const yIx = clampY(Number(j.ixconnectAxis));
const epsMm = 0.1;
let xInbus = clampX(x);
if (isEnd) {
const depthAlongMm = Math.min(zEdge, widthMm * 0.48, 80);
xInbus =
endSide === 'left'
? depthAlongMm / 2 + epsMm
: widthMm - depthAlongMm / 2 - epsMm;
xInbus = clampX(xInbus);
}
out.push({
xMm: xInbus,
yMm: yIx,
xMm: ixconnectInbusXFromEndMm({
endSide,
widthMm,
clampX
}),
yMm: clampY(Number(j.ixconnectAxis)),
diameterMm: dLock,
depthMm: zLock,
type: 'ixconnect_inbus',
face: 'face'
face: carcassFace
});
}
}
@@ -369,6 +471,81 @@ export function buildDrawerRenfortPanelIxconnectSpecsMm(piece, widthMm, heightMm
return [];
}
/**
* Coulisses Legrabox — perçages de vissage sur montants / séparateurs.
* Repère plan pièce : X depuis chant AVANT (origine bas-gauche), Y depuis bas panneau.
* `machining.drawerSlideDrilling` : { holeDiameterMm, holeDepthMm, positionsXFromFrontMm,
* cotes: { leftInnerFace: { railAxesYMm }, rightInnerFace: { railAxesYMm } },
* separators: [{ faceTowardLowerModule: { railAxesYMm }, faceTowardHigherModule: { railAxesYMm } }] }
* @param {number} [partIndex] — cotes: 0 = montant gauche, 1 = droit ; séparateurs: index
*/
export function buildLegraboxRailHoleSpecsMm(piece, widthMm, heightMm, partIndex = 0) {
const et = piece?.elementType || '';
const m = piece?.machining?.drawerSlideDrilling;
if (!m?.positionsXFromFrontMm?.length) return [];
const d = Number(m.holeDiameterMm) || 5;
const z = Number(m.holeDepthMm) || 13;
const clampX = (v) => Math.max(0, Math.min(widthMm, Number(v) || 0));
const clampY = (v) => Math.max(0, Math.min(heightMm, Number(v) || 0));
const out = [];
const pushFace = (axesYMm, faceKey, tag) => {
if (!Array.isArray(axesYMm) || !axesYMm.length) return;
for (const y of axesYMm) {
for (const x of m.positionsXFromFrontMm) {
out.push({
xMm: clampX(x),
yMm: clampY(y),
diameterMm: d,
depthMm: z,
type: `legrabox_rail${tag ? `_${tag}` : ''}`,
face: faceKey
});
}
}
};
if (et === 'cotes' && m.cotes) {
const L = m.cotes.leftInnerFace;
const R = m.cotes.rightInnerFace;
const hasL = Array.isArray(L?.railAxesYMm) && L.railAxesYMm.length > 0;
const hasR = Array.isArray(R?.railAxesYMm) && R.railAxesYMm.length > 0;
const face = hasL && hasR ? (partIndex === 0 ? L : R) : hasL ? L : hasR ? R : null;
pushFace(face?.railAxesYMm, 'face');
return out;
}
if (et === 'panneauxInterieurs' && Array.isArray(m.separators)) {
const sep = m.separators[partIndex];
if (!sep) return [];
pushFace(sep.faceTowardLowerModule?.railAxesYMm, 'face', 'sep_lower');
pushFace(sep.faceTowardHigherModule?.railAxesYMm, 'back', 'sep_higher');
return out;
}
return [];
}
/**
* Façades tiroir Legrabox — fixation INSERTA (cheville expansible Ø10, prof. 13 mm).
* Repère plan façade : origine bas-gauche, X/Y en mm (face arrière de la façade).
*/
export function buildLegraboxFacadeInsertaSpecsMm(piece, widthMm, heightMm) {
if ((piece?.elementType || '') !== 'tiroirFacade') return [];
const ins = piece?.machining?.legraboxInserta;
if (!ins?.holes?.length) return [];
const d = Number(ins.holeDiameterMm) || 10;
const z = Number(ins.holeDepthMm) || 13;
return ins.holes.map((h) => ({
xMm: Math.max(0, Math.min(widthMm, Number(h.xMm) || 0)),
yMm: Math.max(0, Math.min(heightMm, Number(h.yMm) || 0)),
diameterMm: d,
depthMm: z,
type: 'legrabox_inserta',
face: 'face'
}));
}
export function buildDrawerReinforcementHoleSpecsMm(piece, widthMm, heightMm) {
if ((piece?.elementType || '') !== 'renfortsTiroir') return [];
const ix = piece?.machining?.drawerReinforcement?.ixconnectFixing;
@@ -402,6 +579,19 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
const H = Math.max(1, Number(heightMm) || 1);
const W = Math.max(1, Number(widthMm) || 1);
const finishedW =
Number(piece?.widthMm) > 0 ? Math.min(W, Number(piece.widthMm)) : W;
const backClearanceMm =
et === 'cotes' && piece?.machining?.groove
? getBackGrooveInnerEndInsetFromBackEdgeMm()
: 0;
const backReferenceMm = Math.max(0, finishedW - backClearanceMm);
const mapAxisX = (axisMm) =>
mapFinishedXOntoWorkpieceMm(
mapJunctionXAcrossWidthMm(finishedW, axisMm),
finishedW,
W
);
const inset = IXCONNECT_VERTICAL_JOINT_ROW_INSET_MM;
if (H < 2 * inset + 5) return out;
@@ -420,9 +610,9 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
const t2 = Number(p.tourillon2Axis);
const ix = Number(p.ixconnectAxis);
const mirrorBack =
W >= IXCONNECT_SEPARATOR_BACK_ROW_MIN_WIDTH_MM && t2 > t1;
finishedW >= IXCONNECT_SEPARATOR_BACK_ROW_MIN_WIDTH_MM && t2 > t1;
if (et === 'panneauxInterieurs') {
if (et === 'panneauxInterieurs' || (et === 'cotes' && ch.verticalJointMode === 'edge')) {
const seqFront = [
{ axis: t1, d: dTour, z: zTourEdge, type: 'tourillon' },
{ axis: ix, d: dEdge, z: zIxEdge, type: 'ixconnect' },
@@ -430,16 +620,21 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
];
const seqBack = mirrorBack
? [
{ axis: W - t2, d: dTour, z: zTourEdge, type: 'tourillon' },
{ axis: W - ix, d: dEdge, z: zIxEdge, type: 'ixconnect' },
{ axis: W - t1, d: dTour, z: zTourEdge, type: 'tourillon' }
{ axis: backReferenceMm - t2, d: dTour, z: zTourEdge, type: 'tourillon' },
{ axis: backReferenceMm - ix, d: dEdge, z: zIxEdge, type: 'ixconnect' },
{ axis: backReferenceMm - t1, d: dTour, z: zTourEdge, type: 'tourillon' }
]
: null;
const lock = CONNECTORS.ixconnect.lockScrewInbus;
const dLock = Number(lock?.diameter) || 6;
const zLock = Number(lock?.depth) || 9;
const inbusOffset = Number(lock?.offsetFromAssemblyEdgeMm) || 25;
const pushEndRow = (yMm, seq, endSide) => {
const cy = clampY(yMm);
const inbusY = clampY(endSide === 'bottom' ? inbusOffset : H - inbusOffset);
for (const h of seq) {
const xMm = mapJunctionXAcrossWidthMm(W, Number(h.axis));
const xMm = mapAxisX(Number(h.axis));
out.push({
xMm,
yMm: cy,
@@ -449,6 +644,16 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
face: 'end',
end: endSide
});
if (h.type === 'ixconnect') {
out.push({
xMm,
yMm: inbusY,
diameterMm: dLock,
depthMm: zLock,
type: 'ixconnect_inbus',
face: 'face'
});
}
}
};
@@ -460,7 +665,8 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
return out;
}
/* cotes : face */
/* cotes sans habillage latéral : logement face uniquement.
* L'accès Inbus est sur la pièce opposée qui porte le long perçage en chant. */
const seqFront = [
{ axis: t1, d: dTour, z: zTour, type: 'tourillon' },
{ axis: ix, d: dFace, z: zFace, type: 'ixconnect' },
@@ -468,15 +674,15 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
];
const seqBack = mirrorBack
? [
{ axis: W - t2, d: dTour, z: zTour, type: 'tourillon' },
{ axis: W - ix, d: dFace, z: zFace, type: 'ixconnect' },
{ axis: W - t1, d: dTour, z: zTour, type: 'tourillon' }
{ axis: backReferenceMm - t2, d: dTour, z: zTour, type: 'tourillon' },
{ axis: backReferenceMm - ix, d: dFace, z: zFace, type: 'ixconnect' },
{ axis: backReferenceMm - t1, d: dTour, z: zTour, type: 'tourillon' }
]
: null;
const pushFaceRow = (yMm, seq) => {
const cy = clampY(yMm);
for (const h of seq) {
const xMm = mapJunctionXAcrossWidthMm(W, Number(h.axis));
const xMm = mapAxisX(Number(h.axis));
out.push({
xMm,
yMm: cy,
@@ -487,10 +693,10 @@ export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heigh
});
}
};
for (const yMm of yRows) {
pushFaceRow(yMm, seqFront);
if (seqBack) pushFaceRow(yMm, seqBack);
}
pushFaceRow(yRows[0], seqFront);
if (seqBack) pushFaceRow(yRows[0], seqBack);
pushFaceRow(yRows[1], seqFront);
if (seqBack) pushFaceRow(yRows[1], seqBack);
return out;
}