import type { CustomizerModel, CustomizerParameter, ParameterValues } from "./catalog"; import { LIMITS } from "@/config/business"; const number = (key: string, label: string, value: number, min: number, max: number): CustomizerParameter => ({ key, label, default: value, min, max, step: .01, unit: "mm", kind: "number" }); const list = (key: string, label: string, values: number[]): CustomizerParameter => ({ key, label, default: values, min: 0, max: 260, step: .01, unit: "mm", kind: "list", minItems: 2, maxItems: 6 }); const parameters = [ number("plate_width", "Finished plate width", 120, 35, 260), number("plate_length", "Finished plate length", 95, 35, 260), number("board_width", "Measured circuit-board width", 72, 15, 240), number("board_length", "Measured circuit-board length", 50, 15, 240), number("board_x", "Board left edge from plate left", 24, 0, 240), number("board_y", "Board bottom edge from plate bottom", 22.5, 0, 240), list("board_hole_x", "Board hole X centers from board left", [4, 68, 68, 4]), list("board_hole_y", "Board hole Y centers from board bottom", [4, 4, 46, 46]), list("case_hole_x", "Enclosure hole X centers from plate left", [8, 112, 112, 8]), list("case_hole_y", "Enclosure hole Y centers from plate bottom", [8, 8, 87, 87]), number("standoff_height", "Board underside above plate top", 8, 4, 35), number("component_drop", "Components or solder below board", 4, 0, 30), number("plate_thickness", "Plate thickness", 3.2, 2.4, 8), number("corner_radius", "Plate outside corner radius", 4, 0, 20), number("boss_diameter", "Printed support outside diameter", 8, 6, 16), number("board_bore", "Board bolt clearance diameter", 3.2, 2.2, 4.5), number("board_head", "Underside countersunk bolt head diameter", 6.2, 4, 9), number("case_bore", "Enclosure screw clearance diameter", 4.5, 2.5, 6.5), number("case_head", "Enclosure screw head or washer diameter", 9, 5, 14), ]; export const PCB_ADAPTER: CustomizerModel = { id: "pcb_mounting_adapter", slug: "custom-pcb-mounting-adapter", name: "Custom PCB mounting adapter", description: "Match a small circuit board to an existing enclosure with a different screw pattern. Enter independent board and enclosure hole centers, board placement and underside clearance. One PETG plate with raised supports and recessed underside bolt seats per quantity. Bolts, nuts, washers, board and enclosure are separate. Indoor low-voltage hobby electronics only. Physical print and fit testing are pending; no brand compatibility, load, thermal, electrical or fire rating.", sourcePath: "src/lib/customizer/cad/pcb-mounting-adapter.ts", sourceUrl: "https://3dprinterverse.com/designs/pcb-mounting-adapter", geometryMode: "browser-csg", sourceKind: "store-original", family: "measured-electronics-mounts", kind: "print", parameters, defaults: Object.fromEntries(parameters.map(p => [p.key, p.default])), stlPath: "/models/parametric/custom-pcb-mounting-adapter.stl", imagePath: "/images/galaxy/custom-pcb-mounting-adapter.webp", volumeMm3: 1, surfaceAreaMm2: 1, bbox: { x: 1, y: 1, z: 1 }, printable: true, physicalPrintTested: false, materials: ["PETG"], infillPct: 100, priceCentsByMaterial: { PETG: 1600 }, presets: [ { id: "four-corner", label: "72 × 50 mm board · four corner holes", parameters: {}, description: "Example measurements only. Check both patterns and the enclosure's usable floor space." }, { id: "three-hole", label: "Triangular three-hole board · two case screws", parameters: { plate_width: 100, plate_length: 85, board_width: 55, board_length: 42, board_x: 22.5, board_y: 21.5, board_hole_x: [5, 50, 27.5], board_hole_y: [5, 5, 37], case_hole_x: [8, 92], case_hole_y: [42.5, 42.5], standoff_height: 7, component_drop: 3 } }, { id: "offset-board", label: "Offset 58 × 36 mm board · staggered case holes", parameters: { plate_width: 115, plate_length: 80, board_width: 58, board_length: 36, board_x: 16, board_y: 24, board_hole_x: [4, 54, 54, 4], board_hole_y: [4, 4, 32, 32], case_hole_x: [8, 104, 94], case_hole_y: [8, 22, 70], standoff_height: 10, component_drop: 6 } }, { id: "six-supports", label: "135 × 85 mm board · six supports", parameters: { plate_width: 175, plate_length: 120, board_width: 135, board_length: 85, board_x: 20, board_y: 17.5, board_hole_x: [5, 67.5, 130, 130, 67.5, 5], board_hole_y: [5, 5, 5, 80, 80, 80], case_hole_x: [8, 167, 167, 8], case_hole_y: [8, 8, 112, 112], standoff_height: 12, component_drop: 8, plate_thickness: 4 } }, ], }; export function pcbValues(input: ParameterValues) { const result: ParameterValues = {}; for (const p of parameters) { const value = input[p.key] ?? p.default, values = Array.isArray(value) ? value : [value]; if ((p.kind === "list") !== Array.isArray(value) || values.length < (p.minItems ?? 1) || values.length > (p.maxItems ?? 1) || values.some(v => !Number.isFinite(v) || v < p.min || v > p.max)) throw new Error(`Choose supported ${p.label.toLowerCase()}.`); result[p.key] = Array.isArray(value) ? [...value] : value; } return result as Record<"plate_width" | "plate_length" | "board_width" | "board_length" | "board_x" | "board_y" | "standoff_height" | "component_drop" | "plate_thickness" | "corner_radius" | "boss_diameter" | "board_bore" | "board_head" | "case_bore" | "case_head", number> & Record<"board_hole_x" | "board_hole_y" | "case_hole_x" | "case_hole_y", number[]>; } export const PCB_SEGMENTS = 64; const boreRadius = (diameter: number) => diameter / (2 * Math.cos(Math.PI / PCB_SEGMENTS)); export function pcbOutline(input: ParameterValues): [number, number][] { const p = pcbValues(input), w = p.plate_width, h = p.plate_length, r = p.corner_radius; if (!r) return [[0, 0], [w, 0], [w, h], [0, h]]; return [[w - r, h - r], [r, h - r], [r, r], [w - r, r]].flatMap(([x, y], i) => Array.from({ length: 17 }, (_, j): [number, number] => { const a = (i * 90 + j * 90 / 16) * Math.PI / 180; return [x + r * Math.cos(a), y + r * Math.sin(a)]; })); } export function pcbDimensions(input: ParameterValues) { const p = pcbValues(input); return { boardHoles: p.board_hole_x.map((x, i) => ({ x: x + p.board_x, y: p.board_hole_y[i] + p.board_y, localX: x, localY: p.board_hole_y[i] })), caseHoles: p.case_hole_x.map((x, i) => ({ x, y: p.case_hole_y[i] })), bossRadius: p.boss_diameter / 2, boardRadius: boreRadius(p.board_bore), headRadius: boreRadius(p.board_head), caseRadius: boreRadius(p.case_bore), sinkDepth: boreRadius(p.board_head) - boreRadius(p.board_bore), bbox: { x: p.plate_width, y: p.plate_length, z: p.plate_thickness + p.standoff_height }, }; } export function validatePcbAdapter(input: ParameterValues) { const p = pcbValues(input), d = pcbDimensions(p), tolerance = 1e-7; if (Object.values(d.bbox).some(v => v > LIMITS.maxDimensionMm + tolerance)) throw new Error(`The complete plate exceeds the ${LIMITS.maxDimensionMm} mm usable envelope inside the Bambu A1's 256 mm bed.`); if (p.board_hole_x.length !== p.board_hole_y.length || p.case_hole_x.length !== p.case_hole_y.length) throw new Error("Pair every X center with one Y center in the same order, separately for the board and enclosure."); if (p.corner_radius > Math.min(p.plate_width, p.plate_length) / 2) throw new Error("The plate corner radius must fit inside both outside spans."); if (p.board_x < 2 || p.board_y < 2 || p.board_x + p.board_width > p.plate_width - 2 + tolerance || p.board_y + p.board_length > p.plate_length - 2 + tolerance) throw new Error("Keep the complete measured board at least 2 mm inside the plate outline's bounding edges."); if (p.standoff_height < p.component_drop + 2 - tolerance) throw new Error("Leave at least 2 mm between the lowest underside component or solder and the plate top."); if (p.board_head < p.board_bore + .8 || p.case_head < p.case_bore + 1) throw new Error("Head diameters must exceed their matching bolt/screw bores by at least 0.8 / 1 mm."); if (p.plate_thickness - d.sinkDepth < 1.2 - tolerance) throw new Error("Leave at least 1.2 mm of plate above each 90-degree underside bolt seat. Increase plate thickness or reduce the measured bolt head diameter."); if (d.bossRadius - d.boardRadius < 1.6 - tolerance) throw new Error("Leave at least 1.6 mm of support wall around each board bolt bore."); // Distance to every edge of the exact convex polygon used by the solid. const outline = pcbOutline(p); const inset = (x: number, y: number, radius: number) => outline.every((a, i) => { const b = outline[(i + 1) % outline.length], dx = b[0] - a[0], dy = b[1] - a[1]; const length = Math.hypot(dx, dy); return length < 1e-10 || (dx * (y - a[1]) - dy * (x - a[0])) / length >= radius - tolerance; }); const distance = (a: { x: number; y: number }, b: { x: number; y: number }) => Math.hypot(a.x - b.x, a.y - b.y); for (const h of d.boardHoles) { if (h.localX < p.board_bore / 2 + 1 || h.localY < p.board_bore / 2 + 1 || h.localX > p.board_width - p.board_bore / 2 - 1 || h.localY > p.board_length - p.board_bore / 2 - 1) throw new Error("Board holes must lie inside the measured board with at least 1 mm to its edge beyond the bolt bore."); if (!inset(h.x, h.y, Math.max(d.bossRadius, d.headRadius) + 2)) throw new Error("Keep every support and underside bolt seat at least 2 mm inside the actual rounded plate edge."); } const boardKeepoutDistance = (h: { x: number; y: number }) => Math.hypot(Math.max(p.board_x - h.x, 0, h.x - p.board_x - p.board_width), Math.max(p.board_y - h.y, 0, h.y - p.board_y - p.board_length)); for (const h of d.caseHoles) { if (!inset(h.x, h.y, Math.max(d.caseRadius + 2.4, p.case_head / 2 + 2))) throw new Error("Keep enclosure screw bores and heads clear of the actual rounded plate edge."); if (boardKeepoutDistance(h) < p.case_head / 2 + 2 - tolerance) throw new Error("Enclosure screw heads need 2 mm of tool access beyond the complete board footprint. Move the board or choose a larger plate."); for (const b of d.boardHoles) if (distance(h, b) < Math.max(d.bossRadius, d.headRadius) + Math.max(d.caseRadius, p.case_head / 2) + 2 - tolerance) throw new Error("Keep enclosure screw heads and bores 2 mm away from every board support and bolt seat."); } for (let i = 0; i < d.boardHoles.length; i++) for (const b of d.boardHoles.slice(i + 1)) if (distance(d.boardHoles[i], b) < 2 * Math.max(d.bossRadius, d.headRadius) + 2 - tolerance) throw new Error("Separate board supports and underside bolt seats by at least 2 mm; remove duplicate centers."); for (let i = 0; i < d.caseHoles.length; i++) for (const b of d.caseHoles.slice(i + 1)) if (distance(d.caseHoles[i], b) < p.case_head + 2 - tolerance) throw new Error("Separate enclosure screw heads by at least 2 mm; remove duplicate centers."); } /** Exact material metrics for the same 64-sided bores/supports and planar * countersinks as the mesh. Intersections and merged bosses are rejected. */ export function pcbMetrics(input: ParameterValues) { const p = pcbValues(input), d = pcbDimensions(p), outline = pcbOutline(p), n = PCB_SEGMENTS; let twiceArea = 0, perimeter = 0; outline.forEach((a, i) => { const b = outline[(i + 1) % outline.length]; twiceArea += a[0] * b[1] - b[0] * a[1]; perimeter += Math.hypot(b[0] - a[0], b[1] - a[1]); }); const area = Math.abs(twiceArea) / 2; const disk = (r: number) => n / 2 * r * r * Math.sin(2 * Math.PI / n), rim = (r: number) => 2 * n * r * Math.sin(Math.PI / n); const boss = disk(d.bossRadius), bore = disk(d.boardRadius), head = disk(d.headRadius), caseBore = disk(d.caseRadius); const h = d.sinkDepth, frustum = h / 3 * (head + bore + Math.sqrt(head * bore)); const slant = Math.hypot(h, (d.headRadius - d.boardRadius) * Math.cos(Math.PI / n)); const slopeArea = (rim(d.headRadius) + rim(d.boardRadius)) * slant / 2; return { volumeMm3: area * p.plate_thickness + d.boardHoles.length * (boss * p.standoff_height - bore * (p.plate_thickness + p.standoff_height - h) - frustum) - d.caseHoles.length * caseBore * p.plate_thickness, surfaceAreaMm2: 2 * area + perimeter * p.plate_thickness + d.boardHoles.length * (-head - bore + rim(d.bossRadius) * p.standoff_height + rim(d.boardRadius) * (p.plate_thickness + p.standoff_height - h) + slopeArea) + d.caseHoles.length * (rim(d.caseRadius) * p.plate_thickness - 2 * caseBore), bbox: d.bbox, }; } export function pcbPrintability(input: ParameterValues) { return { fits: Object.values(pcbDimensions(input).bbox).every(v => v <= LIMITS.maxDimensionMm + 1e-7), instruction: "Print the broad plate bottom down, supports pointing up. PETG, 100% infill, 0.4 mm nozzle; vertical bores and 45-degree underside seat roofs need no supports.", notes: "One plate only; hardware separate. Assemble the board with 90-degree flat-head bolts from below, washers and nuts above while both faces are accessible, then fasten the complete assembly to the enclosure. Check flush bolt heads, component/connector clearance and enclosure lid height. Indoor low-voltage hobby boards only; physical print and fit testing are pending." }; }