/* sb-report.scad — Strap-Beam shared validation and reporting =========================================================== Part of the Strap-Beam library. VALIDATION Checks are values, not statements. A profile builder returns a list of [condition, message] pairs and the core asserts over that list. Because the list is built inside the selected profile's own function, no other profile's parameters are ever touched — a slider that belongs to one catalogue entry cannot break a different one. REPORTING Every render emits a block of SB_KEY=value lines on stderr. These are stable, flat, and trivially scraped by the catalogue front end, which needs dimensions and a pass/fail without parsing geometry. Requires sb-geom.scad, sb-join.scad and BOSL2. */ // --------------------------------------------------------------------------- // Checks // --------------------------------------------------------------------------- function sb_check(condition, message) = [condition ? true : false, message]; function sb_first_failure(checks, i = 0) = i >= len(checks) ? undef : checks[i][0] ? sb_first_failure(checks, i + 1) : checks[i][1]; /* Assert the whole list and return a status string. Assign the result to a variable at file scope so the assertion runs before any geometry does. */ function sb_require(checks) = let(fail = sb_first_failure(checks)) assert(is_undef(fail), str("\n[strap-beam] ", fail, "\n")) "ok"; // --------------------------------------------------------------------------- // Metrics // --------------------------------------------------------------------------- SB_R_AREA = 0; SB_R_PARTS = 1; SB_R_SLOTS = 2; SB_R_MINW = 3; SB_R_SIZE = 4; SB_R_LEAK = 5; function sb_metrics(section, shell, members, g) = let( cav = sb_cavity_region(members, g), hull = hull_region(shell), b = pointlist_bounds(hull) ) [ sb_area(section), // PLA+ per unit length sb_nparts(section), // connected solids sb_nparts(cav), // separate strap channels sb_region_min_gap(section), // thinnest surviving wall [b[1].x - b[0].x, b[1].y - b[0].y], // envelope size sb_area(difference(cav, shell)) // cavity outside envelope ]; /* Checks every profile must pass, whatever its shape or member count. The connectivity test alone is not enough: a cross-section joined by a 0.14 mm knife edge is topologically connected and physically useless. The minimum-wall test is what actually catches over-large corner radii, swallowed junction gaps and fillets that have stopped bridging. */ function sb_universal_checks(section, metrics, expected_members, g) = [ sb_check(metrics[SB_R_PARTS] == 1, str("Cross-section is not one connected solid (", metrics[SB_R_PARTS], " separate pieces). Widen the junctions or thicken the walls.")), sb_check(metrics[SB_R_SLOTS] == expected_members, str("Expected ", expected_members, " separate strap channels but found ", metrics[SB_R_SLOTS], ". Neighbouring channels have merged, so those straps share one slot and are not retained. Increase the relevant web.")), sb_check(metrics[SB_R_MINW] >= sb_min_wall(g) - 1e-4, str("Thinnest PLA+ wall is ", metrics[SB_R_MINW], " mm, below the required minimum of ", sb_min_wall(g), " mm. Reduce the corner radius, increase the web, or lower min_wall_mm if this really is acceptable.")), sb_check(is_region_simple(section), str("The cross-section touches itself at a point rather than ", "crossing cleanly. Such an outline is valid but cannot be ", "tessellated, so it would fail on extrusion. Nudge the junction ", "fillet radius away from zero, or change the web slightly.")), sb_check(metrics[SB_R_LEAK] < 1e-4, str("A strap cavity breaks out of the outer envelope (", metrics[SB_R_LEAK], " mm^2 outside). The straps would not be enclosed.")) ]; // --------------------------------------------------------------------------- // Report // --------------------------------------------------------------------------- function sb_kv(key, value) = [key, value]; module sb_emit(key, value) { echo(str("SB_", key, "=", value)); } module sb_emit_all(pairs) { for (p = pairs) sb_emit(p[0], p[1]); } /* Standard report. `extra` carries whatever the individual profile wants to publish — solved sizes, effective projections, headroom on a radius — as a list of [key, value] pairs. */ module sb_report( family, profile, status, g, metrics, length_mm, density_g_cm3, extra = [] ) { section_area = metrics[SB_R_AREA]; volume_mm3 = section_area * length_mm; sb_emit("STATUS", status); sb_emit("FAMILY", family); sb_emit("PROFILE", profile); sb_emit("STRAP_WIDTH_MM", sb_width(g)); sb_emit("STRAP_THICK_MM", sb_strap_t(g)); sb_emit("BUNDLE_COUNT", sb_count(g)); sb_emit("BUNDLE_THICK_MM", sb_bundle_t(g)); sb_emit("CLEARANCE_MM", sb_clear(g)); sb_emit("WALL_INSIDE_MM", sb_wall_in(g)); sb_emit("WALL_OUTSIDE_MM", sb_wall_out(g)); sb_emit("WALL_EDGE_MM", sb_wall_edge(g)); sb_emit("MIN_WALL_SPEC_MM", sb_min_wall(g)); sb_emit("MIN_WALL_ACTUAL_MM", metrics[SB_R_MINW]); sb_emit("SECTION_AREA_MM2", section_area); sb_emit("SECTION_PARTS", metrics[SB_R_PARTS]); sb_emit("STRAP_CHANNELS", metrics[SB_R_SLOTS]); sb_emit("ENVELOPE_X_MM", metrics[SB_R_SIZE].x); sb_emit("ENVELOPE_Y_MM", metrics[SB_R_SIZE].y); sb_emit("LENGTH_MM", length_mm); sb_emit("VOLUME_MM3", volume_mm3); sb_emit("MASS_G", volume_mm3 * density_g_cm3 / 1000); sb_emit_all(extra); sb_emit("END", 1); } // --------------------------------------------------------------------------- // 3D output // --------------------------------------------------------------------------- /* A profile is a cross-section swept along Z. The section is what every consumer actually cares about — the catalogue renderer projects it, the slicer extrudes it — so it is generated once and reused for both. */ module sb_extrude_section(rgn, length) { linear_sweep(rgn, height = length, center = true); } module sb_extrude_straps(members, g, length) { for (m = members) for (p = sb_strap_layer_paths(m, g)) linear_sweep([p], height = length, center = true); } // Flat 2D output, for the catalogue's SVG pipeline. module sb_draw_section(rgn) { region(rgn); }