import type { CadKernel, CadParameters, Manifold, Vec2 } from "./kernel"; import { basketProfile, bowlProfile, FIBER_MODEL_IDS, leafProfile, validateFiberConstraints, type Point } from "../fiber-crafts"; /** Devan's original fiber tools. Every solid starts with geometric primitives. */ export function buildFiberModel(id: string, values: CadParameters, c: CadKernel): Manifold | undefined { if (!(FIBER_MODEL_IDS as readonly string[]).includes(id)) return; validateFiberConstraints(id, values); const p = values as Record; if (id === "leaf_buttons") { const { outline, holes, offsets } = leafProfile(values); const face = c.sub2(c.polygon(outline), ...holes.map(h => c.move2(c.circle(h.radius, 32), [h.x, h.y]))); const button = c.extrude(face, p.thickness); return c.union(...offsets.map(([x, y]) => c.move(button, [x, y, 0]))); } if (id === "crochet_basket_bottoms") { const { panels, holes } = basketProfile(values); const outline = c.add2(...panels.map(panel => c.polygon(panel))); return c.extrude(c.sub2(outline, ...holes.map(h => c.move2(c.circle(h.radius, 32), [h.x, h.y]))), p.thickness); } if (id === "yarn_bobbin") { const r = p.core_diameter / 2, f = p.flange_diameter / 2, rise = f - r; const body = c.union(c.cylinder(3, f), c.cylinder(p.length, r), c.move(c.cylinder(rise, r, f), [0, 0, p.length - 3 - rise]), c.move(c.cylinder(3, f), [0, 0, p.length - 3])); return c.difference(body, c.move(c.cylinder(p.length + 2, p.spindle_hole / 2), [0, 0, -1])); } const { levels, inner, segments } = bowlProfile(values), h = p.height, r = p.diameter / 2; const revolve = (points: Point[]) => c.keep(c.polygon([[0, points[0][1]], ...points, [0, points.at(-1)![1]]]).revolve(segments)); let outer = revolve(levels); if (p.style === 2) { // A flat, angular mask emblem, deliberately unrelated to anatomical sculpts. const mask: Vec2[] = [[-.35, .7], [-.28, .86], [.28, .86], [.35, .7], [.22, .54], [.18, .36], [-.18, .36], [-.22, .54]] .map(([x, y]) => [x * r, y * h]); const eyes = [-1, 1].map(sign => c.polygon([[sign * r * .08, h * .68], [sign * r * .23, h * .7], [sign * r * .16, h * .58]])); const nose = c.polygon([[-r * .055, h * .54], [0, h * .61], [r * .055, h * .54]]); const teeth = [-1, 0, 1].map(i => c.move2(c.rect(r * .035, h * .13), [i * r * .095, h * .38])); const emblem = c.extrude(c.sub2(c.polygon(mask), ...eyes, nose, ...teeth), r * .2); outer = c.union(outer, c.move(c.rotate(emblem, [90, 0, 0]), [0, -r * .78, 0])); } if (p.style === 4) { // Two raised orbital bands, with shallow 45-degree shoulders. for (const z of [h * .32, h * .58]) { const radius = levels[1][0] + (levels[2][0] - levels[1][0]) * (z - levels[1][1]) / (levels[2][1] - levels[1][1]); outer = c.union(outer, c.move(c.cylinder(1.3, radius, radius + 1.3), [0, 0, z]), c.move(c.cylinder(1.3, radius + 1.3, radius), [0, 0, z + 1.3])); } } const cavity = revolve([...inner, [inner.at(-1)![0], h + 1]]); const stroke = (points: Vec2[], radius: number) => { const sections = points.map(point => c.move2(c.circle(radius, 24), point)); for (let i = 1; i < points.length; i++) { const a = points[i - 1], b = points[i], dx = b[0] - a[0], dy = b[1] - a[1], length = Math.hypot(dx, dy); sections.push(c.move2(c.rotate2(c.rect(length, radius * 2), Math.atan2(dy, dx) * 180 / Math.PI), [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2])); } return c.add2(...sections); }; const path: Vec2[] = [[-9, h + 4], [-9, h * .58]]; if (p.slot_shape === 0) for (let i = 0; i <= 12; i++) { const a = Math.PI + i * Math.PI / 12; path.push([9 * Math.cos(a), h * .58 + 9 * Math.sin(a)]); } let slot = stroke(p.slot_shape === 2 ? [[0, h + 4], [0, h * .52]] : path, 2.6); if (p.slot_shape === 1) slot = c.add2(slot, c.move2(c.circle(6, 40), [-9, h * .58])); const frontSlot = c.move(c.rotate(c.extrude(slot, r * .6), [90, 0, 0]), [0, -r * .6, 0]); const slotCutter = p.style === 2 ? c.rotate(frontSlot, [0, 0, 70]) : frontSlot; const feedHoles = Array.from({ length: p.feed_holes }, (_, i) => { const angle = 20 + i * 210 / Math.max(1, p.feed_holes - 1); return c.rotate(c.move(c.rotate(c.cylinder(r * .7, 3, 3, 40), [0, 90, 0]), [r * .6, 0, h * .72]), [0, 0, angle]); }); return c.difference(outer, cavity, slotCutter, ...feedHoles); }