/** * 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; }