import { LIMITS } from "@/config/business"; import type { ParameterValues } from "./catalog"; type Point = [number, number, number]; export const RECT_DUST_SEGMENTS = 128; export function rectangularDustDimensions(parameters: ParameterValues) { const p = parameters as Record; const insideWidth = p.port_width + p.clearance; const insideHeight = p.port_height + p.clearance; const insideRadius = p.corner_radius + p.clearance / 2; const roundInside = p.hose_fit === 0 ? p.hose_diameter + p.clearance : p.hose_diameter - p.clearance - 2 * p.wall; const roundOutside = roundInside + 2 * p.wall; return { insideWidth, insideHeight, insideRadius, roundInside, roundOutside, bbox: { x: Math.max(insideWidth + 2 * p.wall, roundOutside), y: Math.max(insideHeight + 2 * p.wall, roundOutside), z: p.port_depth + p.transition_length + p.hose_depth } }; } /** Intersect a ray with a true rounded rectangle. Sampling the same angles on * both profiles keeps the transition untwisted, including very narrow ports. */ function profile(width: number, height: number, radius: number, angles: number[]): [number, number][] { const a = width / 2, b = height / 2; return angles.map(angle => { const x = Math.cos(angle), y = Math.sin(angle), dx = Math.abs(x), dy = Math.abs(y); let distance = Math.min(a / dx, b / dy); if (radius > 0 && distance * dx > a - radius && distance * dy > b - radius) { const cx = a - radius, cy = b - radius, dot = cx * dx + cy * dy; distance = dot + Math.sqrt(Math.max(0, dot * dot - cx * cx - cy * cy + radius * radius)); } return [x * distance, y * distance]; }); } export function rectangularDustProfiles(parameters: ParameterValues) { const p = parameters as Record; const d = rectangularDustDimensions(p); const samples = Array.from({ length: RECT_DUST_SEGMENTS }, (_, i) => 2 * Math.PI * i / RECT_DUST_SEGMENTS); // Explicit corner samples prevent narrow, wide ports from losing clearance // to long chords across a small-radius corner. Both shells share all angles. for (const offset of [0, p.wall]) for (let corner = 0; corner < 4; corner++) for (let i = 0; i <= 16; i++) { const t = (corner + i / 16) * Math.PI / 2; const cx = (corner === 0 || corner === 3 ? 1 : -1) * (d.insideWidth / 2 - d.insideRadius); const cy = (corner < 2 ? 1 : -1) * (d.insideHeight / 2 - d.insideRadius); const angle = Math.atan2(cy + (d.insideRadius + offset) * Math.sin(t), cx + (d.insideRadius + offset) * Math.cos(t)); samples.push((angle + 2 * Math.PI) % (2 * Math.PI)); } const angles = samples.sort((a, b) => a - b).filter((a, i, list) => !i || a - list[i - 1] > 1e-9); return { innerRect: profile(d.insideWidth, d.insideHeight, d.insideRadius, angles), outerRect: profile(d.insideWidth + 2 * p.wall, d.insideHeight + 2 * p.wall, d.insideRadius + p.wall, angles), innerRound: profile(d.roundInside, d.roundInside, d.roundInside / 2, angles), outerRound: profile(d.roundOutside, d.roundOutside, d.roundOutside / 2, angles), }; } export function rectangularDustTransitionRun(parameters: ParameterValues) { const rings = rectangularDustProfiles(parameters); return Math.max(...["inner", "outer"].flatMap(side => { const rect = rings[`${side}Rect` as keyof typeof rings], round = rings[`${side}Round` as keyof typeof rings]; return rect.map(([x, y], i) => Math.hypot(x - round[i][0], y - round[i][1])); })); } /** Shared by browser CAD and checkout. Cuffs stand vertically; no transition * wall may move horizontally by more than its rise (45 degrees from vertical). */ export function validateRectangularDust(parameters: ParameterValues) { const p = parameters as Record; if (p.corner_radius > Math.min(p.port_width, p.port_height) / 2) { throw new Error("Corner radius cannot exceed half the measured port width or height."); } const d = rectangularDustDimensions(p); if (d.roundInside < 8) throw new Error("Leave at least 8 mm of clear hose bore; increase the hose diameter or reduce the wall thickness."); if (Object.values(d.bbox).some(value => value > LIMITS.maxDimensionMm + 1e-6)) { throw new Error(`This adapter exceeds the ${LIMITS.maxDimensionMm} mm usable area on the Bambu A1's 256 mm bed. Reduce the port size, cuff lengths, transition or wall thickness.`); } if (rectangularDustTransitionRun(p) > p.transition_length + 1e-6) { throw new Error("Lengthen the transition to keep every inner and outer wall within a 45° support-free slope."); } } /** Original annular loft: two straight cuffs and a ruled transition. The bore * stays open all the way through; no caps, internal ledges or reference meshes. */ export function rectangularDustMesh(parameters: ParameterValues) { const p = parameters as Record; const { innerRect, outerRect, innerRound, outerRound } = rectangularDustProfiles(p); const levels = [0, p.port_depth, p.port_depth + p.transition_length, p.port_depth + p.transition_length + p.hose_depth]; const vertices: Point[] = [], faces: number[] = [], n = innerRect.length; for (const profiles of [[outerRect, outerRect, outerRound, outerRound], [innerRect, innerRect, innerRound, innerRound]]) { for (let ring = 0; ring < 4; ring++) for (const [x, y] of profiles[ring]) vertices.push([x, y, levels[ring]]); } for (let side = 0; side < 2; side++) for (let ring = 0; ring < 3; ring++) for (let i = 0; i < n; i++) { const a = side * 4 * n + ring * n + i, b = side * 4 * n + ring * n + (i + 1) % n; const c = a + n, d = b + n; faces.push(...(side ? [a, d, b, a, c, d] : [a, b, d, a, d, c])); } for (let i = 0; i < n; i++) { const j = (i + 1) % n, inner = 4 * n; faces.push(i, inner + i, inner + j, i, inner + j, j); faces.push(3 * n + i, 3 * n + j, 7 * n + j, 3 * n + i, 7 * n + j, 7 * n + i); } return { vertices, faces }; } /** Integrate the dimension-generated triangular shell without loading WASM on * the server. Unlike a bounding-box proxy, all bore and wall changes count. */ export function rectangularDustMetrics(parameters: ParameterValues) { const { vertices, faces } = rectangularDustMesh(parameters); let volume = 0, surface = 0; for (let i = 0; i < faces.length; i += 3) { const a = vertices[faces[i]], b = vertices[faces[i + 1]], c = vertices[faces[i + 2]]; volume += (a[0] * (b[1] * c[2] - b[2] * c[1]) + a[1] * (b[2] * c[0] - b[0] * c[2]) + a[2] * (b[0] * c[1] - b[1] * c[0])) / 6; const u = b.map((v, axis) => v - a[axis]), v = c.map((value, axis) => value - a[axis]); surface += Math.hypot(u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]) / 2; } return { volumeMm3: volume, surfaceAreaMm2: surface, bbox: rectangularDustDimensions(parameters).bbox }; } export function rectangularDustPrintability(parameters: ParameterValues) { const d = rectangularDustDimensions(parameters), p = parameters as Record; return { fits: Object.values(d.bbox).every(value => value <= LIMITS.maxDimensionMm + 1e-6), instruction: "Print upright on the rectangular cuff in PETG at 100% infill. Valid transitions stay within 45°; no supports are needed. Use a brim for bed adhesion.", notes: `Straight rectangular engagement: ${p.port_depth} mm; round engagement: ${p.hose_depth} mm. Physical print, fit and airflow testing are pending.` }; }