import { LIMITS } from "@/config/business"; import type { ParameterValues } from "./catalog"; /** Zero means the original over-port socket, including for saved v1 orders. */ export const TUBE_REDUCER_DEFAULTS = { large_diameter: 50, small_diameter: 32, large_clearance: .4, small_clearance: .4, wall: 2.4, large_depth: 22, small_depth: 20, transition_length: 30, stop_inset: 2, lead_in: .6, large_fit: 0, small_fit: 0, large_profile: 0, small_profile: 0, large_depth_diameter: 50, small_depth_diameter: 32, }; const ranges: Record = { large_diameter: [20, 240], small_diameter: [12, 220], large_clearance: [.2, 1.2], small_clearance: [.2, 1.2], wall: [2, 5], large_depth: [10, 45], small_depth: [10, 45], transition_length: [12, 90], stop_inset: [1.2, 4], lead_in: [.3, 1.2], large_fit: [0, 1], small_fit: [0, 1], large_profile: [0, 1], small_profile: [0, 1], large_depth_diameter: [20, 240], small_depth_diameter: [12, 220], }; export function tubeReducerParameterVisible(key: string, parameters: ParameterValues) { return !key.endsWith("_depth_diameter") || parameters[key.startsWith("large_") ? "large_profile" : "small_profile"] === 1; } export function tubeReducerSegments(parameters: ParameterValues) { return parameters.large_profile === 1 || parameters.small_profile === 1 ? 192 : 96; } export function tubeReducerValues(parameters: ParameterValues) { const values = { ...TUBE_REDUCER_DEFAULTS }; for (const key of Object.keys(values) as (keyof typeof values)[]) { const value = parameters[key] ?? values[key]; const [min, max] = ranges[key]; if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max || ((key.endsWith("_fit") || key.endsWith("_profile")) && !Number.isInteger(value))) { throw new Error("Choose supported hose diameters, connections and wall dimensions."); } values[key] = value; } if (!values.large_profile) values.large_depth_diameter = TUBE_REDUCER_DEFAULTS.large_depth_diameter; if (!values.small_profile) values.small_depth_diameter = TUBE_REDUCER_DEFAULTS.small_depth_diameter; return values; } export function tubeReducerConnections(parameters: ParameterValues) { const p = tubeReducerValues(parameters); const end = (diameter: number, depthDiameter: number, profile: number, clearance: number, fit: number, depth: number) => { const insert = fit === 1; const matingDiameter = diameter + (insert ? -clearance : clearance); const mouthDiameter = (profile ? depthDiameter : diameter) + (insert ? -clearance : clearance); // New tapered configurations retain the selected minimum socket clearance // across polygon flats. Straight-only v1 meshes remain exactly unchanged. const factor = !insert && tubeReducerSegments(p) === 192 ? 1 / Math.cos(Math.PI / 192) : 1; const inner = matingDiameter / 2 * factor - (insert ? p.wall : 0); const mouthInner = mouthDiameter / 2 * factor - (insert ? p.wall : 0); return { insert, tapered: profile === 1, matingDiameter, mouthDiameter, inner, outer: inner + p.wall, mouthInner, mouthOuter: mouthInner + p.wall, depth, measuredLip: diameter, measuredDepth: profile ? depthDiameter : diameter }; }; return { large: end(p.large_diameter, p.large_depth_diameter, p.large_profile, p.large_clearance, p.large_fit, p.large_depth), small: end(p.small_diameter, p.small_depth_diameter, p.small_profile, p.small_clearance, p.small_fit, p.small_depth) }; } /** One radial profile drives both browser CSG and server-side price estimates. * The two-socket profile is unchanged from the original Everyday Prints CAD. */ export function tubeReducerProfile(parameters: ParameterValues): [number, number][] { const p = tubeReducerValues(parameters), { large, small } = tubeReducerConnections(p); const tapered = tubeReducerSegments(p) === 192; for (const [label, end] of [["Large", large], ["Small", small]] as const) { // Straight legacy sockets use an inscribed 96-gon. At large diameters a // small nominal clearance can disappear between vertices. Keep those meshes // unchanged and reject fits with less than 0.1 mm actual diametral space. const cosine = Math.cos(Math.PI / tubeReducerSegments(p)); const minimumSocketClearance = Math.min(2 * end.inner * cosine - end.measuredLip, 2 * end.mouthInner * cosine - end.measuredDepth); if (!end.insert && minimumSocketClearance < .1 - 1e-9) { throw new Error(`${label} end: increase diametral clearance to leave at least 0.1 mm across the socket's polygon flats. Start with 0.3–0.4 mm and trial the actual port.`); } const slope = Math.abs(end.mouthInner - end.inner) / end.depth; if (slope > .25 + 1e-9) throw new Error(`${label} end: reduce the diameter change or increase engagement depth. A measured cuff taper may change radius by at most 0.25 mm per mm of depth.`); if (end.tapered && (end.insert ? end.measuredDepth > end.measuredLip : end.measuredDepth < end.measuredLip)) { throw new Error(`${label} end: ${end.insert ? "a fit-inside taper must stay the same size or narrow from the free lip into the opening so the insert can slide in." : "a fit-over taper must stay the same size or widen from the free lip toward the tool body so the socket can slide on."} Reverse tapers are not supported.`); } if (p.wall / Math.sqrt(1 + slope ** 2) < 1.6) throw new Error(`${label} end: increase wall thickness to preserve 1.6 mm normal to the tapered cuff.`); } const throat = small.inner - (small.insert ? 0 : p.stop_inset); const innerSlope = (large.inner - throat) / p.transition_length; const outerSlope = (large.outer - small.outer) / p.transition_length; if (p.large_diameter - p.small_diameter < 8) throw new Error("Keep the large port diameter at least 8 mm wider than the small port diameter."); if (throat < 4) throw new Error("Increase the small port diameter or reduce its wall or socket stop to preserve an 8 mm flow passage."); if (2 * Math.min(large.inner, large.mouthInner, small.inner, small.mouthInner, throat) * Math.cos(Math.PI / tubeReducerSegments(p)) < 8) { throw new Error("Keep at least 8 mm of clear flow bore at both lips, both measured depths and the socket stop. Increase diameters or reduce walls or stop inset."); } if (innerSlope <= 0 || outerSlope < 0) throw new Error("Increase the large port diameter or reduce wall thickness to preserve a taper between these connection types."); if (innerSlope > .8 || outerSlope > .8) throw new Error("Increase transition length or reduce the diameter difference to keep the taper printable without supports."); if (p.wall / Math.sqrt(1 + Math.max(innerSlope, outerSlope) ** 2) < 1.6 || p.wall - p.lead_in < 1.4) { throw new Error("Increase wall thickness or reduce the entry chamfer to preserve 1.6 mm transition walls and 1.4 mm entry walls."); } if (!small.insert && p.stop_inset - p.small_clearance / 2 < 1) throw new Error("Increase the small socket stop inset or reduce small clearance to leave a 1 mm stop land."); const shoulder = p.large_depth + p.transition_length, height = shoulder + p.small_depth; const entry = (end: typeof large) => p.lead_in / (.8 - (tapered ? Math.abs(end.mouthInner - end.inner) / end.depth : 0)); const atEntry = (end: typeof large, radius: number) => radius + (end.inner - end.mouthInner) * entry(end) / end.depth; const points: [number, number][] = [ [large.mouthInner + (large.insert ? 0 : p.lead_in), 0], [large.mouthOuter - (large.insert ? p.lead_in : 0), 0], ]; if (large.insert) points.push([atEntry(large, large.mouthOuter), entry(large)]); points.push([large.outer, p.large_depth], [small.outer, shoulder]); if (small.insert) points.push([atEntry(small, small.mouthOuter), height - entry(small)]); points.push([small.mouthOuter - (small.insert ? p.lead_in : 0), height], [small.mouthInner + (small.insert ? 0 : p.lead_in), height]); if (!small.insert) points.push([atEntry(small, small.mouthInner), height - entry(small)]); points.push([small.inner, shoulder]); if (!small.insert) points.push([throat, shoulder]); points.push([large.inner, p.large_depth]); if (!large.insert) points.push([atEntry(large, large.mouthInner), entry(large)]); return points; } export function tubeReducerDimensions(parameters: ParameterValues) { const p = tubeReducerValues(parameters), { large, small } = tubeReducerConnections(p); const diameter = 2 * Math.max(large.outer, large.mouthOuter, small.outer, small.mouthOuter); return { x: diameter, y: diameter, z: p.large_depth + p.transition_length + p.small_depth }; } /** Integrate the annular frusta, entry chamfers and stop, rather than the box. * Smooth-surface metrics differ from the 96-sided preview by under 0.1%. */ export function tubeReducerMetrics(parameters: ParameterValues) { const points = tubeReducerProfile(parameters); let volumeMm3 = 0, surfaceAreaMm2 = 0; points.forEach(([r1, z1], index) => { const [r2, z2] = points[(index + 1) % points.length]; volumeMm3 += Math.PI / 3 * (r1 * r1 + r1 * r2 + r2 * r2) * (z2 - z1); surfaceAreaMm2 += Math.PI * (r1 + r2) * Math.hypot(r2 - r1, z2 - z1); }); return { volumeMm3, surfaceAreaMm2, bbox: tubeReducerDimensions(parameters) }; } export function validateTubeReducer(parameters: ParameterValues) { tubeReducerProfile(parameters); if (Object.values(tubeReducerDimensions(parameters)).some(d => d > 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 diameter, wall thickness or length.`); } } export function tubeReducerPrintability(parameters: ParameterValues) { const bbox = tubeReducerDimensions(parameters); return { fits: Object.values(bbox).every(d => d <= LIMITS.maxDimensionMm + 1e-6), instruction: "Print upright with the large opening on the bed. The transition and entry chamfers are checked for support-free printing.", notes: "PETG recommended for workshop use. Start with 0.3–0.4 mm total diametral clearance and trial the actual ports; large straight sockets may need more allowance. Straight or gently tapered smooth round push-fit ports only. Tapered ends match two measured diameters with positive clearance; no locking fit is promised. Support the hose separately. Physical print, fit and airflow testing are pending." }; }