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("beam_width", "Measured beam width front to back", 40, 18, 110, .01), number("stick_width", "Measured flat stick width", 20, 8, 38, .01), number("stick_thickness", "Measured flat stick thickness", 4, 2, 12, .01), number("stick_pitch", "Stick center spacing", 50, 20, 90, .01), number("first_center", "First stick center from beam face", 34, 12, 90, .01), number("engagement", "Depth of beam engagement", 25, 12, 80), number("beam_clearance", "Total beam width clearance", .6, .3, 2, .05), number("stick_clearance", "Total stick width clearance", .6, .3, 2, .05), number("depth", "Bracket width along beam", 14, 10, 28), number("wall", "Mount and end wall thickness", 4, 3.2, 7), number("floor", "Material below stick seats", 5, 3.2, 10), number("lip", "Notch height above measured stick", 3, 2, 7), ]; export const LEASE_STICK_BRACKET: CustomizerModel = { id: "lease_stick_bracket", slug: "custom-lease-stick-loom-brackets", name: "Custom lease-stick loom brackets", description: "A pair of PETG brackets for holding flat lease sticks during threading of a slack warp. Fit the measured flat-topped rectangular loom beam, actual stick section and desired stick positions. Separate soft lashing is required around each bracket and beam; keep the cross tied independently. Loom, sticks and lashing are not included. Physical print, fit and use testing are pending; presets make no loom-fit claim.", sourcePath: "src/lib/customizer/cad/lease-stick-bracket.ts", sourceUrl: "https://3dprinterverse.com/designs/lease-stick-bracket", geometryMode: "browser-csg", sourceKind: "store-original", family: "measured-loom-tools", kind: "print", parameters, defaults: Object.fromEntries(parameters.map(p => [p.key, p.default])), stlPath: "/models/parametric/custom-lease-stick-loom-brackets.stl", imagePath: "/images/galaxy/custom-lease-stick-loom-brackets.webp", volumeMm3: 1, surfaceAreaMm2: 1, bbox: { x: 1, y: 1, z: 1 }, printable: true, physicalPrintTested: false, materials: ["PETG"], infillPct: 100, priceCentsByMaterial: { PETG: 1800 }, presets: [ { id: "medium-beam", label: "40 mm beam · 20 × 4 mm sticks", parameters: {}, description: "Measured example only. Check beam access, both stick ends and clearance to the shafts and warp." }, { id: "small-beam", label: "25 mm beam · 12 × 3 mm sticks", parameters: { beam_width: 25, stick_width: 12, stick_thickness: 3, stick_pitch: 36, first_center: 24, engagement: 18, depth: 12 } }, { id: "wide-sticks", label: "55 mm beam · 30 × 6 mm sticks", parameters: { beam_width: 55, stick_width: 30, stick_thickness: 6, stick_pitch: 60, first_center: 38, engagement: 32, depth: 18 } }, { id: "deep-beam", label: "65 mm beam · deeper 45 mm engagement", parameters: { beam_width: 65, stick_width: 24, stick_thickness: 5, stick_pitch: 55, first_center: 36, engagement: 45, depth: 16, wall: 5, floor: 6 } }, ], }; export function leaseValues(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<"beam_width" | "stick_width" | "stick_thickness" | "stick_pitch" | "first_center" | "engagement" | "beam_clearance" | "stick_clearance" | "depth" | "wall" | "floor" | "lip", number>; } export const LEASE_PRINT_GAP = 4; export const LEASE_MOUTH_CHAMFER = .8; export type LeasePoint = [number, number]; export function leaseDimensions(input: ParameterValues) { const p = leaseValues(input), opening = p.beam_width + p.beam_clearance, slot = p.stick_width + p.stick_clearance; const mountWidth = opening + 2 * p.wall, beamFace = p.wall + opening; const first = beamFace + p.first_center, second = first + p.stick_pitch; const length = second + slot / 2 + LEASE_MOUTH_CHAMFER + p.wall; const seat = p.engagement + p.floor, height = seat + p.stick_thickness + p.lip; const braceDrop = Math.min(20, p.engagement - 4); return { opening, slot, mountWidth, beamFace, first, second, length, seat, height, braceDrop, projection: length - mountWidth, bbox: { x: length, y: 2 * height + LEASE_PRINT_GAP, z: p.depth } }; } /** One outline: an open-bottom beam saddle and a braced arm with two open, * square-bottom seats. Chamfers widen only the mouths, preserving the full * measured rectangular stick envelope down to each flat seat. */ export function leaseOutline(input: ParameterValues): LeasePoint[] { const p = leaseValues(input), d = leaseDimensions(input), c = LEASE_MOUTH_CHAMFER; const points: LeasePoint[] = [[0, 0], [p.wall, 0], [p.wall, p.engagement], [d.beamFace, p.engagement], [d.beamFace, 0], [d.mountWidth, 0], [d.mountWidth, p.engagement - d.braceDrop], [d.length, p.engagement], [d.length, d.height]]; for (const center of [d.second, d.first]) points.push( [center + d.slot / 2 + c, d.height], [center + d.slot / 2, d.height - c], [center + d.slot / 2, d.seat], [center - d.slot / 2, d.seat], [center - d.slot / 2, d.height - c], [center - d.slot / 2 - c, d.height]); points.push([d.beamFace, d.height], [d.beamFace, d.seat], [0, d.seat]); return points; } export function validateLeaseBracket(input: ParameterValues) { const p = leaseValues(input), d = leaseDimensions(input); if (Object.values(d.bbox).some(v => v > LIMITS.maxDimensionMm + 1e-6)) throw new Error(`The complete bracket pair exceeds the ${LIMITS.maxDimensionMm} mm usable area on the Bambu A1’s 256 mm bed. Reduce beam width, stick positions or engagement depth.`); if (p.first_center < p.wall + d.slot / 2 + LEASE_MOUTH_CHAMFER + 4 - 1e-7) throw new Error("Move the first stick farther from the beam face to leave at least 4 mm between the slot mouth and the beam mount."); if (p.stick_pitch < d.slot + 2 * LEASE_MOUTH_CHAMFER + 4 - 1e-7) throw new Error("Increase stick center spacing to leave at least 4 mm of material between the two slot mouths."); if (d.projection > 130 + 1e-7) throw new Error("Keep the arm projection beyond the beam mount within 130 mm. Reduce the first-stick offset, spacing or stick width."); } /** Exact extruded-polygon metrics for both printed parts, excluding the gap. */ export function leaseMetrics(input: ParameterValues) { const p = leaseValues(input), points = leaseOutline(input); 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]); }); const area = Math.abs(twiceArea) / 2; return { volumeMm3: 2 * area * p.depth, surfaceAreaMm2: 2 * (2 * area + perimeter * p.depth), bbox: leaseDimensions(input).bbox }; } export function leasePrintability(input: ParameterValues) { return { fits: Object.values(leaseDimensions(input).bbox).every(v => v <= LIMITS.maxDimensionMm + 1e-6), instruction: "Print both broad side profiles flat as shown, 4 mm apart. PETG, 100% infill, 0.4 mm nozzle; no supports.", notes: "One pair per quantity. Soft lashing, loom and flat sticks are separate. Open seats do not lock the sticks. Support slack sticks for threading only; keep the cross tied independently and remove before tensioning or weaving. Physical print, fit and use tests are pending." }; }