Files
apex/shared/cncPanelHoles.js
T
lejayandCursor 30921aade6 v2.1
Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
2026-10-07 17:13:03 +02:00

798 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}