Co-authored-by: Cursor <[email protected]>
This commit is contained in:
lejay
2026-05-28 07:20:38 +02:00
co-authored by Cursor
parent 6923c319a1
commit 6c0d7e47b8
81 changed files with 21780 additions and 2986 deletions
+601
View File
@@ -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,
};