Ship configurator updates with catalog DB included for server deploy. Co-authored-by: Cursor <[email protected]>
798 lines
31 KiB
JavaScript
798 lines
31 KiB
JavaScript
/**
|
||
* Perçages face panneaux dessus/fond — alignés sur la vue A des plans (cuttingEngine)
|
||
* et sur les commentaires HOLE du G-code (cncGenerator).
|
||
*/
|
||
import {
|
||
CONNECTORS,
|
||
PANEL,
|
||
CARCASS_FASTENERS,
|
||
getCarcassFerrureDowelDrillings,
|
||
getHingeCountForDoor,
|
||
getHingeCupPositions,
|
||
HINGE,
|
||
tourillonFaceDrillDepthMm,
|
||
DRAWER_REINFORCEMENT,
|
||
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;
|
||
|
||
/** Séparateurs verticaux : second jeu T–IX–T symétrique depuis le bord arrière (profondeur), si largeur brut le permet. */
|
||
export const IXCONNECT_SEPARATOR_BACK_ROW_MIN_WIDTH_MM = 200;
|
||
|
||
export function mapJunctionXAcrossWidthMm(widthMm, xmm) {
|
||
const edgeInset = PANEL.structure * 0.5;
|
||
const clamped = Math.max(0, Math.min(widthMm, Number(xmm) || 0));
|
||
if (clamped <= 0) return edgeInset;
|
||
if (clamped >= widthMm) return Math.max(0, widthMm - edgeInset);
|
||
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);
|
||
const off = cf.dowelSideOffset ?? CARCASS_FASTENERS.dowel.sideOffset;
|
||
let positions = cf.positionsAlongJointMm;
|
||
if (!positions?.length && cf.dowelStations?.length) {
|
||
positions = cf.dowelStations.map((s) => s.centerMm).filter((v) => Number.isFinite(Number(v)));
|
||
}
|
||
return getCarcassFerrureDowelDrillings(positions || [], L, off);
|
||
}
|
||
|
||
/**
|
||
* @returns {Array<{ xMm: number, yMm: number, diameterMm: number, depthMm: number, type: string, face: string }>}
|
||
*/
|
||
export function buildTopBottomPlanHoleSpecsMm(piece, widthMm, heightMm) {
|
||
const out = [];
|
||
const m = piece?.machining || {};
|
||
const thicknessMm = Math.max(1, Number(piece?.thicknessMm) || 19);
|
||
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)) {
|
||
const edgeL = Math.max(1, Number(cf.edgeLengthMm) || heightMm);
|
||
const stations = getCarcassDowelStationsForPlan(cf, edgeL);
|
||
const jax = cf.junctionAxesAcrossWidthMm?.length ? cf.junctionAxesAcrossWidthMm : [0, refW];
|
||
const dTour = Number(cf.dowelDiameter || CARCASS_FASTENERS.dowel.diameter);
|
||
const zTour = tourillonFaceDrillDepthMm(thicknessMm);
|
||
const zFerr = Number(cf.ferrureDepthInThicknessMm ?? CARCASS_FASTENERS.ferrureDepthInThicknessMm);
|
||
const zFerrEdge = Number(cf.ferrureDepthAlongLengthMm ?? 46);
|
||
|
||
for (let junctionIndex = 0; junctionIndex < jax.length; junctionIndex++) {
|
||
const xmm = jax[junctionIndex];
|
||
const rawX = clampX(xmm);
|
||
const explicitFaceMode = cf.junctionFaceModes?.[junctionIndex];
|
||
const isEnd = explicitFaceMode
|
||
? explicitFaceMode === 'end'
|
||
: rawX <= edgeTolMm || rawX >= refW - edgeTolMm;
|
||
const endSide = rawX <= refW / 2 ? 'left' : 'right';
|
||
const x = mapX(mapJunctionXAcrossWidthMm(refW, xmm));
|
||
for (const st of stations) {
|
||
const along = st.holesAlongMm || [];
|
||
for (let hi = 0; hi < along.length; hi++) {
|
||
const alongMm = along[hi];
|
||
const yMm = clampY(alongMm);
|
||
const isTourillon = hi !== 1;
|
||
if (isTourillon) {
|
||
out.push({
|
||
xMm: x,
|
||
yMm,
|
||
diameterMm: dTour,
|
||
depthMm: zTour,
|
||
type: 'tourillon',
|
||
face: isEnd ? 'end' : carcassFace,
|
||
...(isEnd ? { end: endSide } : {})
|
||
});
|
||
} else {
|
||
/* Même perçage ferrure (Ø/prof) ; type ixconnect pour G-code / 3D : violet + long sur chant montant, comme renfort. */
|
||
out.push({
|
||
xMm: x,
|
||
yMm,
|
||
diameterMm: CARCASS_FERRURE_FACE_DIAMETER_MM,
|
||
depthMm: isEnd ? zFerrEdge : zFerr,
|
||
type: 'ixconnect',
|
||
face: isEnd ? 'end' : carcassFace,
|
||
...(isEnd ? { end: endSide } : {})
|
||
});
|
||
/* 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;
|
||
out.push({
|
||
xMm: ixconnectInbusXFromEndMm({
|
||
endSide,
|
||
widthMm,
|
||
clampX
|
||
}),
|
||
yMm,
|
||
diameterMm: dLock,
|
||
depthMm: zLock,
|
||
type: 'ixconnect_inbus',
|
||
face: carcassFace
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const ch = m.connectorHoles;
|
||
const joints = ch?.ixconnectJointsAlongDepth;
|
||
/* 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);
|
||
const zFace = Number(ch.faceHole?.depth || CONNECTORS.ixconnect.faceHole.depth || 12);
|
||
const dEdge = Number(ch.edgeHole?.diameter || CONNECTORS.ixconnect.edgeHole.diameter || 8);
|
||
const zEdge = Number(ch.edgeHole?.depth || CONNECTORS.ixconnect.edgeHole.depth || 45);
|
||
const zTourEdge = Math.min(zTour, 16);
|
||
|
||
for (const j of joints) {
|
||
/* isEnd doit utiliser l’axe nominal (repère caisse), pas clampX avant : si brut CNC < largeur finie,
|
||
* 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 = j.junctionFace
|
||
? j.junctionFace === 'end'
|
||
: axisNominal <= edgeTolMm || axisNominal >= refW - edgeTolMm;
|
||
const endSide = rawAcross <= refW / 2 ? 'left' : 'right';
|
||
const x = isEnd
|
||
? rawAcross <= edgeTolMm
|
||
? 0
|
||
: 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' },
|
||
{ axis: j.tourillon2Axis, d: dTour, z: isEnd ? zTourEdge : zTour, type: 'tourillon' }
|
||
];
|
||
for (const h of seq) {
|
||
const pos = Number(h.axis);
|
||
const yMm = clampY(pos);
|
||
out.push({
|
||
xMm: x,
|
||
yMm,
|
||
diameterMm: h.d,
|
||
depthMm: h.z,
|
||
type: h.type,
|
||
face: isEnd ? 'end' : carcassFace,
|
||
...(isEnd ? { end: endSide } : {})
|
||
});
|
||
}
|
||
|
||
/* 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;
|
||
out.push({
|
||
xMm: ixconnectInbusXFromEndMm({
|
||
endSide,
|
||
widthMm,
|
||
clampX
|
||
}),
|
||
yMm: clampY(Number(j.ixconnectAxis)),
|
||
diameterMm: dLock,
|
||
depthMm: zLock,
|
||
type: 'ixconnect_inbus',
|
||
face: carcassFace
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Rayons mobiles (Étagères amovibles) — système SP 15/8/5
|
||
* - Perçage face (dessous non visible): Ø20 × 15, axe à 9.5 mm du bord (gauche/droite)
|
||
* - Perçage sur chant (épaisseur, contre côtés): Ø8 × 37, positionné à 6 mm de la face inférieure
|
||
* - Positionnement en profondeur (Système 32):
|
||
* - Depuis bord avant: 32 mm (37 axe caisse - 5 mm retrait étagère)
|
||
* - Depuis bord arrière: 37 mm
|
||
*
|
||
* Note: le plan 2D (X,Y) ne porte pas l'offset en épaisseur (6 mm) pour le perçage chant ; on l’encode
|
||
* comme perçage "end" pour la visualisation et l’annotation G-code.
|
||
*
|
||
* @returns {Array<{ xMm: number, yMm: number, diameterMm: number, depthMm: number, type: string, face: string, end?: string }>}
|
||
*/
|
||
export function buildShelfMobileHoleSpecsMm(piece, widthMm, heightMm) {
|
||
const out = [];
|
||
const et = (piece?.elementType || '');
|
||
if (!['etageres', 'rayonFixe'].includes(et)) return out;
|
||
const m = piece?.machining || {};
|
||
const sp = m?.mobileShelfSystem;
|
||
const isFixed = et === 'rayonFixe';
|
||
if (!sp || (sp.model !== 'sp_15_8_5' && sp.model !== 'sp_15_8_5_face_only')) return out;
|
||
|
||
const W = Math.max(1, Number(widthMm) || 1);
|
||
const D = Math.max(1, Number(heightMm) || 1);
|
||
const clampX = (v) => Math.max(0, Math.min(W, Number(v) || 0));
|
||
const clampY = (v) => Math.max(0, Math.min(D, Number(v) || 0));
|
||
|
||
const axisFromSide = Number(sp.faceHole?.axisFromSideMm ?? 9.5);
|
||
const dFace = Number(sp.faceHole?.diameterMm ?? 20);
|
||
const zFace = Number(sp.faceHole?.depthMm ?? 15);
|
||
|
||
const dEdge = Number(sp.edgeHole?.diameterMm ?? 8);
|
||
const zEdge = Number(sp.edgeHole?.depthMm ?? 37);
|
||
|
||
const yFront = clampY(Number(sp.positions?.fromFrontMm ?? 32));
|
||
const yBack = clampY(D - Number(sp.positions?.fromBackMm ?? 37));
|
||
const yPos = [yFront, yBack].filter((v, idx, arr) => arr.findIndex((u) => Math.abs(u - v) < 1e-6) === idx);
|
||
|
||
const xLeft = clampX(axisFromSide);
|
||
const xRight = clampX(W - axisFromSide);
|
||
const xSides = [xLeft, xRight].filter((v, idx, arr) => arr.findIndex((u) => Math.abs(u - v) < 1e-6) === idx);
|
||
|
||
for (const yMm of yPos) {
|
||
// Perçage face dessous (non visible): 2 côtés (gauche/droite)
|
||
for (const xMm of xSides) {
|
||
out.push({
|
||
xMm,
|
||
yMm,
|
||
diameterMm: dFace,
|
||
depthMm: zFace,
|
||
type: 'shelf_mobile_face',
|
||
face: 'face'
|
||
});
|
||
}
|
||
|
||
// Perçage chant côté gauche/droit (rayons mobiles uniquement)
|
||
if (!isFixed) {
|
||
out.push({
|
||
xMm: 0,
|
||
yMm,
|
||
diameterMm: dEdge,
|
||
depthMm: zEdge,
|
||
type: 'shelf_mobile_edge',
|
||
face: 'end',
|
||
end: 'left'
|
||
});
|
||
out.push({
|
||
xMm: W,
|
||
yMm,
|
||
diameterMm: dEdge,
|
||
depthMm: zEdge,
|
||
type: 'shelf_mobile_edge',
|
||
face: 'end',
|
||
end: 'right'
|
||
});
|
||
}
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Renforts tiroir 25 mm — 4× Ixconnect SC 8/25 aux coins (50 mm des bords), repère Y depuis bord haut.
|
||
* @returns {Array<{ xMm: number, yMm: number, diameterMm: number, depthMm: number, type: string, face: string }>}
|
||
*/
|
||
function coteInnerFaceHasRenfortHoles(faceBlock) {
|
||
return Array.isArray(faceBlock?.holes) && faceBlock.holes.length > 0;
|
||
}
|
||
|
||
/**
|
||
* Pièce « côté » seul (gauche ou droit) : une seule face intérieure est présente dans `machining`.
|
||
* Pièce qty 2 : gauche = partIndex 0, droit = 1 lorsque les deux faces existent.
|
||
*/
|
||
function resolveCoteDrawerRenfortInnerFace(cotes, partIndex) {
|
||
const L = cotes?.leftInnerFace;
|
||
const R = cotes?.rightInnerFace;
|
||
const hasL = coteInnerFaceHasRenfortHoles(L);
|
||
const hasR = coteInnerFaceHasRenfortHoles(R);
|
||
if (hasL && hasR) {
|
||
return partIndex === 0 ? L : R;
|
||
}
|
||
if (hasL) return L;
|
||
if (hasR) return R;
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Montants / séparateurs : Ixconnect SC 8/25 zone renfort tiroirs cachés (repère X avant, Y bas).
|
||
* @param {number} [partIndex] — pour cotes: 0 = montant gauche, 1 = droit lorsque les deux sont sur la même ligne ; pour séparateurs: index du séparateur
|
||
*/
|
||
export function buildDrawerRenfortPanelIxconnectSpecsMm(piece, widthMm, heightMm, partIndex = 0) {
|
||
const et = piece?.elementType || '';
|
||
const m = piece?.machining?.drawerRenfortPanelIxconnect;
|
||
if (!m) return [];
|
||
const d = Number(m.faceHole?.diameter ?? DRAWER_RENFORT_PANEL_IXCONNECT.faceHoleDiameterMm) || 8;
|
||
const z = Number(m.faceHole?.depth ?? DRAWER_RENFORT_PANEL_IXCONNECT.faceHoleDepthMm) || 12;
|
||
const out = [];
|
||
|
||
if (et === 'cotes' && m.cotes) {
|
||
const face = resolveCoteDrawerRenfortInnerFace(m.cotes, partIndex);
|
||
const holes = face?.holes;
|
||
if (!holes?.length) return [];
|
||
for (const h of holes) {
|
||
out.push({
|
||
xMm: Number(h.xMm) || 0,
|
||
yMm: Number(h.yMm) || 0,
|
||
diameterMm: d,
|
||
depthMm: z,
|
||
type: 'ixconnect_drawer_renfort_panel',
|
||
face: 'face'
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
if (et === 'panneauxInterieurs' && Array.isArray(m.separators)) {
|
||
const sep = m.separators[partIndex];
|
||
if (!sep) return [];
|
||
/** Vers module d’indice bas : une grande face du panneau ; vers module haut : l’autre (épaisseur). */
|
||
const pushFace = (block, tag, faceKey) => {
|
||
if (!block?.holes?.length) return;
|
||
for (const h of block.holes) {
|
||
out.push({
|
||
xMm: Number(h.xMm) || 0,
|
||
yMm: Number(h.yMm) || 0,
|
||
diameterMm: d,
|
||
depthMm: z,
|
||
type: `ixconnect_drawer_renfort_${tag}`,
|
||
face: faceKey
|
||
});
|
||
}
|
||
};
|
||
pushFace(sep.faceTowardLowerModule, 'sep_lower', 'face');
|
||
pushFace(sep.faceTowardHigherModule, 'sep_higher', 'back');
|
||
return out;
|
||
}
|
||
|
||
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;
|
||
if (!ix?.holes?.length) return [];
|
||
const d = Number(ix.holeDiameterMm ?? DRAWER_REINFORCEMENT.ixconnectHoleDiameterMm) || 8;
|
||
const z = Number(ix.depthInReinforcementMm ?? DRAWER_REINFORCEMENT.ixconnectDepthInReinforcementMm) || 13;
|
||
return ix.holes.map((h) => ({
|
||
xMm: Number(h.xMm) || 0,
|
||
yMm: Number(h.yMm) || 0,
|
||
diameterMm: d,
|
||
depthMm: z,
|
||
type: 'ixconnect_drawer_reinforcement',
|
||
face: 'face'
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Montants : perçages face (axe épaisseur) Ixconnect + tourillons, 2 rangées haut/bas.
|
||
* Séparateurs verticaux : perçages en about sur les petits côtés d’extrémité (faces w×ép. aux deux bouts de la longueur),
|
||
* axe le long de Z (hauteur brut), pas depuis les grands chants profondeur (X).
|
||
* X = centres depuis bord avant profondeur ; Y G-code = 0 (bord bas) ou H (bord haut) — repère contour brut dans le plan pièce.
|
||
* @returns {Array<{ xMm: number, yMm: number, diameterMm: number, depthMm: number, type: string, face: string, end?: string }>}
|
||
*/
|
||
export function buildVerticalPanelIxconnectFaceHoleSpecsMm(piece, widthMm, heightMm) {
|
||
const out = [];
|
||
const et = piece?.elementType || '';
|
||
if (!['cotes', 'panneauxInterieurs'].includes(et)) return out;
|
||
const ch = piece?.machining?.connectorHoles;
|
||
const p = ch?.jointPattern;
|
||
if (!p) return out;
|
||
|
||
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;
|
||
|
||
const thicknessMm = Math.max(1, Number(piece?.thicknessMm) || 19);
|
||
const clampY = (v) => Math.max(0, Math.min(H, Number(v) || 0));
|
||
const dTour = Number(ch.tourillonDiameter || CONNECTORS.tourillonDiameter || 8);
|
||
const zTour = tourillonFaceDrillDepthMm(thicknessMm);
|
||
const dFace = Number(ch.faceHole?.diameter || CONNECTORS.ixconnect.faceHole.diameter || 8);
|
||
const zFace = Number(ch.faceHole?.depth || CONNECTORS.ixconnect.faceHole.depth || 12);
|
||
const dEdge = Number(ch.edgeHole?.diameter || CONNECTORS.ixconnect.edgeHole.diameter || 8);
|
||
const zIxEdge = Number(ch.edgeHole?.depth || CONNECTORS.ixconnect.edgeHole.depth || 45);
|
||
const zTourEdge = Math.min(tourillonFaceDrillDepthMm(thicknessMm), 15);
|
||
|
||
const yRows = [inset, H - inset];
|
||
const t1 = Number(p.tourillon1Axis);
|
||
const t2 = Number(p.tourillon2Axis);
|
||
const ix = Number(p.ixconnectAxis);
|
||
const mirrorBack =
|
||
finishedW >= IXCONNECT_SEPARATOR_BACK_ROW_MIN_WIDTH_MM && t2 > t1;
|
||
|
||
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' },
|
||
{ axis: t2, d: dTour, z: zTourEdge, type: 'tourillon' }
|
||
];
|
||
const seqBack = mirrorBack
|
||
? [
|
||
{ 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 = mapAxisX(Number(h.axis));
|
||
out.push({
|
||
xMm,
|
||
yMm: cy,
|
||
diameterMm: h.d,
|
||
depthMm: h.z,
|
||
type: h.type,
|
||
face: 'end',
|
||
end: endSide
|
||
});
|
||
if (h.type === 'ixconnect') {
|
||
out.push({
|
||
xMm,
|
||
yMm: inbusY,
|
||
diameterMm: dLock,
|
||
depthMm: zLock,
|
||
type: 'ixconnect_inbus',
|
||
face: 'face'
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
/* Y = 0 / H : extrémités longueur du brut (petits côtés w×ép.), pas un inset sur la grande face. */
|
||
pushEndRow(0, seqFront, 'bottom');
|
||
if (seqBack) pushEndRow(0, seqBack, 'bottom');
|
||
pushEndRow(H, seqFront, 'top');
|
||
if (seqBack) pushEndRow(H, seqBack, 'top');
|
||
return out;
|
||
}
|
||
|
||
/* 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' },
|
||
{ axis: t2, d: dTour, z: zTour, type: 'tourillon' }
|
||
];
|
||
const seqBack = mirrorBack
|
||
? [
|
||
{ 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 = mapAxisX(Number(h.axis));
|
||
out.push({
|
||
xMm,
|
||
yMm: cy,
|
||
diameterMm: h.d,
|
||
depthMm: h.z,
|
||
type: h.type,
|
||
face: 'face'
|
||
});
|
||
}
|
||
};
|
||
pushFaceRow(yRows[0], seqFront);
|
||
if (seqBack) pushFaceRow(yRows[0], seqBack);
|
||
pushFaceRow(yRows[1], seqFront);
|
||
if (seqBack) pushFaceRow(yRows[1], seqBack);
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Habillages de finition Premium — perçages SC 8/25 (Ixconnect) stockés dans `piece.machining.finishHabillage.holes`.
|
||
* @returns {Array<{ xMm: number, yMm: number, diameterMm: number, depthMm: number, type: string, face: string, end?: string }>}
|
||
*/
|
||
export function buildFinishHabillageHoleSpecsMm(piece, widthMm, heightMm) {
|
||
const out = [];
|
||
const fh = piece?.machining?.finishHabillage;
|
||
const holes = fh?.holes;
|
||
if (!Array.isArray(holes) || holes.length === 0) return out;
|
||
const W = Math.max(1, Number(widthMm) || 1);
|
||
const H = Math.max(1, Number(heightMm) || 1);
|
||
const clampX = (v) => Math.max(0, Math.min(W, Number(v) || 0));
|
||
const clampY = (v) => Math.max(0, Math.min(H, Number(v) || 0));
|
||
for (const h of holes) {
|
||
if (!h) continue;
|
||
out.push({
|
||
xMm: clampX(h.xMm),
|
||
yMm: clampY(h.yMm),
|
||
diameterMm: Number(h.diameterMm) || Number(fh?.faceHole?.diameter) || 8,
|
||
depthMm: Number(h.depthMm) || Number(fh?.faceHole?.depth) || 12.5,
|
||
type: h.type || 'ixconnect',
|
||
face: h.face || 'face',
|
||
...(h.end ? { end: h.end } : {})
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* Portes : cuvettes charnières (Blum) + 2 perçages Ø8 par cuvette (45 mm c/c, centres à 9,5 mm du centre cuvette vers l’intérieur).
|
||
* Positions (depuis haut) : axe 103 mm et grille System 32 (voir getHingeCupPositions).
|
||
* Repère plan: x = largeur gauche→droite ; y = hauteur depuis bas.
|
||
*
|
||
* @returns {Array<{ xMm: number, yMm: number, diameterMm: number, depthMm: number, type: string, face: string }>}
|
||
*/
|
||
export function buildDoorHingeCupHoleSpecsMm(piece, widthMm, heightMm) {
|
||
const out = [];
|
||
if ((piece?.elementType || '') !== 'portes') return out;
|
||
|
||
const W = Math.max(1, Number(widthMm) || 1);
|
||
const H = Math.max(1, Number(heightMm) || 1);
|
||
const clampX = (v) => Math.max(0, Math.min(W, Number(v) || 0));
|
||
const clampY = (v) => Math.max(0, Math.min(H, Number(v) || 0));
|
||
|
||
const hc = piece?.machining?.hingeCups;
|
||
if (!hc) return out;
|
||
|
||
const diameterMm = Number(hc.diameter) || 35;
|
||
const depthMm = Number(hc.cupDepth) || 12;
|
||
|
||
const screwDiameterMm = Number(hc.screwDiameterMm ?? HINGE.cupScrewDiameter) || 8;
|
||
const screwDepthMm = Number(hc.screwDepthMm ?? HINGE.cupScrewDepth) || 13;
|
||
const screwSpacingMm = Number(hc.screwSpacingMm ?? HINGE.cupScrewSpacing) || 45;
|
||
const screwOffsetMm = Number(hc.screwOffsetFromCupCenterMm ?? HINGE.cupScrewOffsetFromCupCenter) || 9.5;
|
||
const halfScrewSpacing = screwSpacingMm / 2;
|
||
|
||
// Default Blum inset for cup center (~22mm from edge) unless explicitly set upstream.
|
||
const axisFromEdgeMm = Number(hc.axisFromEdgeMm ?? hc.axisFromSideMm ?? hc.axisFromEdge ?? 22);
|
||
|
||
// Hinge side hint (viewer-only). If absent, keep a consistent default.
|
||
const hingeSide = piece?.doorPlanHints?.hingeSide === 'right' ? 'right' : 'left';
|
||
const xMm = hingeSide === 'right' ? clampX(W - axisFromEdgeMm) : clampX(axisFromEdgeMm);
|
||
const dxInterior = hingeSide === 'right' ? -screwOffsetMm : screwOffsetMm;
|
||
|
||
const count = Math.max(2, Number(hc.count) || getHingeCountForDoor({ heightMm: H }));
|
||
const positionsFromTop =
|
||
Array.isArray(hc.positionsFromTop) && hc.positionsFromTop.length
|
||
? hc.positionsFromTop.map((v) => Number(v)).filter((v) => Number.isFinite(v))
|
||
: getHingeCupPositions(H, count);
|
||
|
||
for (const fromTop of positionsFromTop) {
|
||
const yMm = clampY(H - fromTop); // convert "from top" to plan Y from bottom
|
||
out.push({
|
||
xMm,
|
||
yMm,
|
||
diameterMm,
|
||
depthMm,
|
||
type: 'hinge_cup',
|
||
face: 'face'
|
||
});
|
||
const xScrew = clampX(xMm + dxInterior);
|
||
for (const dy of [-halfScrewSpacing, halfScrewSpacing]) {
|
||
out.push({
|
||
xMm: xScrew,
|
||
yMm: clampY(yMm + dy),
|
||
diameterMm: screwDiameterMm,
|
||
depthMm: screwDepthMm,
|
||
type: 'hinge_screw',
|
||
face: 'face'
|
||
});
|
||
}
|
||
}
|
||
|
||
return out;
|
||
}
|