diff --git a/lib/models/catalog.ts b/lib/models/catalog.ts index 88f6acb..7b648aa 100644 --- a/lib/models/catalog.ts +++ b/lib/models/catalog.ts @@ -144,6 +144,11 @@ export const CATALOG: Record = { blurb: "Ready-to-print openGrid snap connectors (28mm pitch). One STL per variant: lite/full depth x directional/bidirectional. Press straight into an openGrid board.", }, + multiconnect_connectors: { + categoryId: "multiboard", + blurb: + "Ready-to-print Multiconnect (Multiboard) connectors that lock an accessory backer into a tile (25mm grid). One STL per variant: snap-regular (bidirectional click snap) or pushfit (press-in peg). Print one, press it in.", + }, kidscleancar_knob_cover: { categoryId: "toys", blurb: diff --git a/libs/README.md b/libs/README.md index 1377549..73bc140 100644 --- a/libs/README.md +++ b/libs/README.md @@ -44,9 +44,18 @@ patching them. `snapConnector.scad` was already unused by every model; the two models that used `multiconnectSlotDesignBOSL.scad` moved to QuackWorks' BOSL2-free master copy of the same backer, `Modules/multiconnectSlotDesign.scad` (patch 0002 below). Nothing in the -catalog references either vector-spin call site now, so `is_finite(spin)` is -moot. That migration is what this change lands, and it is green on the current -pin: full sweep 720 passed / 20 skipped / 0 failed. +catalog referenced either vector-spin call site at that time, so +`is_finite(spin)` was moot. That migration is what pst-9sw landed, and it was +green on the current pin: full sweep 720 passed / 20 skipped / 0 failed. + +**Update (pst-ks2, 2026-08-03):** `snapConnector.scad` is a live consumer +again — `models/multiconnect_connectors.scad`'s `snap-regular` variant invokes +`snapConnectBacker`, i.e. the `snapConnector.scad:59` vector-spin call site +(`spin=[0,270,0]`). So `is_finite(spin)` matters once more for that consumer: +the pin **must stay** at `456fcd8` while `snap-regular` ships (a BOSL2 bump +would break that variant's export, on top of the wasm hull regressions above). +`multiconnectSlotDesignBOSL.scad` is still unused; the `pushfit` variant is +pure OpenSCAD and pin-independent. **The bump itself is still blocked, for a new and different reason.** Moving to `fbcdfdd5` (v2.0.747) *with* the migration in place regresses 6 wasm sweep diff --git a/models/multiconnect_connectors.invariants.py b/models/multiconnect_connectors.invariants.py new file mode 100644 index 0000000..7cc6ed4 --- /dev/null +++ b/models/multiconnect_connectors.invariants.py @@ -0,0 +1,166 @@ +"""Invariants for the standalone Multiconnect connectors (pst-ks2). + +`connector_type` fans the export grid into one STL per variant +(snap-regular, pushfit), so this sidecar walks BOTH files rather than +trusting the single default variant the harness hands us as ctx["stl"]. +Only two variants exist: the vendored QuackWorks connector library has +exactly one snap module (snapConnectBacker, bidirectional = Multiboard +"Regular") and one push-fit module — the bead's moderate-wb / heavy-wb +tiers are Multiboard product taxonomy with no geometry behind them, so +there is nothing here to assert for them. + +For each variant it pins the claims the part exists for: + + 1. **Renders without error into a real solid.** A wasm/CGAL failure + can exit 0 with a dropped mesh; every variant STL must exist and + carry positive volume. + + 2. **One watertight solid.** A connector that presses into a board has + to be a single closed body — a split shell would slice or leak in + the slicer. snapConnectBacker's BOSL2 diff() tags survive the + print-orientation transform only if the pinned BOSL2 456fcd8 is in + force; a bad pin move shows up here first (dropped/split mesh). + + 3. **Fits one Multiboard cell, connector-side down.** Both connectors + seat on the 25mm (1 MU) grid, so the footprint must fit inside one + pitch; and each sits min-Z on the bed in its support-free print + orientation (slots-down snap / collar-down peg). + + 4. **Engagement depth on the order of the 6.25mm standoff.** The snap + and peg both stand ~6.5mm — the Part-A standoff height that lets + them reach through the tile and lock. A collapsed or doubled height + means the geometry drifted. + +Component count uses a local union-find over face adjacency: CI has no +scipy/networkx, so trimesh.split is unavailable (mirrors +opengrid_snaps.invariants / opengrid_bin.invariants). +""" + +from __future__ import annotations + +from pathlib import Path + +import trimesh + +from scripts.invariants import Failure + +MODELS_DIR = Path(__file__).resolve().parent +EXPORTS_DIR = MODELS_DIR.parent / "exports" + +_MB_PITCH = 25.0 # mm, one Multiboard grid unit — a connector fits inside it +_MB_STANDOFF = 6.25 # mm, Part-A standoff — the engagement-depth anchor +_CONTACT_EPS_MM = 0.05 +_FOOTPRINT_TOL = 0.6 # mm, allowed drift on the measured footprint +_HEIGHT_TOL = 0.5 # mm, allowed drift on the measured height + +# variant name -> (expected footprint mm [x==y], expected height mm) +_VARIANTS = { + "snap-regular": (23.37, 6.58), + "pushfit": (13.50, 6.50), +} + + +def check(ctx): + failures: list[Failure] = [] + for variant, (want_fp, want_h) in _VARIANTS.items(): + failures.extend(_check_variant(ctx["stem"], variant, want_fp, want_h)) + return failures + + +def _check_variant(stem: str, variant: str, want_fp: float, want_h: float) -> list[Failure]: + path = EXPORTS_DIR / f"{stem}-{variant}.stl" + if not path.exists(): + return [Failure( + f"{variant}-export", + f"{path.name} missing — run scripts/export-all.py " + "(the connector_type filename grid should produce it)", + )] + mesh = trimesh.load(str(path), force="mesh") + failures: list[Failure] = [] + + # 1. Positive volume: a dropped/empty CSG result exits 0 but is hollow. + if mesh.volume <= 1.0: + failures.append(Failure( + f"{variant}-volume", + f"{path.name} volume {mesh.volume:.2f}mm^3 <= 1 — the connector " + "solid is empty or collapsed (silent wasm/CGAL drop?)", + )) + + # 2. One watertight solid. + if not bool(mesh.is_watertight): + failures.append(Failure( + f"{variant}-watertight", + f"{path.name} is not watertight — the connector is not a single " + "closed body (BOSL2 diff()/pin drift on the snap?)", + )) + n = _component_count(mesh) + if n != 1: + failures.append(Failure( + f"{variant}-topology", + f"{path.name} has {n} connected components, expected 1 — the " + "connector broke into pieces", + )) + + b = mesh.bounds + ext = b[1] - b[0] + + # 3. Fits one Multiboard cell, connector-side down at z=0. + for axis, label in ((0, "x"), (1, "y")): + if abs(ext[axis] - want_fp) > _FOOTPRINT_TOL: + failures.append(Failure( + f"{variant}-footprint-{label}", + f"{path.name} {label} footprint {ext[axis]:.2f}mm != " + f"{want_fp}mm (+/-{_FOOTPRINT_TOL}) for '{variant}'", + )) + if ext[axis] > _MB_PITCH: + failures.append(Failure( + f"{variant}-pitch-{label}", + f"{path.name} {label} footprint {ext[axis]:.2f}mm exceeds the " + f"{_MB_PITCH}mm Multiboard cell — a connector must fit one cell", + )) + if abs(b[0][2]) > _CONTACT_EPS_MM: + failures.append(Failure( + f"{variant}-orientation", + f"{path.name} does not sit on z=0 (zmin={b[0][2]:.3f}) — not in " + "its support-free connector-side-down print orientation", + )) + + # 4. Engagement depth on the order of the 6.25mm standoff. + if abs(ext[2] - want_h) > _HEIGHT_TOL: + failures.append(Failure( + f"{variant}-depth", + f"{path.name} stands {ext[2]:.2f}mm, expected {want_h}mm for " + f"'{variant}'", + )) + if abs(ext[2] - _MB_STANDOFF) > 1.0: + failures.append(Failure( + f"{variant}-standoff", + f"{path.name} height {ext[2]:.2f}mm is not within 1mm of the " + f"{_MB_STANDOFF}mm Multiboard standoff — engagement depth drifted", + )) + + return failures + + +def _component_count(mesh) -> int: + """Connected components via union-find over face adjacency. + + trimesh.split needs scipy/networkx which CI doesn't have; this + mirrors scripts/check-invariants.py's built-in approach. + """ + n = len(mesh.faces) + if n == 0: + return 0 + parent = list(range(n)) + + def find(i: int) -> int: + while parent[i] != i: + parent[i] = parent[parent[i]] + i = parent[i] + return i + + for a, b in mesh.face_adjacency: + ra, rb = find(int(a)), find(int(b)) + if ra != rb: + parent[ra] = rb + return len({find(i) for i in range(n)}) diff --git a/models/multiconnect_connectors.scad b/models/multiconnect_connectors.scad new file mode 100644 index 0000000..becde30 --- /dev/null +++ b/models/multiconnect_connectors.scad @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: CC-BY-NC-SA-4.0 +// Copyright (c) 2026 Sean O'Connor +// +// Standalone Multiconnect (Multiboard) connectors — ready-to-print +// pieces that lock an accessory backer into a Multiboard tile (25mm +// grid, 6.25mm standoff) (pst-ks2). One connector per part; the +// connector_type enum fans the export grid into one STL per variant so +// the buildable kinds stay ready-to-print in the repo instead of being +// regenerated from the library each time. Print one, press it in. +// +// === SCOPE: only two connectors exist in the vendored library === +// +// pst-ks2 originally named four variants — snap-regular, snap-moderate-wb, +// snap-heavy-wb, pushfit. Only TWO are buildable. The vendored QuackWorks +// connector library exposes exactly one snap module (snapConnectBacker +// in Modules/snapConnector.scad — a single BIDIRECTIONAL click snap, +// i.e. Multiboard's "Regular" type; params offset + holdingTolerance +// only) plus one push-fit module (multiboard_push_fit in +// Modules/pushFitConnector.scad). The "Moderate WB" and "Heavy WB" tiers +// are Multiboard PRODUCT taxonomy (libs/README.md, "Multiboard +// constants") describing unidirectional wing-back snaps — there is NO +// .scad module or parameter for them in the vendored generators, so +// nobody can produce them from this library. The enum therefore ships +// the two connectors that exist: snap-regular | pushfit. (Recorded +// assumption — operator option A; see the bead. If the WB tiers are +// wanted they must first be authored/vendored as real geometry.) +// +// LICENSING: the connectors are QuackWorks' snapConnectBacker and +// multiboard_push_fit (libs/QuackWorks/Modules/{snapConnector, +// pushFitConnector}.scad; Multiconnect by Andy Levesque, credit David D; +// Multiboard by Keep Making; push-fit by Evil_K9), licensed CC +// BY-NC-SA 4.0 AND the Multiboard License — NON-COMMERCIAL, attribution, +// share-alike. This derived part is for personal use only; do not sell +// prints or files. +// +// === BOSL2 pin coupling (do NOT bump BOSL2) === +// +// snapConnectBacker passes spin=[x,y,z] VECTORS into BOSL2's +// offset_sweep()/attachable(), which only the pinned BOSL2 456fcd8 +// accepts — newer BOSL2 tightened attachable() to assert is_finite(spin) +// and rejects a vector (libs/README.md, "BOSL2 pin note"). Shipping +// snap-regular therefore re-couples a part to that pin: it must stay at +// 456fcd8 while this snap ships. pushfit is pure OpenSCAD and is +// independent of the pin. +// +// === Print orientation (native): ZERO supports === +// +// snap-regular prints slots-down: the four L-slots that let the snap +// nubs flex are cut through to the bed (z=0), so a solid base pad would +// foul them — the same rule the merged opengrid_snaps sibling follows. +// The upward octagonal bevel is a gentle wall and the four side bumpouts +// are small self-supporting arcs. pushfit prints collar-down (widest +// 14.6mm face on the bed, mirrored) so the peg tapers inward going up — +// no overhang at all. Both sit min-Z on the bed; no supports, no raft. +// +// === BOSL2 diff() tags: root-level siblings only === +// +// snapConnectBacker builds itself with BOSL2 diff()/tag("remove"), which +// BREAKS if wrapped in an explicit union() alongside a sibling. Each +// connector is emitted as a lone top-level statement (a bare transform +// around one primitive), never union()'d with anything else. + +include +// `use` not `include`: both connector files end with top-level demo +// geometry (snapConnector.scad renders a full backer plate) that would +// otherwise inject stray solids into every render. `use` pulls in the +// module definitions only. +use +use + +$fn = 64; + +// === User-tunable parameters === + +// The two buildable Multiconnect connectors, one exported STL each (the +// 'filename' flag fans the export grid over the enum). snap-regular = +// snapConnectBacker (bidirectional click snap); pushfit = +// multiboard_push_fit (press-in peg). See the SCOPE note above for why +// the moderate-wb / heavy-wb tiers are absent. +connector_type = "snap-regular"; // @param enum choices=snap-regular|pushfit group=connector label="Connector type" filename + +// Grip strength of the snap's holding bumpouts (snapConnectBacker's +// holdingTolerance): scales the click nubs that lock into the slot. +// Higher = tighter hold / harder to pull out. Inert for pushfit. +holding_tolerance = 1.0; // @param number min=0.5 max=1.5 step=0.05 group=connector label="Snap grip" + +// @preset id="default" label="Regular snap" connector_type=snap-regular +// @preset id="pushfit" label="Push-fit peg" connector_type=pushfit + +// === Derived === + +is_snap = (connector_type == "snap-regular"); + +// Multiboard reference constants (libs/README.md): 25mm grid pitch, +// 6.25mm Part-A standoff. Documentation/sanity anchors only — no +// geometry is derived from them (the connectors carry their own dims). +MB_PITCH = 25; // mm, 1 MU +MB_STANDOFF = 6.25; // mm, offset snap (DS Part A) standoff + +// PRINT_ANCHOR_BBOX at defaults (connector_type = "snap-regular"), +// measured from the export. The invariants gate fails on >1mm drift, so +// keep this current. snapConnectBacker is a 23.37mm octagonal snap +// standing 6.58mm once its slot base is dropped to the bed. +PRINT_ANCHOR_BBOX = [23.37, 23.37, 6.58]; + +// === Connectors (root-level siblings; never union()'d — diff() tags) === + +// snap-regular: snapConnectBacker's native z-extent is [-3.485, 3.09]; +// lift its slot base to the bed (z=0), slots-down. +if (is_snap) + translate([0, 0, 3.485]) + snapConnectBacker(offset = 0, holdingTolerance = holding_tolerance); + +// pushfit: multiboard_push_fit's native z-extent is [-0.5, 6.0], narrow +// tip down / wide collar up. Mirror it collar-down (zero overhang) and +// drop min-Z to the bed. +if (!is_snap) + translate([0, 0, 6.0]) + mirror([0, 0, 1]) + multiboard_push_fit(); diff --git a/renders/lcd_stylus_hex_8mm/iso.png b/renders/lcd_stylus_hex_8mm/iso.png index a65e1ae..2cb7717 100644 Binary files a/renders/lcd_stylus_hex_8mm/iso.png and b/renders/lcd_stylus_hex_8mm/iso.png differ diff --git a/renders/multiconnect_connectors/front.png b/renders/multiconnect_connectors/front.png new file mode 100644 index 0000000..9364293 Binary files /dev/null and b/renders/multiconnect_connectors/front.png differ diff --git a/renders/multiconnect_connectors/iso.png b/renders/multiconnect_connectors/iso.png new file mode 100644 index 0000000..37031d4 Binary files /dev/null and b/renders/multiconnect_connectors/iso.png differ diff --git a/renders/multiconnect_connectors/side.png b/renders/multiconnect_connectors/side.png new file mode 100644 index 0000000..9364293 Binary files /dev/null and b/renders/multiconnect_connectors/side.png differ diff --git a/renders/multiconnect_connectors/top.png b/renders/multiconnect_connectors/top.png new file mode 100644 index 0000000..ea352f4 Binary files /dev/null and b/renders/multiconnect_connectors/top.png differ diff --git a/tests/sweep/multiconnect_connectors.test.ts b/tests/sweep/multiconnect_connectors.test.ts new file mode 100644 index 0000000..c798c58 --- /dev/null +++ b/tests/sweep/multiconnect_connectors.test.ts @@ -0,0 +1,3 @@ +import { sweepModel } from "./runner"; + +sweepModel("multiconnect_connectors");