Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/models/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ export const CATALOG: Record<string, CatalogEntry> = {
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:
Expand Down
15 changes: 12 additions & 3 deletions libs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
166 changes: 166 additions & 0 deletions models/multiconnect_connectors.invariants.py
Original file line number Diff line number Diff line change
@@ -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)})
120 changes: 120 additions & 0 deletions models/multiconnect_connectors.scad
Original file line number Diff line number Diff line change
@@ -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 <BOSL2/std.scad>
// `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 <QuackWorks/Modules/snapConnector.scad>
use <QuackWorks/Modules/pushFitConnector.scad>

$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();
Binary file modified renders/lcd_stylus_hex_8mm/iso.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added renders/multiconnect_connectors/front.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added renders/multiconnect_connectors/iso.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added renders/multiconnect_connectors/side.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added renders/multiconnect_connectors/top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/sweep/multiconnect_connectors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { sweepModel } from "./runner";

sweepModel("multiconnect_connectors");
Loading