@@ -0,0 +1,591 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
/**
|
||||
* APEX 4.0 — Dossier Technique Maître
|
||||
* Single source of truth for all manufacturing specifications.
|
||||
* Every dimension in millimetres unless noted otherwise.
|
||||
*/
|
||||
|
||||
// ── Panel thicknesses ────────────────────────────────────────────────
|
||||
export const PANEL = {
|
||||
structure: 19, // Côtés, dessus, fond, séparateurs, étagères
|
||||
reinforcement: 16, // Renforts Forteresse
|
||||
back: 8, // Dos (panneau arrière)
|
||||
reinforcementWidth: 100, // Largeur standard d'un renfort
|
||||
};
|
||||
|
||||
// ── Reinforcement layout rules (Renforts Forteresse) ────────────────
|
||||
export const REINFORCEMENT = {
|
||||
topBottomOffsetMm: 19, // Renfort haut/bas à 19 mm
|
||||
thirdRequiredAboveHeightMm: 1500, // 3e renfort si armoire > 1.5 m
|
||||
};
|
||||
|
||||
// ── Edge banding (Chants ABS) ────────────────────────────────────────
|
||||
export const EDGE_BANDING = {
|
||||
abs: 2, // Chant ABS visible (2 mm)
|
||||
hidden: 0.5, // Chant fin caché (0.5 mm)
|
||||
backPanelSides: 2, // Dos: ABS sur 2 chants longs (haut + bas)
|
||||
topBottomSides: 2, // Dessus/Fond: ABS faces avant ET arrière
|
||||
shelfSides: 1, // Étagères: ABS face avant uniquement
|
||||
};
|
||||
|
||||
// ── Groove for back panel (Rainure dos) ──────────────────────────────
|
||||
export const GROOVE = {
|
||||
back: {
|
||||
width: 9, // mm — largeur rainure
|
||||
depth: 10.5, // mm — profondeur rainure
|
||||
axisFromPanelEdge: 16, // mm — rainure à 16 mm du bord des côtés/fond/dessus
|
||||
},
|
||||
};
|
||||
|
||||
/** Rainure dos — plans / CNC : même coupe que montant (vertical) vs dessus/fond/bandeau (horizontal). */
|
||||
export const BACK_GROOVE_PLAN_VERTICAL_TYPES = ['cotes', 'fileur', 'cotesDoublage'];
|
||||
export const BACK_GROOVE_PLAN_HORIZONTAL_TYPES = ['dessus', 'fond', 'bandeauHaut'];
|
||||
|
||||
/** @returns {'vertical' | 'horizontal' | null} */
|
||||
export function getBackGroovePlanOrientation(elementType) {
|
||||
if (BACK_GROOVE_PLAN_VERTICAL_TYPES.includes(elementType)) return 'vertical';
|
||||
if (BACK_GROOVE_PLAN_HORIZONTAL_TYPES.includes(elementType)) return 'horizontal';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Séparateurs : plaqués contre le dos (sans rainure). Largeur = même entraxe profondeur que le montant,
|
||||
* diminuée du décalage d’axe et de la demi-largeur de rainure (côtés).
|
||||
* @param {number} carcassDepthMm — profondeur caisse / largeur en plan du panneau côté (mm)
|
||||
*/
|
||||
export function getSeparatorDepthMm(carcassDepthMm) {
|
||||
const d = Number(carcassDepthMm) || 0;
|
||||
return Math.round(d - GROOVE.back.axisFromPanelEdge - GROOVE.back.width / 2);
|
||||
}
|
||||
|
||||
// ── Connectors (Häfele Ixconnect) ────────────────────────────────────
|
||||
export const CONNECTORS = {
|
||||
// SC 8/60 — structural assembly (caisse: côtés ↔ dessus/fond)
|
||||
ixconnect: {
|
||||
model: 'SC 8/60',
|
||||
edgeHole: { diameter: 8, depth: 45 }, // mm — perçage en about
|
||||
faceHole: { diameter: 8, depth: 12 }, // mm — perçage en face
|
||||
/** Trou de serrage Inbus — face intérieure panneaux horizontaux (dessus/fond) uniquement */
|
||||
lockScrewInbus: {
|
||||
diameter: 6,
|
||||
depth: 9,
|
||||
/** mm — depuis le chant d’assemblage (axe du perçage Ø45), vers l’intérieur du panneau */
|
||||
offsetFromAssemblyEdgeMm: 25
|
||||
}
|
||||
},
|
||||
// SC 8/25 — finish panels (portes, finitions)
|
||||
ixconnectFinish: {
|
||||
model: 'SC 8/25',
|
||||
faceHole: { diameter: 8, depth: 12.5 }, // mm — face interne Ø8 × 12.5mm
|
||||
axes: { back: 30, front: 43 }, // mm — axes: 30mm (arrière) / 43mm (avant)
|
||||
},
|
||||
// Positioning per structural joint (axes mm depuis le bord avant de l’arête, le long de la profondeur)
|
||||
// — Arêtes d’extrémité (montants extérieurs): T → IX → T
|
||||
// — Jonctions internes (séparateurs): Ixconnect à l’axe médian profondeur, tourillons ±30 mm de cet axe
|
||||
jointPatternEdge: {
|
||||
tourillon1Axis: 20,
|
||||
ixconnectAxis: 50,
|
||||
tourillon2Axis: 80,
|
||||
},
|
||||
/** @deprecated use jointPatternEdge — alias pour compat. */
|
||||
jointPattern: {
|
||||
tourillon1Axis: 20,
|
||||
ixconnectAxis: 50,
|
||||
tourillon2Axis: 80,
|
||||
},
|
||||
/** Tourillons par rapport à l’axe Ixconnect sur jonction milieu (mm). */
|
||||
jointPatternMiddleTourillonOffsetFromIxMm: 30,
|
||||
tourillon: {
|
||||
diameter: 8, // mm — Ø tourillon
|
||||
length: 30, // mm — tourillon Ø8×30
|
||||
},
|
||||
tourillonDiameter: 8, // mm — backward compat
|
||||
};
|
||||
|
||||
/**
|
||||
* mm — depuis le bord physique panneau côté dos : axe rainure + largeur rainure (= fin utile côté dos, repère Ixconnect).
|
||||
* Évite chevauchement perçages / rainure dos sur dessus·fond.
|
||||
*/
|
||||
export function getBackGrooveInnerEndInsetFromBackEdgeMm() {
|
||||
return GROOVE.back.axisFromPanelEdge + GROOVE.back.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Axes Ixconnect / tourillons le long de l’arête (profondeur caisse pour dessus·fond, largeur = profondeur pour côtés),
|
||||
* mesurés depuis le bord avant (face avant).
|
||||
* @param {'edge'|'middle'} variant — extrémité de caisse vs jonction entre modules (séparateur)
|
||||
* @param {{ horizontalPanel?: boolean }} [options] — si `horizontalPanel`, la profondeur utile exclut la zone rainure dos (16+9 mm depuis le bord arrière).
|
||||
*/
|
||||
export function getIxconnectJointPatternAlongDepthMm(depthMm, variant, options = {}) {
|
||||
const horizontalPanel = Boolean(options.horizontalPanel);
|
||||
const inset = horizontalPanel ? getBackGrooveInnerEndInsetFromBackEdgeMm() : 0;
|
||||
const d = Math.max(0, Number(depthMm) || 0);
|
||||
const dEff = horizontalPanel ? Math.max(0, d - inset) : d;
|
||||
const round1 = (v) => Math.round(v * 10) / 10;
|
||||
if (variant === 'edge') {
|
||||
const t1 = CONNECTORS.jointPatternEdge.tourillon1Axis;
|
||||
const ix = CONNECTORS.jointPatternEdge.ixconnectAxis;
|
||||
const t2 = CONNECTORS.jointPatternEdge.tourillon2Axis;
|
||||
if (!horizontalPanel || dEff <= 0) {
|
||||
return {
|
||||
role: 'edge',
|
||||
tourillon1Axis: t1,
|
||||
ixconnectAxis: ix,
|
||||
tourillon2Axis: t2
|
||||
};
|
||||
}
|
||||
if (dEff >= t2) {
|
||||
return {
|
||||
role: 'edge',
|
||||
tourillon1Axis: t1,
|
||||
ixconnectAxis: ix,
|
||||
tourillon2Axis: t2
|
||||
};
|
||||
}
|
||||
const s = dEff / t2;
|
||||
return {
|
||||
role: 'edge',
|
||||
tourillon1Axis: round1(t1 * s),
|
||||
ixconnectAxis: round1(ix * s),
|
||||
tourillon2Axis: round1(t2 * s)
|
||||
};
|
||||
}
|
||||
if (dEff <= 0) {
|
||||
return {
|
||||
role: 'middle',
|
||||
tourillon1Axis: 0,
|
||||
ixconnectAxis: 0,
|
||||
tourillon2Axis: 0
|
||||
};
|
||||
}
|
||||
const ixMid = round1(dEff / 2);
|
||||
const off = CONNECTORS.jointPatternMiddleTourillonOffsetFromIxMm;
|
||||
return {
|
||||
role: 'middle',
|
||||
tourillon1Axis: round1(Math.max(0, ixMid - off)),
|
||||
ixconnectAxis: ixMid,
|
||||
tourillon2Axis: round1(Math.min(dEff, ixMid + off))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Largeurs de colonnes (mm) pour placer les jonctions — ordre: modules API, puis blocks, puis config.modules.
|
||||
* Évite le cas « modules = [] » alors que `config.modules` contient les largeurs (une seule jonction / mauvais comptage).
|
||||
*/
|
||||
function getModuleWidthsForJunctions(modules = [], config = {}) {
|
||||
const fromArr = (arr) =>
|
||||
(Array.isArray(arr) ? arr : [])
|
||||
.map((m) => Number(m?.widthMm) || 0)
|
||||
.filter((v) => v > 0);
|
||||
let w = fromArr(modules);
|
||||
if (w.length === 0) w = fromArr(config.blocks);
|
||||
if (w.length === 0) w = fromArr(config.modules);
|
||||
return w;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions X des jonctions verticales (côtés + séparateurs) sur un panneau horizontal — mm depuis le bord gauche.
|
||||
*/
|
||||
export function getJunctionAxesAcrossWidthMm(dimensionsWidthMm, modules = [], config = {}) {
|
||||
const maxW = Math.max(0, Number(dimensionsWidthMm) || 0);
|
||||
const mWidths = getModuleWidthsForJunctions(modules, config);
|
||||
if (mWidths.length > 0) {
|
||||
let acc = 0;
|
||||
const axes = [0];
|
||||
for (let i = 0; i < mWidths.length - 1; i++) {
|
||||
acc += mWidths[i];
|
||||
axes.push(Math.max(0, Math.min(maxW, acc)));
|
||||
}
|
||||
axes.push(maxW);
|
||||
const sorted = [...axes].sort((a, b) => a - b);
|
||||
const dedup = [];
|
||||
const eps = 0.5;
|
||||
for (const v of sorted) {
|
||||
if (dedup.length === 0 || Math.abs(v - dedup[dedup.length - 1]) > eps) {
|
||||
dedup.push(Math.round(v * 100) / 100);
|
||||
}
|
||||
}
|
||||
return dedup;
|
||||
}
|
||||
return [0, maxW];
|
||||
}
|
||||
|
||||
// ── System 32 drilling ───────────────────────────────────────────────
|
||||
export const SYSTEM32 = {
|
||||
firstHoleOffset: 51, // mm — distance from panel top/bottom edge to first/last hole
|
||||
pitch: 32, // mm — distance between consecutive holes
|
||||
holeDiameter: 5, // mm — Ø 5 mm
|
||||
holeDepth: 12, // mm — profondeur de perçage (borgne, côtés de caisse)
|
||||
throughHole: false, // Côtés de caisse: perçages borgnes (non traversants)
|
||||
};
|
||||
|
||||
// ── System 32 for vertical separators ────────────────────────────────
|
||||
export const SYSTEM32_SEPARATORS = {
|
||||
firstHoleOffset: 32, // mm — 51mm - 19mm (fond) = 32mm from bottom edge
|
||||
pitch: 32, // mm — same pitch
|
||||
holeDiameter: 5, // mm — Ø 5 mm
|
||||
throughHole: true, // Séparations verticales: Ø5mm TRAVERSANTS
|
||||
};
|
||||
|
||||
// ── Hinge positioning (Blum CLIP top) ────────────────────────────────
|
||||
export const HINGE = {
|
||||
axisOffset: 103, // mm — per APEX 4.0 reversibility rule
|
||||
axisFrom: 'top_and_bottom',
|
||||
perDoor: 3, // default, real count uses matrix below
|
||||
reversible: true, // Panels are reversible (symmetric S32 drilling)
|
||||
model: 'Blum CLIP top',
|
||||
/** Per hinge cup: two Ø8 bores (Blum drilling pattern), depth mm, spacing mm (center–center), offset mm from cup center toward door interior (⟂ hinge edge). */
|
||||
cupScrewDiameter: 8,
|
||||
cupScrewDepth: 13,
|
||||
cupScrewSpacing: 45,
|
||||
cupScrewOffsetFromCupCenter: 9.5,
|
||||
};
|
||||
|
||||
// Height/weight matrix from APEX 4.0 section 6.B
|
||||
export const HINGE_MATRIX = [
|
||||
{ maxHeightMm: 900, maxWeightKg: 6, hinges: 2 },
|
||||
{ maxHeightMm: 1600, maxWeightKg: 17, hinges: 3 },
|
||||
{ maxHeightMm: 2000, maxWeightKg: 20, hinges: 4 },
|
||||
{ maxHeightMm: 2400, maxWeightKg: 22, hinges: 5 },
|
||||
{ maxHeightMm: 2600, maxWeightKg: Infinity, hinges: 6 }
|
||||
];
|
||||
|
||||
export function getHingeCountForDoor({ heightMm, weightKg = null }) {
|
||||
const h = Number(heightMm) || 0;
|
||||
const hasWeight = weightKg !== null && weightKg !== undefined && weightKg !== '';
|
||||
const w = hasWeight && Number.isFinite(Number(weightKg)) ? Number(weightKg) : null;
|
||||
for (const row of HINGE_MATRIX) {
|
||||
const byHeight = h <= row.maxHeightMm;
|
||||
const byWeight = w == null ? false : w <= row.maxWeightKg;
|
||||
if (byHeight || byWeight) return row.hinges;
|
||||
}
|
||||
return HINGE_MATRIX[HINGE_MATRIX.length - 1].hinges;
|
||||
}
|
||||
|
||||
export function getHingeCupPositions(doorHeightMm, hingeCount = null) {
|
||||
const height = Number(doorHeightMm) || 0;
|
||||
const count = Math.max(2, Number(hingeCount) || getHingeCountForDoor({ heightMm: height }));
|
||||
const first = HINGE.axisOffset;
|
||||
const last = Math.max(first, height - HINGE.axisOffset);
|
||||
|
||||
if (count <= 2) return [first, last];
|
||||
|
||||
const spaces = count - 1;
|
||||
const rawStep = (last - first) / spaces;
|
||||
const positions = [first];
|
||||
|
||||
for (let i = 1; i < spaces; i++) {
|
||||
const rawPos = first + rawStep * i;
|
||||
// Rule: align intermediary hinges on System 32 grid.
|
||||
const snapped = Math.round(rawPos / 32) * 32;
|
||||
const minPos = positions[positions.length - 1] + 32;
|
||||
const maxPos = last - (spaces - i) * 32;
|
||||
positions.push(Math.min(Math.max(snapped, minPos), maxPos));
|
||||
}
|
||||
|
||||
positions.push(last);
|
||||
return positions;
|
||||
}
|
||||
|
||||
// ── Plinth system (AXILO™ 78) ───────────────────────────────────────
|
||||
export const PLINTH = {
|
||||
model: 'AXILO 78',
|
||||
footHeight: 78, // mm — hauteur pied réglable
|
||||
clipsPerMeter: 3, // clips de plinthe par mètre linéaire
|
||||
// Reserved packing space for hardware kit
|
||||
packingReserved: true,
|
||||
};
|
||||
|
||||
// ── Reinforcement machining (Renforts Forteresse) ────────────────────
|
||||
export const REINFORCEMENT_MACHINING = {
|
||||
// Sur chaque petit chant d'extrémité: 1× Ixconnect SC 8/60 au centre + 2× tourillons à 20 mm des bords
|
||||
connectorModel: 'SC 8/60',
|
||||
connector: {
|
||||
model: 'SC 8/60',
|
||||
edgeHole: { diameter: 8, depth: 45 },
|
||||
faceHole: { diameter: 8, depth: 12 },
|
||||
},
|
||||
ixconnectCenter: true,
|
||||
tourillonCount: 2,
|
||||
tourillonEdgeOffset: 20, // mm — tourillons à 20 mm des bords (axe mesuré depuis le même repère que l’Ixconnect)
|
||||
tourillonDiameter: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* Axes de perçage sur le petit chant d'extrémité (mm depuis le bord haut du renfort).
|
||||
* @param {number} reinforcementFaceHeightMm — hauteur de la face de renfort (généralement 100 mm)
|
||||
*/
|
||||
export function getReinforcementEndFixingAxesFromTopMm(reinforcementFaceHeightMm) {
|
||||
const h = Math.max(0, Number(reinforcementFaceHeightMm) || 0);
|
||||
const off = REINFORCEMENT_MACHINING.tourillonEdgeOffset;
|
||||
const round1 = (v) => Math.round(v * 10) / 10;
|
||||
return {
|
||||
// Deux tourillons proches des bords haut/bas de la face 100mm.
|
||||
tourillonAxesFromTopMm: [round1(off), round1(Math.max(off, h - off))],
|
||||
ixconnectAxisFromTopMm: round1(h / 2),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Niche and sanitary gaps ─────────────────────────────────────────────
|
||||
export const NICHE = {
|
||||
sanitaryGapAround: 30, // mm — vide sanitaire de 3 cm tout autour
|
||||
drawerBackGap: 10, // mm — vide sanitaire derrière tiroir
|
||||
};
|
||||
|
||||
// ── Drawers (manufactured) ──────────────────────────────────────────────
|
||||
export const DRAWERS = {
|
||||
maxColumnWidth: 1000, // mm
|
||||
minDepth: 400, // mm
|
||||
maxDepth: 650, // mm
|
||||
depthStep: 50, // mm
|
||||
hiddenHandleDepthReduction: 40, // mm (assumption until final handle specs)
|
||||
standardHeights: [60, 90, 128, 177, 240],
|
||||
};
|
||||
|
||||
/**
|
||||
* Renforts latéraux caisse (25 mm) — uniquement si tiroirs cachés derrière porte ;
|
||||
* un montant gauche + un droit par colonne à tiroirs, même profondeur (min. des deux parois).
|
||||
* Fixation sur la face intérieure des montants / séparateurs.
|
||||
*/
|
||||
export const DRAWER_REINFORCEMENT = {
|
||||
thicknessMm: 25,
|
||||
/** mm depuis le fond caisse (repère rainure côté ou fond séparateur) — début utile du renfort */
|
||||
backInsetFromRearMm: 25,
|
||||
/** mm réservés vers l’avant quand les tiroirs sont derrière une porte à charnières */
|
||||
hingeFrontClearanceMm: 77,
|
||||
/** mm — déduit de la hauteur pile : partie droite au-dessus du rayon façade tiroir du haut */
|
||||
topFrontRadiusMm: 3,
|
||||
/** Ixconnect « petit » — même famille que finitions SC 8/25 */
|
||||
ixconnectModel: 'SC 8/25',
|
||||
/** mm — retrait des axes depuis chaque bord (chaque coin : 50 mm des deux côtés) */
|
||||
ixconnectCornerInsetMm: 50,
|
||||
ixconnectHoleDiameterMm: 8,
|
||||
/** Perçage face côté renfort (vers montant / séparateur) */
|
||||
ixconnectDepthInReinforcementMm: 13,
|
||||
/** Perçage face intérieure montant ou séparateur (vis-à-vis) */
|
||||
ixconnectDepthInVerticalPanelMm: 12,
|
||||
};
|
||||
|
||||
/**
|
||||
* Ixconnect SC 8/25 sur montants / séparateurs — zone tiroirs cachés + renfort.
|
||||
* Repère face intérieure : X depuis bord avant (profondeur), Y depuis bas du panneau (corps).
|
||||
*/
|
||||
export const DRAWER_RENFORT_PANEL_IXCONNECT = {
|
||||
model: 'SC 8/25',
|
||||
/** 77 + 50 mm depuis le devant (axe avant) */
|
||||
frontAxesFromFrontMm: 77 + 50,
|
||||
/** Côté caisse : 19 + 9 + 50 mm depuis le fond */
|
||||
backAxesFromRearCoteMm: 19 + 9 + 50,
|
||||
/** Séparateur : 50 mm depuis le fond */
|
||||
backAxesFromRearSeparatorMm: 50,
|
||||
/** 50 mm du haut / bas de la zone renfort */
|
||||
verticalInsetMm: 50,
|
||||
faceHoleDiameterMm: 8,
|
||||
faceHoleDepthMm: 12
|
||||
};
|
||||
|
||||
/**
|
||||
* 4 trous (rectangle) sur une face intérieure montant/séparateur pour renfort tiroir caché.
|
||||
* @param {number} panelDepthMm — largeur du panneau en plan (= profondeur caisse ou prof. séparateur)
|
||||
* @param {boolean} isCote — true = montant extérieur, false = séparateur
|
||||
* @param {number} zoneBottomFromPanelBottomMm — bas de la zone tiroirs depuis bas du panneau
|
||||
* @param {number} renfortHeightMm — hauteur utile renfort (= pile tiroirs − R)
|
||||
*/
|
||||
export function buildDrawerRenfortPanelIxconnectCornersMm(
|
||||
panelDepthMm,
|
||||
isCote,
|
||||
zoneBottomFromPanelBottomMm,
|
||||
renfortHeightMm
|
||||
) {
|
||||
const P = DRAWER_RENFORT_PANEL_IXCONNECT;
|
||||
const D = Math.max(0, Number(panelDepthMm) || 0);
|
||||
const z0 = Math.max(0, Number(zoneBottomFromPanelBottomMm) || 0);
|
||||
const h = Math.max(0, Number(renfortHeightMm) || 0);
|
||||
const backInset = isCote ? P.backAxesFromRearCoteMm : P.backAxesFromRearSeparatorMm;
|
||||
const xf = P.frontAxesFromFrontMm;
|
||||
const xb = Math.max(xf + 1, D - backInset);
|
||||
const y1 = z0 + P.verticalInsetMm;
|
||||
const y2 = z0 + h - P.verticalInsetMm;
|
||||
if (h < 2 * P.verticalInsetMm + 5 || D < xf + backInset + 5 || y2 <= y1) return [];
|
||||
return [
|
||||
{ xMm: xf, yMm: y1, role: 'avant_bas' },
|
||||
{ xMm: xf, yMm: y2, role: 'avant_haut' },
|
||||
{ xMm: xb, yMm: y1, role: 'fond_bas' },
|
||||
{ xMm: xb, yMm: y2, role: 'fond_haut' }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 4 trous d’angle pour Ixconnect SC 8/25 — repère plan face : X depuis bord gauche, Y depuis bord haut (mm).
|
||||
* @returns {{ xMm: number, yMm: number, role: string }[]}
|
||||
*/
|
||||
export function getDrawerReinforcementIxconnectHolePositionsMm(widthMm, heightMm, insetMm = DRAWER_REINFORCEMENT.ixconnectCornerInsetMm) {
|
||||
const w = Math.max(0, Number(widthMm) || 0);
|
||||
const h = Math.max(0, Number(heightMm) || 0);
|
||||
const ins = Math.max(8, Math.min(Number(insetMm) || 50, Math.min(w, h) / 2 - 0.5));
|
||||
if (w < 2 * ins || h < 2 * ins) return [];
|
||||
return [
|
||||
{ xMm: ins, yMm: ins, role: 'nw' },
|
||||
{ xMm: w - ins, yMm: ins, role: 'ne' },
|
||||
{ xMm: ins, yMm: h - ins, role: 'sw' },
|
||||
{ xMm: w - ins, yMm: h - ins, role: 'se' }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauteur totale du bloc tiroirs (façades + jeux), mm.
|
||||
* @param {number} drawerCount
|
||||
* @param {number[]} drawerHeightsMm
|
||||
* @param {number} [drawerGapMm]
|
||||
*/
|
||||
export function drawerStackHeightMm(drawerCount, drawerHeightsMm, drawerGapMm = 8) {
|
||||
const n = Math.max(0, Number(drawerCount) || 0);
|
||||
if (n <= 0) return 0;
|
||||
const heights = Array.isArray(drawerHeightsMm) && drawerHeightsMm.length ? drawerHeightsMm : DRAWERS.standardHeights;
|
||||
const gap = Number.isFinite(Number(drawerGapMm)) ? Number(drawerGapMm) : 8;
|
||||
const sum = heights
|
||||
.slice(0, Math.min(n, heights.length))
|
||||
.reduce((s, v) => s + (Number(v) || 0), 0);
|
||||
return Math.max(0, Math.round(sum + Math.max(0, n - 1) * gap));
|
||||
}
|
||||
|
||||
// ── Full-height handle assumptions ──────────────────────────────────────
|
||||
export const HANDLE = {
|
||||
/** Retrait mécanique rainure poignée (haut + bas) */
|
||||
fullHeightTopBottomOffset: 2, // mm from top and bottom
|
||||
/** Déduction largeur panneau porte (vs poignée standard) — même valeur liste de débitage / plans */
|
||||
fullHeightWidthDeductionMm: 18,
|
||||
/** Rainure usinée sur le montant côté poignée (profil type dos, à confirmer au profilé) */
|
||||
fullHeightGroove: {
|
||||
width: GROOVE.back.width,
|
||||
depth: GROOVE.back.depth,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Côté charnières en élévation pour la porte d’index `doorIndex` :
|
||||
* vantail extérieur gauche → charnières gauche ; droit → droite ; portes centrales → réglage utilisateur.
|
||||
*/
|
||||
export function getDoorHingeSide(doorIndex, doorCount, hardware = {}) {
|
||||
const hw = hardware || {};
|
||||
const n = Math.max(1, Number(doorCount) || 1);
|
||||
const i = Math.max(0, Number(doorIndex) || 0);
|
||||
if (n <= 1) return hw.doorHingeSide === 'right' ? 'right' : 'left';
|
||||
if (i === 0) return 'left';
|
||||
if (i === n - 1) return 'right';
|
||||
return hw.centerDoorHingeSide === 'right' ? 'right' : 'left';
|
||||
}
|
||||
|
||||
// ── Block assembly drilling ─────────────────────────────────────────────
|
||||
export const BLOCK_ASSEMBLY = {
|
||||
sideOffsetFromFace: 30, // mm
|
||||
sideOffsetFromBack: 30, // mm
|
||||
topBottomOffset: 100, // mm
|
||||
middleCount: 3, // per joint
|
||||
};
|
||||
|
||||
// ── Carcass assembly ferrures ───────────────────────────────────────────
|
||||
export const CARCASS_FASTENERS = {
|
||||
/** Ferrure d'assemblage utilisée pour les jonctions de caisse (côtés/séparateurs ↔ dessus/fond). */
|
||||
model: 'Häfele Rasant-Tab',
|
||||
/** Référence produit (Ferrures d’assemblage Rasant-Tab, Ø trou 3 ou 5 mm). */
|
||||
productUrl: 'https://www.haefele.ch/fr/produkt/ferrures-d-assemblage-rasant-tab-visser-dans-trou-de-per-age-3-ou-5-mm/26236520/?MasterSKU=P-00861456',
|
||||
piecesPerJoint: 3,
|
||||
/** Retrait des ferrures d’extrémité (mm) */
|
||||
endInsetMm: 50,
|
||||
edgeOffsets: [50, 'middle', 50],
|
||||
/** Perçage ferrure: profondeur sur longueur de la pièce (mm) */
|
||||
ferrureDepthAlongLengthMm: 46,
|
||||
/** Perçage ferrure: profondeur sur l’épaisseur de la pièce (mm) */
|
||||
ferrureDepthInThicknessMm: 12,
|
||||
dowel: {
|
||||
diameter: 8,
|
||||
/** Longueur nominale tourillon (réf. atelier) */
|
||||
depth: 40,
|
||||
/** Perçage borgne en face (dans l’épaisseur du panneau), mm — toujours 12 pour dessus/fond */
|
||||
faceDrillDepthMm: 12,
|
||||
/** 2× tourillons par ferrure : axes à ±sideOffset du centre de chaque ferrure (mm) */
|
||||
sideOffset: 30,
|
||||
},
|
||||
};
|
||||
|
||||
/** Profondeur effective perçage tourillon en face (ne dépasse pas l’épaisseur). */
|
||||
export function tourillonFaceDrillDepthMm(thicknessMm) {
|
||||
const cap = CARCASS_FASTENERS.dowel.faceDrillDepthMm;
|
||||
const t = Math.max(1, Number(thicknessMm) || PANEL.structure);
|
||||
return Math.min(cap, Math.max(6, t - 2));
|
||||
}
|
||||
|
||||
/** Longueur mini. d’arête (mm) pour placer une ferrure au milieu (en plus des 50 mm d’extrémité). */
|
||||
export const CARCASS_MIDDLE_FERRURE_MIN_EDGE_MM = 500;
|
||||
|
||||
/**
|
||||
* Centres des ferrures le long d’une arête de jonction.
|
||||
* Règle: 50 mm des extrémités ; ferrure centrale seulement si l’arête dépasse {@link CARCASS_MIDDLE_FERRURE_MIN_EDGE_MM}.
|
||||
* @param {number} edgeLengthMm — longueur de l’arête (mm), ici hauteur utile de la jonction
|
||||
* @returns {number[]} distances depuis le début de l’arête (mm)
|
||||
*/
|
||||
export function getCarcassJointPositions(edgeLengthMm) {
|
||||
const L = Math.max(0, Number(edgeLengthMm) || 0);
|
||||
if (L <= 0) return [];
|
||||
const inset = Math.min(CARCASS_FASTENERS.endInsetMm, Math.max(20, L * 0.1));
|
||||
const tail = L - inset;
|
||||
const minMid = CARCASS_MIDDLE_FERRURE_MIN_EDGE_MM;
|
||||
|
||||
if (tail <= inset + 20) {
|
||||
if (L > minMid) {
|
||||
const u = L / 4;
|
||||
return [u, L / 2, L - u].map((v) => Math.round(Math.min(L, Math.max(0, v)) * 10) / 10);
|
||||
}
|
||||
const u = L / 4;
|
||||
return [u, L - u].map((v) => Math.round(Math.min(L, Math.max(0, v)) * 10) / 10);
|
||||
}
|
||||
|
||||
if (L > minMid) {
|
||||
return [inset, L / 2, tail].map((v) => Math.round(v * 10) / 10);
|
||||
}
|
||||
return [inset, tail].map((v) => Math.round(v * 10) / 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chaque ferrure: 3 perçages alignés sur la longueur:
|
||||
* - 1 perçage ferrure au centre
|
||||
* - 2 perçages tourillons à ±sideOffset du centre
|
||||
* @returns {{ centerMm: number, holesAlongMm: [number, number, number] }[]}
|
||||
*/
|
||||
export function getCarcassFerrureDowelDrillings(centerPositionsMm, edgeLengthMm, sideOffsetMm = CARCASS_FASTENERS.dowel.sideOffset) {
|
||||
const L = Math.max(0, Number(edgeLengthMm) || 0);
|
||||
const off = Math.max(0, Number(sideOffsetMm) || 0);
|
||||
const centers = centerPositionsMm || [];
|
||||
const clamp = (v) => Math.round(Math.min(L, Math.max(0, v)) * 10) / 10;
|
||||
return centers.map((c0) => {
|
||||
const c = Number(c0);
|
||||
return { centerMm: c, holesAlongMm: [clamp(c - off), clamp(c), clamp(c + off)] };
|
||||
});
|
||||
}
|
||||
|
||||
// ── Shelf machining ─────────────────────────────────────────────────
|
||||
export const SHELF_MACHINING = {
|
||||
// Tourillons Ø8 front + back for shelf pin alignment
|
||||
tourillonDiameter: 8, // mm
|
||||
tourillonCount: 4, // 2 front + 2 back per shelf
|
||||
// "2 rayons sur une même strate de 19mm" — 2 shelves can share same S32 row
|
||||
shelvesPerStratum: 2,
|
||||
};
|
||||
|
||||
// ── Packing ──────────────────────────────────────────────────────────
|
||||
export const PACKING = {
|
||||
maxWeightKg: 28,
|
||||
// Approximate density for weight estimation (kg per m³)
|
||||
densityKgM3: {
|
||||
melamine19: 680, // Panneau mélaminé 19 mm
|
||||
melamine16: 660, // Panneau mélaminé 16 mm (renforts)
|
||||
melamine25: 670, // Panneau mélaminé 25 mm (renforts tiroir)
|
||||
melamine8: 640, // Panneau mélaminé 8 mm (dos)
|
||||
},
|
||||
// Layer-based packing strategy (per APEX 4.0 spec):
|
||||
layers: {
|
||||
level1: 'sides', // Niveau 1: Côtés empilés
|
||||
level2: 'topAndBottom', // Niveau 2: Dessus + fond côte à côte
|
||||
level3: 'shelvesAndRenforts', // Niveau 3: Étagères + renforts groupés par épaisseur
|
||||
},
|
||||
// Hardware kit reservation (AXILO™ pieds + clips de plinthe)
|
||||
hardwareKitReserved: true, // Espace réservé dans le colisage
|
||||
};
|
||||
|
||||
// ── Convenience: convert mm constants to metres (for Three.js) ──────
|
||||
export const PANEL_M = {
|
||||
structure: PANEL.structure / 1000,
|
||||
reinforcement: PANEL.reinforcement / 1000,
|
||||
back: PANEL.back / 1000,
|
||||
reinforcementWidth: PANEL.reinforcementWidth / 1000,
|
||||
};
|
||||
Reference in New Issue
Block a user