Commit Graph
100 Commits
Author SHA1 Message Date
matthewtrahan 5caa288b45 plugins: nozzle review fixes -- an out-of-range or null nozzle is clamped/defaulted instead of keeping the last part's widths; Fusion's hand-built dialog gets the Nozzle field it was documented with 2026-10-13 13:32:31 -05:00
matthewtrahan 041448a71f plugins: nozzle size -- Cura and Orca read it from the part's extruder, CAD dialogs show it; the entry resets to 0.4 when a call leaves it out
options.json gains nozzle (hostSupplied: slicer, 0.2-0.8, default 0.4). The
entry sends it in tunables on every call, so a host's long-lived engine context
can't keep the last part's widths. Cura: machine_nozzle_size on the part's
extruder. Orca: nozzle_diameter, the wall extruder's entry. FreeCAD, Blender,
Fusion, SolidWorks and the CLI pick it up from the schema.
2026-10-13 13:28:16 -05:00
matthewtrahan 172d810eab nozzle: a Nozzle size field that scales the support widths -- every width was a 0.4 nozzle's, so a 0.6 or 0.8 dropped the tines or laid the wall as one line
Tines and the contact tip scale by nozzle/0.4 both ways; the wall, wedge and
sway brace rib only grow (a finer nozzle doesn't stiffen a free-standing wall).
Heights stay on the layer height. 0.4 is unchanged (sweep vs main: identical).
The Worker gets it in opts.tunables.nozzle; the plugins stay at 0.4 for now.
2026-10-13 13:14:20 -05:00
Matthew Trahanandmatthewtrahan 0a1cd56564 cura: slider values in their own right-hand column -- Cura's % badge floated above the handle over the row above, and the overhang's 45° sat crammed against the 70 end label (#242) (#243)
Co-authored-by: matthewtrahan <mathyout@pm.me>
2026-10-13 08:27:55 -05:00
Per Hjort a5170233d9 Allow optional Sway depth beyond the existing 60 mm cap (#241) 2026-10-13 07:38:43 -05:00
360b4ad9b9 tests: load the vendored occt-import-js as CommonJS by hand -- on Windows Deno's require() of it returns an empty ESM namespace, so step.test.js and FreeCAD's STEP smoke check couldn't run there (#228)
tests/_occt.js evaluates the UMD file with module/exports/require/__filename/
__dirname supplied, and both callers use it. site_step.js also drops its last
URL.pathname. Follows #230, which fixed the other Windows paths.

Co-authored-by: matthewtrahan <mathyout@pm.me>
Co-authored-by: Per Hjort <35040883+HjortCreations@users.noreply.github.com>
Co-authored-by: MiSTRFiNGA <5302926+MiSTRFiNGA@users.noreply.github.com>
2026-10-10 13:41:23 -05:00
chromaglow 0887505092 tests: build file paths with fileURLToPath so the suite runs on Windows (#230)
URL.pathname gives '/C:/dir%20with%20space/...' on Windows, so every test that
reads a model or fixture failed there (75 of 265). import() specifiers keep the
URL; disk reads take a filesystem path.
2026-10-10 13:35:51 -05:00
Matthew Trahanandmatthewtrahan a59fb2ba6d Interface material: wall crests + tines as their own 3MF object for a toolchanger (#21) (#205)
* engine: interface crest (GitHub #21) -- with PROP.iface on, every wall (prop, part-attached, squat, Draw/fill, wedge blade) splits 0.6 mm under its top into body + crest, the crest and the tine comb tagged (v[3] = 1) so triRanges/removal/Worker are untouched; the 3MF writer's separate form writes the tagged triangles as a third object, '<name> interface', in register; off (the default) is byte-identical

* site: Interface material switch (Walls) -- the crest and tines show purple and export as their own 3MF object, for a toolchanger's second filament (PETG under PLA); reaches the Worker via tunables and Draw on the page; re-import still pre-selects the part; German strings; plugins leave it out (one body to the host)

* engine: interface crest review fixes -- sway brace tines are interface too (Auto and Draw); the crest is one trapezoid on the taper line, so no section has a collinear corner (zero-area cap triangles); Interface material is in the undo history; FIN-SPEC lists what is not split yet (sway rib edge, a part-standing wall's foot, a wedge whose contact steps straight up) (review)

* site: the interface is a part of the supports object, not its own -- an object starting in mid-air is refused by Orca ('empty first layer'); as the supports' second component it stands on the plate with them and takes its own filament per part

* engine: interface crest modes -- 'flat' (the default when on) crests a wall station only under an underside within 10deg of level, a full-width (th) 0.6 mm block with no tip taper, tines left in the body; 'all' crests every wall top and the tines as before. A 35deg cube's print with the crest everywhere couldn't lay PETG on the narrow PLA tip through every layer; PETG interfaces are used on flat undersides. Off stays byte-identical (sweep vs main)

* site: Interface material is a dropdown (Off / Flat contacts / Everywhere); the readout says when it is on and no wall got a crest; German strings

* engine: interface modes review fixes -- cutouts under a flat crest keep their old ceiling (holeTop: the taper start, not the crest's cut), so the split loses no plastic (lbracket Y8 lattice lost 4.6%); the crest's part reference is set only while the crest is on and dropped after each build; the readout's 'no flat contacts' note counts walls only (not the pad) and only in Flat contacts; tests: flat mode with lattice/diamond cutouts, mixed flat/tilted walls, squat walls (review)

* engine: the interface crest is one layer of the print (PROP.ifaceLayers 1, the Layer height field, as the tines), not a fixed 0.6 mm -- one tool change each way per flat contact instead of three; tooltip and FIN-SPEC say so; tests: the crest is one 0.2 / 0.3 mm layer

---------

Co-authored-by: matthewtrahan <mathyout@pm.me>
2026-10-09 22:00:37 -05:00
Matthew Trahanandmatthewtrahan f2147bd19f Plate only: every support on the build plate, none on the part (#218) (#221)
* engine: plateOnly -- every support on the build plate, none on the part (#218). Auto and the short walls drop a line the part-attached path would take (built or floored) and count it as skipped.onPart, never stilting it to the plate through the part; drawnWall refuses a line with part under it, naming the setting, and its squat fallback skips the part; the fill pass never retries a wall on the part. Off by default: sweep vs main identical. tests: plate_only -- a drawn line over the part refused, over open plate byte-identical; Auto and Full on the portal at Y90 build nothing on the part and count it; off, the lbracket is unchanged

* ui + plugins: a Plate only checkbox under Walls (#218) -- read by the auto build and the drawn walls; the readout says how many supports it left off and why, and names it when it emptied a part; undoable; German strings. options.json gains plateOnly, so every plugin dialog shows it, and both entries pass it on. tests: the dead-control check runs it on the portal at Y90 (the lbracket has no wall on the part); FreeCAD and Blender unit fixtures list it

* plate only: Full refuses a line over the part before building it, and a mixed line keeps its plate stretch (review) -- the local pass turned the on-part retry back on, and every try reached drawnWall to be refused: the portal at Y90 spent all 120 tries and blamed the wall limit. stationClear's `plate` now refuses a station with part under it on every try. Auto hands a line the part-attached path claims to the plate path (its column probes trim the stations over the part) instead of dropping it whole. The readout names Plate only when it emptied a part in any mode, not only Prop; German help line. tests: every built support reaches the plate, Full isn't capped

---------

Co-authored-by: matthewtrahan <mathyout@pm.me>
2026-10-08 22:32:15 -05:00
matthewtrahan 2dda91573a nav: a Mouse menu in the rotate panel picks which button orbits, pans and zooms (#53) -- Default (unchanged), Fusion 360, Onshape, SolidWorks, Blender. A preset is just OrbitControls.mouseButtons: a held Shift/Ctrl swaps orbit and pan on a button, so the CAD chords (Shift+middle orbit, Ctrl+middle pan) come free. The CAD presets leave left-drag to the picks, as those packages do; touch and wheel zoom unchanged; a middle press no longer starts the browser's autoscroll. Remembered in localStorage (optional), undoable. tests: navpresets -- Default is the old mapping, every preset orbits + pans, CAD presets leave left unbound; options -- nav-preset is site-only 2026-10-08 17:15:13 -05:00
matthewtrahan ae1ded3e43 pose: "Show rotate rings" checkbox hides the rotate gizmo (#53) -- the rings sit over the part and some users never use them; off hides and disables them, while Rotate 90deg, Reset and Lay a face flat still turn the part. Lives next to "Show layers" in the rotate panel (the Display section only shows once fins are on, and the rings matter before that). Remembered in localStorage (optional), undoable like every setting; the "Drag the rings" hint hides with them. tests: options -- show-rings is site display, not a plugin option 2026-10-08 17:04:04 -05:00
matthewtrahan 42dfed35b2 plugins/solidworks: reopening the window works (the WebView2 loader folder is set once per process); a piece's shells union into one body; earlier fins are also known by their feature; triangles wound by SolidWorks' normals; startup key for the running user; uninstall checks RegAsm; hand test grows (review) 2026-10-05 20:51:22 -05:00
matthewtrahan 1001b26a22 ci: build the SolidWorks add-in (dotnet on Linux) and publish support-fins-solidworks.zip to plugins-latest, labelled with its .csproj version 2026-10-05 20:51:22 -05:00
matthewtrahanandHaititei 54fc69eb02 plugins/solidworks: SolidWorks add-in on the shared engine, run in WebView2
Takes over #183. The add-in (C#, .NET Framework 4.8) reads the part's visible
solid bodies, and a WebView2 window runs the website's engine (the shared
bundle, unmodified) at V8 speed, outside SolidWorks' thread. Its dialog is
built from options.json and its readout is the shared reportLine, so overhangs
it can't reach and floating pieces are said. Print bed: Top Plane (Y up),
Front Plane (Z up) or a selected flat face, never guessed. Each piece goes in
as its own named solid body (STL import as solid, mm, the user's import
settings put back after); the next run skips and counts them.

SolidWorks interop is compile-time only, its types embedded: no Dassault DLL
ships and no single SolidWorks release is required. Builds with dotnet on any
OS (build.py --zip). tests/host.test.js pins the frame mapping (same fins as
computeFins whichever way up the model is drawn) and that every piece is a
closed, consistently wound STL. Not yet run in SolidWorks: README has the hand
test.

From #183: the ribbon command, reading tessellation in metres, the Y-up frame.

Co-authored-by: Haititei <werbung@iggena.de>
2026-10-05 20:51:22 -05:00
matthewtrahan 7b13a58601 plugins/shared: the bridge exports reportLine, for a host whose dialog is a web page (SolidWorks) to show the shared result line; every other export unchanged 2026-10-05 20:51:22 -05:00
matthewtrahan 59848dc207 blender smoke: its too-short drawn line is 1 mm, under DRAW_MIN_LEN (2 mm is now a wall) 2026-10-04 14:09:50 -05:00
matthewtrahan 54e02ee141 draw: lines down to 2 mm, and squat walls when no full wall fits -- on the part with 0.6 mm of room, on the plate (only over clear plate) as a brimmed stem; tried after both full walls fail, so every line that built before builds the same (local issue 011) 2026-10-04 14:09:50 -05:00
matthewtrahan d89f72f0df engine: sway brace tines kiss the part's face (kissEnds), and the material's tine bite is gone -- one SWAY.tineReach 0.3 gates placement; Material tooltip no longer claims PETG shrinks a bite; same braces and tines on 16 cases x PLA/PETG, slices identical on 30 of 32 (lighthouse Y90 notch: +-0.13 mm2) 2026-10-03 15:14:03 -05:00
matthewtrahan ec7475a9ea tine knobs cleanup: comments say 'at' the reach, the bead width is likely, TINE_REACH in the real-parts test, Prusa port notes the old name (review) 2026-10-03 13:52:03 -05:00
matthewtrahan b7236f0529 plugins/prusa README: material note drops the retired bite coupon (review) 2026-10-03 09:39:44 -05:00
matthewtrahan 4d2fa682a2 site: drop the Tine bite field -- bite doesn't change the weld (the part's next layer prints on the tine) and a small bite only removed tines on shallow faces; CALIBRATION.md loses the bite test 2026-10-02 18:19:49 -05:00
matthewtrahan 1904c8f4fb site: Plugins menu in the topbar -- every plugin from plugins-latest with its install note, one Your-computer picker for the per-platform files, version line from the release labels 2026-10-02 15:28:54 -05:00
matthewtrahan 1251ea3303 docs: PrusaSlicer plugin confirmed working in 3.0 alpha11 (Matthew, 2026-10-02); size the fin with Fin Height, not the scale tool, which stretches the one-layer tines 2026-10-02 15:22:17 -05:00
matthewtrahan 21fc21220a prusa: tines keep a bare layer between them (tight spacing on a shallow slope fused them into 2-layer bands), spacing described as approximate; README plugin section describes the fin, in-slicer check pending (review) 2026-10-02 14:24:21 -05:00
matthewtrahan 632ea3633a prusa: Tine Spacing field (default 6 mm, was a fixed 2) -- tines spread evenly foot to top, no further apart than the spacing, at least 3; a 25 mm 45 deg fin goes from 18 tines to 7 2026-10-02 14:24:21 -05:00
matthewtrahan abea1cdc05 prusa: Add a Fin builds the angled-print support fin (was a slab with 2x12 mm plates)
A thin right triangle whose slope sits 0.2 mm under a tilted part's underside
(the 30/45/60 deg fins on printables.com/model/1771718): straight back edge,
short flat top, foot that keeps the slope's gap, one-layer horizontal tines along
the slope biting 0.5 into the part at mid-layer, tops snapped to the print
preset's layer grid. Slope Angle field (20-70, default 45). The smooth slope is
one cube turned about Y over a hidden step fill (make_prism is isosceles and would
need a Negative cut the slicer draws as a grey box). Mock test models rotation,
the layer grid and string presets; README + in-slicer checklist rewritten.
2026-10-02 14:24:21 -05:00
matthewtrahan 1b7334c2df site: Tine bite field (Tines section) drives the walls' PROP.tineBite through tunables.wallBite; default 0.5 unchanged, plugins keep the engine default 2026-10-02 13:52:52 -05:00
matthewtrahan ea76eb5632 ci: publish pins Python 3.12 for release_labels.py (tomllib); FreeCAD README names the 1.0 Mod path (review) 2026-10-02 11:18:53 -05:00
matthewtrahan f1fd249dd0 ci: publish FreeCAD zips + the CLI to plugins-latest, label every asset with its plugin version and engine commit (the site's Plugins menu reads the labels) 2026-10-02 11:18:53 -05:00
matthewtrahan 727a9de2e7 cli: build.py bundles the command line into one support-fins.mjs (Deno or Node), README runs the download 2026-10-02 11:18:53 -05:00
matthewtrahan 4d0daf0585 freecad: build.py --all zips the add-on per platform (support-fins-freecad-<platform>.zip, unzip into Mod/), README install from the download 2026-10-02 11:18:53 -05:00
matthewtrahan 28ed79c481 cura: whole-plate Add skips support blockers and support/infill/cutting meshes, names groups it left out in the report; package test lists scope.py (review) 2026-10-02 08:40:28 -05:00
matthewtrahan e6e0cc2bda cura: Add Support Fins with nothing selected fins every part on the plate -- forgetting to click the part was the hand test's one snag; a group-only selection says ungroup instead 2026-10-02 08:40:28 -05:00
matthewtrahan 48d12cbe98 blender: export review fixes -- red overhangs stayed red (a shared material painted both overlays amber), no empty 3MF objects
- Both overlays used the material "Support Fins overhangs", and _material repaints a
  material by name: on a part with both kinds of face, the amber pass turned the red
  overlay amber too (Blender's solid view colours by material), mixing up "held" with
  "too small to fin". Each overlay now has its own material; the smoke test builds the
  bracket + a 1.5 mm nub (both kinds) and checks each colour (fails without the fix).
- A fin whose triangles all fall to the degenerate filter is left out of the 3MF
  (an object with no triangles isn't valid 3MF).
- README: Blender's own STL export needs Selection Only, or the overlay sheets go too.
2026-10-01 14:34:41 -05:00
matthewtrahan 7071a598ba blender: 3MF export skips an empty drawn wall -- one that no longer fits is emptied, and failed the export if unhidden 2026-10-01 14:34:41 -05:00
matthewtrahan 0ad4db7a05 blender: Show overhangs -- the site's red (held) and amber (too small to fin) faces over the part
Generate paints the overhangs the fins hold red and the faces past the angle but too
small to fin amber, as on printfins.com, so an overhang nothing holds is shown rather
than hidden. They are unselectable, unrendered objects under the part, drawn in front;
Show overhangs (after Generate) toggles them, skipping any in an excluded collection.
Lay face flat and Clear remove them with the fins.
2026-10-01 14:34:41 -05:00
matthewtrahanandRemusTL 26252741ec blender: Export part + fins (.3mf) -- one assembly in mm, the fins where the engine put them
Blender has no 3MF exporter. The part and each visible fin / drawn wall are their own
3MF objects, all in one build item, so a slicer opens them as one object in their
original places. Triangles the weld closed are dropped (invalid in 3MF), and the export
warns when the fins are out of date. The manifest asks for file access for it.

Co-authored-by: RemusTL <336249490+RemusTL@users.noreply.github.com>
2026-10-01 14:34:41 -05:00
matthewtrahan 20b707c22c blender: draw review fixes -- Clear works for a drawn-only part, a wall that no longer fits is emptied, a click off the part doesn't select
- A part that only ever had drawn walls never gets a result line: with those walls
  deleted by hand, Clear was disabled while the pose stamp stayed (out of date
  forever). Clear now also runs for the stamp alone.
- A drawn wall that no longer fits the pose was hidden but kept its old-pose mesh,
  which Blender's own STL export (Visible Only is off by default) would still write.
  It is now emptied as well; its ends stay, so it comes back when the part does.
- A click in the viewport that misses the part no longer passes through to Blender
  (it changed the selection mid-tool); the header says to click on the part.
  Clicks outside the viewport still pass through.
2026-10-01 14:24:13 -05:00
matthewtrahanandRemusTL 08c0a2b1ce blender: Draw wall + Lay face flat -- click the part for one hand-placed wall (the site's Draw), or the face to put on the bed
Draw wall: two clicks under an overhang make one wall through drawWall, with an orange
preview line between the first click and the mouse; a line the engine refuses says why.
Each drawn wall keeps its ends in the part's frame, so Generate re-stands it for the
part's current pose (the site's rebuildDrawn) or says why it no longer fits; a drawn
wall alone marks the fins out of date when the part moves. Lay face flat turns the part
so the clicked face is down, keeping its lowest point, and drops the old pose's fins.

The tools start from the sidebar but cast from the 3D View's window region, and any
error ends them cleanly (no preview handler or header text left behind).

Co-authored-by: RemusTL <336249490+RemusTL@users.noreply.github.com>
2026-10-01 14:24:13 -05:00
matthewtrahan ce7dbed736 blender: hand-test fixes -- panel messages wrap to the sidebar's width (were cut off with ...), Clear works once every fin was deleted by hand
- Blender cuts a label longer than the sidebar off with an ellipsis; the fixed
  42-character wrap was wider than the default sidebar, so "That size looks wrong for
  a print: is ..." and long result lines lost their ends. The wrap now follows the
  sidebar's width and UI scale.
- Clear needed a fin to exist; with every fin deleted by hand the result line and the
  pose stamp stayed (and the out-of-date check ran against fins that were gone).
- The manifest's Import-Export tag waits for the 3MF export.
2026-10-01 13:51:02 -05:00
matthewtrahanandRemusTL aaf962c648 ci: blender job -- tests + a zip per platform, and a headless-Blender smoke test (4.2 LTS, 5.2) of the Linux zip
The smoke test installs the zip into a checksum-verified official Blender with its user
folders in a temp dir, then: lbracket @35 gets the site's 4 walls / ~20 tines as closed
objects on the bed; moved / settings / edited / unit scale mark the fins out of date;
settings hide as the engine says; the panel draws in every state; Clear.
The zips go to plugins-latest with the others.

Co-authored-by: RemusTL <336249490+RemusTL@users.noreply.github.com>
2026-10-01 13:51:02 -05:00
matthewtrahan 0cddbe2460 blender: Support Fins extension core -- Generate puts the site's fins under the part as objects, on the shared engine path
Rebuild of #145 on the plugins' model: the panel is generated from options.json and a
setting shows only when the engine uses it; the fins come from computeFins through the
shared host (mini-racer as a manifest wheel, JIT on), one object per wall / sway brace
/ pad (pieces); the result line is host_report, so overhangs that weren't reached and
floating pieces are always said. Moving the part, editing its mesh (caught across a
save + reopen by a mesh fingerprint), changing a setting or the unit scale marks the
fins out of date. Units: Unit Scale x 1000 = mm; the panel offers Use millimetres when
the part's size looks wrong.

build.py makes one zip per platform and flattens mini-racer's wheel: it publishes the
package under .data/purelib/, which Blender 4.2 / 4.3's installer leaves unimportable.

GPL-3.0-or-later (Blender's rule for add-ons); the bundled engine stays MIT. Draw wall,
Lay face flat, 3MF export and the overhang overlay follow on their own branches.
2026-10-01 13:51:02 -05:00
matthewtrahan fdd4d3307b shared: host_engine(jit=True) keeps V8's JIT on macOS for a host that allows it -- 80 s -> 3 s on a 75k-triangle part
The default stays --jitless on macOS (Orca's hardened app refuses JIT memory). Blender's
macOS app is signed with com.apple.security.cs.allow-jit, and jitless ran issue121 (75k
triangles, 81 walls) in 80 s against 3 s with the JIT -- same walls and tines. Pinned:
jit=True gives lbracket's 4 walls / 20 tines in a fresh process.
2026-10-01 13:51:02 -05:00
matthewtrahan 56e23fe43a shared: vendor.py hands out the mini-racer wheel itself -- for Blender, which installs an extension's wheels
mini_racer_wheel(plat, cache) downloads (once) and returns the wheel; vendor_mini_racer
unpacks it as before for Cura and FreeCAD.
2026-10-01 13:51:02 -05:00
matthewtrahan 902f0f48f3 shared: review fixes -- overFaces is the site's red (kept), smallFaces its amber; draw tests compare coordinates
overFaces was every face past the threshold, but the site paints only the faces in
regions it supports red (res.kept) and the too-small ones amber. A host painting
overFaces red would have called slivers overhangs that no fin targets. Now
overFaces = red, smallFaces = amber, both pinned to paintOverhangs' rule (a shelf +
2 mm nub part gives an amber face, so that half of the check runs).

drawWall: the site comparison checks where the wall is, not just its counts, on an
off-centre part; layer height 0.1 / 0.3 reach the wall. host_draw_wall refuses a
NaN end in Python instead of a raw JS parse error; stats say partAttached.
2026-10-01 08:32:48 -05:00
matthewtrahan 33bce1252e shared: Draw mode for the plugins -- drawWall builds one hand-drawn wall the way the site's Draw does
engine/draw_entry.js drawWall(soup, a, b, options): the part as computeFins takes it
and the two clicked points in the same frame -> web/draw.js drawnWall, called as
web/ui/walls.js calls it (print-frame part, the material's tunables set). Same seat
as computeFins, so the wall is bit-identical wherever the part sits; a wall it can't
build comes back with the site's reason ("wall too short ..."). Bridge: drawWallB64;
Python: host_draw_wall.

Tests pin it to the site's wall (PLA and PETG), plate-position independent, no
clearance leak into the next wall or the next computeFins.
2026-10-01 08:32:48 -05:00
matthewtrahan 2e7503aa43 shared: computeFins says which triangles are which fin (pieces) and which faces overhang -- for a host that shows one object per fin
A host that wants each wall as its own object (Blender next) had to call buildFins
itself to get the fin ranges, skipping the entry's seat and material clearances.
computeFins now returns pieces ({id, kind, ranges} in triangles of its soup; every
triangle in exactly one) and overFaces (input faces analyze() marks red). Python:
host_compute_pieces; host_compute is unchanged for Orca, Cura and FreeCAD.

The seat (centre + 1 nm snap) and the material clearances move to engine/seat.js
so the next entry (Draw mode) seats a part exactly the same way.
2026-10-01 08:32:48 -05:00
matthewtrahan 254b9dfa02 fusion: review nits -- pin the Y-up mapping (print = x, -z, y) and the -Z / near-axis beds 2026-10-01 07:40:54 -05:00
matthewtrahan b87759d404 fusion: the print frame no longer turns the part 90 degrees -- an XY bed hands the engine the part as the site sees it (v0.6.2)
PrintFrame.from_bed took x = n x ref, so a bed on the XY plane gave print x = world
+Y, y = world -X. The engine isn't the same under a turn about z: the headset got 95
walls / 144 tines in Fusion, 93 / 140 on the site and CLI. x is now ref projected onto
the bed, so an XY bed is the identity frame. Chairs, lampshade, bunny, gree, bear,
zombie, lighthouse and the headset now match the CLI.
2026-10-01 07:40:54 -05:00
matthewtrahan cc38bfa170 fusion: review nits -- 1 tine is singular; the async test matches the (N bodies) form 2026-10-01 07:34:05 -05:00