Files
apex/shared/manufacturing.js
T
lejayandCursor 6c0d7e47b8 v2.0
Co-authored-by: Cursor <[email protected]>
2026-05-28 07:20:38 +02:00

602 lines
25 KiB
JavaScript
Raw Blame History

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