@@ -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