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("assembled_length", "Assembled end-to-end length", 140, 60, 270), number("flange_a_diameter", "End A flange diameter", 70, 36, 130), number("flange_b_diameter", "End B flange diameter", 70, 36, 130), number("shaft_diameter", "Measured straight shaft diameter", 6, 3, 20, .01), number("running_clearance", "Total shaft diameter clearance", .4, .2, 1, .05), number("core_diameter", "Winding core outside diameter", 24, 12, 50), number("flange_a_thickness", "End A flange thickness", 6, 3, 14), number("flange_b_thickness", "End B flange thickness", 6, 3, 14), { key: "brake_end", label: "V brake groove", default: 1, min: 0, max: 3, step: 1, integer: true, unit: "", kind: "number", choices: [{ value: 0, label: "Neither end · plain flanges" }, { value: 1, label: "End A only" }, { value: 2, label: "End B only" }, { value: 3, label: "Both ends" }] }, number("groove_width", "Brake groove axial width", 3, 2, 8), number("groove_depth", "Brake groove radial depth", 1.2, .5, 4), number("groove_a_position", "A groove center from outer face", 3, 1, 13), number("groove_b_position", "B groove center from outer face", 3, 1, 13), number("socket_depth", "Core insertion at each end", 8, 4, 16), number("joint_clearance", "Total glue-joint diameter clearance", .3, .2, .8, .05), number("collar_wall", "Joint collar wall thickness", 2.4, 2.4, 5), number("core_wall", "Hollow core wall thickness", 2.4, 2.4, 5), number("web_thickness", "Flange web thickness", 2.4, 2.4, 5), ]; export const SPINNING_BOBBIN: CustomizerModel = { id: "spinning_bobbin", slug: "custom-spinning-bobbin", name: "Custom spinning bobbin kit", description: "Replace an unusual straight-shaft bobbin using measured length, shaft size and independent end diameters. Add a V brake groove to either end. One three-piece PETG kit per quantity; adhesive assembly required. Physical print, fit and spinning tests are pending. Presets do not promise wheel compatibility.", sourcePath: "src/lib/customizer/cad/spinning-bobbin.ts", sourceUrl: "https://3dprinterverse.com/designs/spinning-bobbin", geometryMode: "browser-csg", sourceKind: "store-original", family: "measured-fiber-repairs", kind: "print", parameters, defaults: Object.fromEntries(parameters.map(p => [p.key, p.default])), stlPath: "/models/parametric/custom-spinning-bobbin.stl", imagePath: "/images/galaxy/custom-spinning-bobbin.webp", volumeMm3: 1, surfaceAreaMm2: 1, bbox: { x: 1, y: 1, z: 1 }, printable: true, physicalPrintTested: false, materials: ["PETG"], infillPct: 100, priceCentsByMaterial: { PETG: 2000 }, presets: [ { id: "medium", label: "140 mm long · 70 / 70 mm ends", parameters: {}, description: "Example dimensions only. Measure your existing bobbin and compare shaft, groove and flyer clearance before ordering." }, { id: "compact", label: "110 mm long · 55 / 60 mm ends", parameters: { assembled_length: 110, flange_a_diameter: 55, flange_b_diameter: 60, shaft_diameter: 5, core_diameter: 20, flange_a_thickness: 5, flange_b_thickness: 5, groove_a_position: 2.5, groove_b_position: 2.5, groove_width: 2.5, groove_depth: 1, socket_depth: 6 } }, { id: "unequal", label: "155 mm long · 80 / 65 mm ends", parameters: { assembled_length: 155, flange_a_diameter: 80, flange_b_diameter: 65, shaft_diameter: 8, core_diameter: 26, flange_a_thickness: 7, flange_b_thickness: 5, groove_a_position: 3.5, brake_end: 1 } }, { id: "plain-storage", label: "170 mm long · plain 85 mm ends", parameters: { assembled_length: 170, flange_a_diameter: 85, flange_b_diameter: 85, shaft_diameter: 8, core_diameter: 28, brake_end: 0 }, description: "Plain-end example for storage or a suitable lazy kate. Match your actual rod; no drive whorl is included." }, ], }; export function bobbinValues(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 || (p.integer && !Number.isInteger(value))) throw new Error(`Choose supported ${p.label.toLowerCase()}.`); values[p.key] = value; } return values as Record<"assembled_length" | "flange_a_diameter" | "flange_b_diameter" | "shaft_diameter" | "running_clearance" | "core_diameter" | "flange_a_thickness" | "flange_b_thickness" | "brake_end" | "groove_width" | "groove_depth" | "groove_a_position" | "groove_b_position" | "socket_depth" | "joint_clearance" | "collar_wall" | "core_wall" | "web_thickness", number>; } export type BobbinPoint = [number, number]; export const BOBBIN_SEGMENTS = 192; const facetCos = Math.cos(Math.PI / BOBBIN_SEGMENTS); export function bobbinDimensions(input: ParameterValues) { const p = bobbinValues(input); // Circumscribed bores preserve the requested clearance between opposite flats. const boreRadius = (p.shaft_diameter + p.running_clearance) / (2 * facetCos); const socketRadius = (p.core_diameter + p.joint_clearance) / (2 * facetCos); const collarRadius = (socketRadius + p.collar_wall) / facetCos; const tubeInnerRadius = p.core_diameter / 2 * facetCos - p.core_wall; const tubeLength = p.assembled_length - p.flange_a_thickness - p.flange_b_thickness; const layoutWidth = p.flange_a_diameter + p.flange_b_diameter + p.core_diameter + 8; const centers = [-layoutWidth / 2 + p.flange_a_diameter / 2, -layoutWidth / 2 + p.flange_a_diameter + 4 + p.flange_b_diameter / 2, layoutWidth / 2 - p.core_diameter / 2]; return { boreRadius, socketRadius, collarRadius, tubeInnerRadius, tubeLength, centers, minimumBore: p.shaft_diameter + p.running_clearance, socketMinimumDiameter: p.core_diameter + p.joint_clearance, tubeMinimumBore: 2 * tubeInnerRadius * facetCos, windingSpan: tubeLength - 2 * p.socket_depth, bbox: { x: layoutWidth, y: Math.max(p.flange_a_diameter, p.flange_b_diameter), z: Math.max(tubeLength, p.flange_a_thickness + p.socket_depth, p.flange_b_thickness + p.socket_depth) } }; } /** Three independent radial profiles, in their support-free print orientations. * Each end's flat outside face is on the bed; the tube is upright beside them. */ export function bobbinProfiles(input: ParameterValues): BobbinPoint[][] { const p = bobbinValues(input), d = bobbinDimensions(input); const end = (diameter: number, thickness: number, position: number, grooved: boolean): BobbinPoint[] => { const r = diameter / 2; const rimInner = (r - Math.max(1.6, grooved ? p.groove_depth : 0)) * facetCos - 2.4; return [[d.boreRadius, 0], [r, 0], ...(grooved ? [[r, position - p.groove_width / 2], [r - p.groove_depth, position], [r, position + p.groove_width / 2]] as BobbinPoint[] : []), [r, thickness], [rimInner, thickness], [rimInner, p.web_thickness], [d.collarRadius, p.web_thickness], [d.collarRadius, thickness + p.socket_depth], [d.socketRadius, thickness + p.socket_depth], [d.socketRadius, thickness], [d.boreRadius, thickness]]; }; return [end(p.flange_a_diameter, p.flange_a_thickness, p.groove_a_position, p.brake_end === 1 || p.brake_end === 3), end(p.flange_b_diameter, p.flange_b_thickness, p.groove_b_position, p.brake_end === 2 || p.brake_end === 3), [[d.tubeInnerRadius, 0], [p.core_diameter / 2, 0], [p.core_diameter / 2, d.tubeLength], [d.tubeInnerRadius, d.tubeLength]]]; } export function validateBobbin(input: ParameterValues) { const p = bobbinValues(input), d = bobbinDimensions(input); if (Object.values(d.bbox).some(v => v > LIMITS.maxDimensionMm + 1e-6)) throw new Error(`The complete three-piece layout exceeds the ${LIMITS.maxDimensionMm} mm usable area on the Bambu A1’s 256 mm bed. Reduce the end diameters, core diameter or length.`); if (d.tubeInnerRadius * facetCos - d.boreRadius < .5 - 1e-7) throw new Error("Keep the hollow core bore at least 1 mm wider than the end shaft bore. Increase core diameter or reduce core wall, shaft size or clearance."); if (d.windingSpan < 20) throw new Error("Leave at least 20 mm of winding span between the two joint collars. Increase length or reduce flange thickness or insertion depth."); if (d.tubeLength > p.core_diameter * 10) throw new Error("Keep the upright core length within ten times its diameter for printing. Increase core diameter or reduce length."); if (p.web_thickness > Math.min(p.flange_a_thickness, p.flange_b_thickness) - .5) throw new Error("Keep flange web thickness at least 0.5 mm below both flange thicknesses."); const ends = [[p.flange_a_diameter, p.flange_a_thickness, p.groove_a_position, p.brake_end === 1 || p.brake_end === 3], [p.flange_b_diameter, p.flange_b_thickness, p.groove_b_position, p.brake_end === 2 || p.brake_end === 3]] as const; for (const [diameter, thickness, position, grooved] of ends) { if ((diameter / 2 - Math.max(1.6, grooved ? p.groove_depth : 0)) * facetCos - d.collarRadius < 3) throw new Error("Leave at least 3 mm of flange beyond each joint collar, including below a brake groove and its rim. Increase the end diameter or reduce core size or groove depth."); if (grooved && (position - p.groove_width / 2 < 1 - 1e-7 || position + p.groove_width / 2 > thickness - 1 + 1e-7)) throw new Error("Keep at least 1 mm of flange on both sides of each brake groove. Adjust groove position, width or that end’s thickness."); } if (p.brake_end !== 0 && 2 * p.groove_depth > p.groove_width + 1e-7) throw new Error("Brake groove width must be at least twice its depth for a support-free 45-degree upper slope."); } /** Exact metrics of the faceted revolutions used by Manifold, summed over all * three pieces. Contact faces stay exposed until the customer assembles them. */ export function bobbinMetrics(input: ParameterValues) { const theta = 2 * Math.PI / BOBBIN_SEGMENTS; let volumeMm3 = 0, surfaceAreaMm2 = 0; for (const profile of bobbinProfiles(input)) profile.forEach(([r1, z1], i) => { const [r2, z2] = profile[(i + 1) % profile.length]; volumeMm3 += BOBBIN_SEGMENTS * Math.sin(theta) / 6 * (r1 * r1 + r1 * r2 + r2 * r2) * (z2 - z1); surfaceAreaMm2 += BOBBIN_SEGMENTS * Math.sin(theta / 2) * (r1 + r2) * Math.hypot(z2 - z1, (r2 - r1) * facetCos); }); return { volumeMm3, surfaceAreaMm2, bbox: bobbinDimensions(input).bbox }; } export function bobbinPrintability(input: ParameterValues) { return { fits: Object.values(bobbinDimensions(input).bbox).every(v => v <= LIMITS.maxDimensionMm + 1e-6), instruction: "Print both end discs flat outside-face down, collars up, and the core tube upright. PETG, 100% infill; no supports. Preview and STL show all three pieces with 4 mm gaps.", notes: "One three-piece bobbin kit per quantity. Dry-fit, bond with a PETG-compatible adhesive and fully cure before use; adhesive is separate. Physical print, fit and spinning tests are pending." }; }