/** * 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; }