import { LIMITS } from "@/config/business"; import type { CustomizerModel, CustomizerParameter, ParameterValues } from "./catalog"; const number = (key: string, label: string, value: number, min: number, max: number, step = .1): CustomizerParameter => ({ key, label, default: value, min, max, step, unit: "mm", kind: "number" }); const parameters: CustomizerParameter[] = [ number("case_width", "Measured case width", 90, 25, 230, .01), number("case_height", "Measured case height", 32, 10, 130, .01), number("screw_pitch", "Mounting screw center spacing", 130, 45, 260, .01), number("top_gap", "Gap above case", 6, 2, 25), number("saddle_width", "Width along case length", 18, 12, 40), number("clearance", "Total case width clearance", .8, .3, 2, .05), number("screw_diameter", "Screw clearance diameter", 4.5, 3, 6.5), number("head_diameter", "Screw head or washer diameter", 9, 6, 14), number("wall", "Side wall thickness", 4, 3.2, 8), number("bridge", "Bottom bridge thickness", 4, 3.2, 8), number("ear", "Mounting ear thickness", 4, 3.2, 8), ]; export const ELECTRONICS_SADDLE: CustomizerModel = { id: "electronics_saddle", slug: "custom-electronics-mounting-saddles", name: "Custom electronics mounting saddles", description: "Fit an unusual enclosed power brick, hub or small electronics case to a flat underside using measured case dimensions and existing screw spacing. One pair of open-ended PETG saddles per quantity. Position them along the case to clear vents and ports. Hardware and a separate retaining strap are required and not included. Physical print, fit, heat and loaded-use testing are pending; no load rating or equipment-fit claim.", sourcePath: "src/lib/customizer/cad/electronics-saddle.ts", sourceUrl: "https://3dprinterverse.com/designs/electronics-saddle", 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-electronics-mounting-saddles.stl", imagePath: "/images/galaxy/custom-electronics-mounting-saddles.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: "medium-brick", label: "90 × 32 mm case · 130 mm screw pitch", parameters: {}, description: "Measured example only. Check your enclosure, flat mounting surface, vents and all four screw positions." }, { id: "small-hub", label: "45 × 18 mm case · 84 mm screw pitch", parameters: { case_width: 45, case_height: 18, screw_pitch: 84, top_gap: 4, saddle_width: 16, screw_diameter: 3.5, head_diameter: 7 } }, { id: "wide-dock", label: "125 × 38 mm case · 168 mm screw pitch", parameters: { case_width: 125, case_height: 38, screw_pitch: 168, top_gap: 10, saddle_width: 22 } }, { id: "tall-case", label: "70 × 62 mm case · 114 mm screw pitch", parameters: { case_width: 70, case_height: 62, screw_pitch: 114, top_gap: 8, saddle_width: 24, wall: 5, bridge: 5, ear: 5 } }, ], }; export function saddleValues(input: ParameterValues) { const values: Record = {}; for (const p of parameters) { const value = input[p.key] ?? p.default; if (typeof value !== "number" || !Number.isFinite(value) || value < p.min || value > p.max) throw new Error(`Choose supported ${p.label.toLowerCase()}.`); values[p.key] = value; } return values as Record<"case_width" | "case_height" | "screw_pitch" | "top_gap" | "saddle_width" | "clearance" | "screw_diameter" | "head_diameter" | "wall" | "bridge" | "ear", number>; } export type SaddlePoint = [number, number]; export const SADDLE_PRINT_GAP = 4; export function saddleDimensions(input: ParameterValues) { const p = saddleValues(input), insideWidth = p.case_width + p.clearance; const outsideWidth = insideWidth + 2 * p.wall, brace = p.wall * 1.5; const height = p.bridge + p.case_height + p.top_gap; const edgeMargin = p.head_diameter / 2 + 3, width = p.screw_pitch + 2 * edgeMargin; return { insideWidth, outsideWidth, brace, height, width, edgeMargin, insideHeight: p.case_height + p.top_gap, minimumCaseLength: 2 * p.saddle_width + 4, bbox: { x: width, y: 2 * height + SADDLE_PRINT_GAP, z: p.saddle_width } }; } /** Cross-section in the installed width/drop plane. Printed on this flat face, * the U, ears and 45° external braces all lie in continuous extrusion layers. */ export function saddleOutline(input: ParameterValues): SaddlePoint[] { const p = saddleValues(input), d = saddleDimensions(input); const i = d.insideWidth / 2, o = d.outsideWidth / 2, e = d.width / 2, h = d.height, b = h - p.ear; return [[-o, 0], [o, 0], [o, b - d.brace], [o + d.brace, b], [e, b], [e, h], [i, h], [i, p.bridge], [-i, p.bridge], [-i, h], [-e, h], [-e, b], [-o - d.brace, b], [-o, b - d.brace]]; } /** A circular screw clearance with a pointed 45° roof in print Z. The faceted * arc circumscribes the requested circle; a round shank keeps its full clearance. */ export function saddleScrewOutline(input: ParameterValues): SaddlePoint[] { const p = saddleValues(input), r = p.screw_diameter / (2 * Math.cos(Math.PI / 64)); return [[0, r * Math.SQRT2], ...Array.from({ length: 49 }, (_, index): SaddlePoint => { const angle = (135 + index * 270 / 48) * Math.PI / 180; return [r * Math.cos(angle), r * Math.sin(angle)]; })]; } export function saddlePolygonMetrics(points: SaddlePoint[]) { let twiceArea = 0, perimeter = 0; points.forEach((a, i) => { const b = points[(i + 1) % points.length]; twiceArea += a[0] * b[1] - b[0] * a[1]; perimeter += Math.hypot(b[0] - a[0], b[1] - a[1]); }); return { area: Math.abs(twiceArea) / 2, perimeter }; } export function validateSaddle(input: ParameterValues) { const p = saddleValues(input), d = saddleDimensions(input); if (Object.values(d.bbox).some(v => v > LIMITS.maxDimensionMm + 1e-6)) throw new Error(`The complete two-saddle print layout exceeds the ${LIMITS.maxDimensionMm} mm usable area on the Bambu A1’s 256 mm bed. Reduce screw spacing, case height or the gap above it.`); if (p.screw_pitch < d.outsideWidth + 2 * d.brace + p.head_diameter + 4 - 1e-7) throw new Error("Move screw centers farther apart to leave 2 mm of straight head/washer access beyond the ear braces. Check the measured case width and screw spacing."); if ((p.screw_pitch - d.outsideWidth) / 2 > 25 + 1e-7) throw new Error("Keep each screw center within 25 mm of its upright outside face. Reduce screw spacing or increase the measured case width to avoid long unsupported mounting ears."); if (p.head_diameter < p.screw_diameter * Math.SQRT2 / Math.cos(Math.PI / 64) + 1 - 1e-7) throw new Error("Use a wider screw head or washer to cover the pointed screw hole with at least 0.5 mm overlap."); if (p.saddle_width < p.head_diameter + 6 - 1e-7) throw new Error("Saddle width along the case must exceed the head/washer diameter by at least 6 mm."); if (d.height - p.ear - d.brace < p.bridge + 2 - 1e-7) throw new Error("Leave at least 2 mm of upright below the ear braces. Increase case height or top gap, or reduce wall and ear thickness."); } /** Both complete extrusions, less four screw passages. All passages stay within * the flat ears, so the removed faces and added bore surfaces are exact. */ export function saddleMetrics(input: ParameterValues) { const p = saddleValues(input), d = saddleDimensions(input); const body = saddlePolygonMetrics(saddleOutline(input)), hole = saddlePolygonMetrics(saddleScrewOutline(input)); return { volumeMm3: 2 * (body.area * p.saddle_width - 2 * hole.area * p.ear), surfaceAreaMm2: 2 * (2 * body.area + body.perimeter * p.saddle_width + 2 * (hole.perimeter * p.ear - 2 * hole.area)), bbox: d.bbox }; } export function saddlePrintability(input: ParameterValues) { return { fits: Object.values(saddleDimensions(input).bbox).every(v => v <= LIMITS.maxDimensionMm + 1e-6), instruction: "Print both saddles on their broad U-shaped side faces as shown, 4 mm apart. PETG, 100% infill, 0.4 mm nozzle; pointed screw-hole roofs need no supports.", notes: "One pair per quantity. Four pan-head screws/flat washers and a separate retaining strap are required, not included. Stationary indoor, fully enclosed equipment only; keep vents clear and follow its mounting/temperature guidance. Physical print, fit, heat and loaded-use tests are pending; no load rating." }; }