592 lines
23 KiB
JavaScript
592 lines
23 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
|
||
} 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;
|
||
|
||
/** 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;
|
||
}
|
||
|
||
/** 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);
|
||
if (!['dessus', 'fond'].includes(piece.elementType || '')) 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 edgeTolMm = 0.5;
|
||
|
||
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 (const xmm of jax) {
|
||
const rawX = clampX(xmm);
|
||
const isEnd = rawX <= edgeTolMm || rawX >= refW - edgeTolMm;
|
||
const endSide = rawX <= refW / 2 ? 'left' : 'right';
|
||
const x = clampX(mapJunctionXAcrossWidthMm(widthMm, 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' : 'face',
|
||
...(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' : 'face',
|
||
...(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. */
|
||
{
|
||
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,
|
||
yMm,
|
||
diameterMm: dLock,
|
||
depthMm: zLock,
|
||
type: 'ixconnect_inbus',
|
||
face: 'face'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const ch = m.connectorHoles;
|
||
const joints = ch?.ixconnectJointsAlongDepth;
|
||
if (joints?.length) {
|
||
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 =
|
||
axisNominal <= edgeTolMm || axisNominal >= refW - edgeTolMm;
|
||
const endSide = rawAcross <= refW / 2 ? 'left' : 'right';
|
||
const x = isEnd
|
||
? rawAcross <= edgeTolMm
|
||
? 0
|
||
: refW
|
||
: clampX(mapJunctionXAcrossWidthMm(widthMm, 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' : 'face',
|
||
...(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. */
|
||
{
|
||
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,
|
||
diameterMm: dLock,
|
||
depthMm: zLock,
|
||
type: 'ixconnect_inbus',
|
||
face: 'face'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
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 [];
|
||
}
|
||
|
||
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 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 =
|
||
W >= IXCONNECT_SEPARATOR_BACK_ROW_MIN_WIDTH_MM && t2 > t1;
|
||
|
||
if (et === 'panneauxInterieurs') {
|
||
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: 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' }
|
||
]
|
||
: null;
|
||
|
||
const pushEndRow = (yMm, seq, endSide) => {
|
||
const cy = clampY(yMm);
|
||
for (const h of seq) {
|
||
const xMm = mapJunctionXAcrossWidthMm(W, Number(h.axis));
|
||
out.push({
|
||
xMm,
|
||
yMm: cy,
|
||
diameterMm: h.d,
|
||
depthMm: h.z,
|
||
type: h.type,
|
||
face: 'end',
|
||
end: endSide
|
||
});
|
||
}
|
||
};
|
||
|
||
/* 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 : face */
|
||
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: 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' }
|
||
]
|
||
: null;
|
||
const pushFaceRow = (yMm, seq) => {
|
||
const cy = clampY(yMm);
|
||
for (const h of seq) {
|
||
const xMm = mapJunctionXAcrossWidthMm(W, Number(h.axis));
|
||
out.push({
|
||
xMm,
|
||
yMm: cy,
|
||
diameterMm: h.d,
|
||
depthMm: h.z,
|
||
type: h.type,
|
||
face: 'face'
|
||
});
|
||
}
|
||
};
|
||
for (const yMm of yRows) {
|
||
pushFaceRow(yMm, seqFront);
|
||
if (seqBack) pushFaceRow(yMm, 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;
|
||
}
|