24 Commits
Author SHA1 Message Date
matthewtrahan a704c9c1f6 import: OBJ files on the site -- a text reader (polygons fan, v/vt/vn and negative refs, o splits objects into the picker, groups and materials don't), sniffed by content like STL/3MF/STEP; no unit, read as mm 2026-10-16 10:00:56 -05:00
matthewtrahan 25a317722e mcp: build it in CI, drive it over stdio on Node outside the checkout, and publish support-fins-mcp.mjs with the other plugins 2026-10-15 14:27:57 -05:00
matthewtrahan 601c18c665 picks: every Mesh raycast through three-mesh-bvh (vendored 0.9.15, MIT) -- a Draw hover on a 1.56M-face mini took 75 ms a pointer move, now under 1 ms with the same hits; the BVH builds on a big mesh's first pick 2026-10-14 21:05:50 -05:00
matthewtrahanandPer Hjort 877111da59 nozzle: review fixes -- the tine reach grows past a wider contact tip (sphere at 45deg: 6 tines at 2.0, now 26; 30 at 0.4), walls stop at 6 mm so a row never fuses, nozzles to 2.0
The reach is measured from the wall's centre, so a tip wider than 0.4's 0.6
swallowed the 0.5 reach and a curved underside lost its tines; the extra
half-tip is added to it (0.15 at 0.6, 0.3 at 0.8, 1.2 at 2.0; 0.4 and finer
unchanged). A wall over 6 mm -- half the 12 mm row spacing -- fused rows into
one block (3.0 x 8 lines: 6.7 kg on a 252 g plate), so walls and wedges cap
there, the page offers only the Wall lines that fit the nozzle, and the
nozzle stops at 2.0 (2.5's two-line wall is already 6.25). tests: every
offered nozzle x lines stays under half the row spacing; the sphere keeps 70%
of its 0.4 tines at 2.0 with nothing inside the part.

Co-authored-by: Per Hjort <35040883+HjortCreations@users.noreply.github.com>
2026-10-14 12:30:30 -05:00
matthewtrahanandPer Hjort e44cb1d846 nozzle: large-format sizes 1.0-2.0 mm, and a nozzle guide -- the same scaling rule (a 2.0 nozzle builds a 5 mm wall); layer height stays capped at 0.6
From Per Hjort's #210, which took nozzles up to 3 mm (2.0 here, see the review fix). Layer height stays at
0.6: the gap, flanges and pad are fixed heights tuned for thin layers. The
guide (docs/NOZZLE.md, adapted from #210's) lists the widths per size, the
limits (thicker walls fit fewer places) and what to check in the slicer. 0.4 is
unchanged (sweep vs main: no mesh changed).

Co-authored-by: Per Hjort <35040883+HjortCreations@users.noreply.github.com>
2026-10-14 12:30:30 -05:00
Per Hjort a5170233d9 Allow optional Sway depth beyond the existing 60 mm cap (#241) 2026-10-13 07:38:43 -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
MorbidJ-hubandClaude Opus 5.5 cdf29ade53 fusion: Insert Support Fins, engine run in a hidden palette (v0.5.0)
The Fusion add-in, cut down to Insert Support Fins and moved off mini-racer.

mini-racer crashes Fusion on macOS (Fusion ships its own libv8 and macOS merges
V8's weak symbols across the two copies), so the website's engine now runs in
Fusion's own browser, in a hidden palette, as the engine-test spike on #83
showed. Nothing native ships: one folder for Windows and macOS.

Python never waits on the palette (its JavaScript runs on Fusion's main
thread). The palette pulls the latest job ('next'), runs computeFinsB64 and
posts the answer ('result'); a newer job replaces one not yet picked up, and a
late answer for an old one is dropped. Python nudges it with
sendInfoToHTML('wake'); the palette also polls (100 ms with the dialog open,
1 s without). When an answer lands, a custom event refreshes the readout and
calls doExecutePreview(), as sway_command did for picks.

- engine_host.py: PaletteEngine, encode_job/decode_result; compute_fins()
  keeps its signature and runs the same bundle under Node, for the tests. The
  mini-racer vendoring, first-run download and SHA pins are gone.
- palette/engine.html + engine.js: the hidden page (a status line if shown).
  Its URL is a file:/// URI: on Windows a bare path reached the browser as
  file:///C:%5CUsers... and the page never loaded.
- fins_command.py: _compute is asynchronous (pending, then _finish);
  Insert stays disabled until the fins are in.
- sway_core/ and Insert Sway Brace are dropped, since sway braces are moving
  onto the shared engine; bodies tagged as braces by 0.4.x still count as
  supports.
- build.py only bundles the engine here; packaging and CI follow separately.

Run in Fusion on Windows (2705.1.25): palette ready while hidden; L-bracket at
35 deg gets 4 fins / 20 tines / bed pad with a live preview, a settings change
re-runs it, Insert adds the tagged bodies. Not yet run on a Mac.

Tests: 34 pass (palette protocol, asynchronous dialog, engine under Node).
The sphere test now checks the pad is its own body: since the small-tube and
raster-wall work the engine may add walls under a sphere too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:17:59 -05:00
matthewtrahan 2aa86a4b70 import: load the STEP kernel while the user picks a file, cache it a year
The pen holder (195 KB SolidWorks STEP) tessellates in 0.3 s; the rest
of a 1.7-3.4 s cold import was the ~2.9 MB kernel download. Start the
worker when Import is clicked or a file is dragged over, so the download
overlaps with the file dialog. The vendor folder now carries the version
(occt-import-js-0.0.23) and web/_headers marks it immutable for a year.
A failed read drops the worker so the next attempt starts clean.
2026-09-24 16:15:26 -05:00
matthewtrahan db5bbdd07c import: open STEP files (#24)
STEP is exact B-rep, so it goes through the Open CASCADE kernel in a
worker, tessellated at 0.01 mm chord / 0.1 rad so the faceting is below
print resolution. The assembly tree maps to the same objects the 3MF
reader returns, so a multi-body STEP uses the existing object picker.
Detected by content (ISO-10303-21 magic), like 3MF.

The top-bar button is now just "Import"; the file dialog filters to
.stl/.3mf/.step/.stp.
2026-09-24 16:15:26 -05:00
matthewtrahan de6ef783f7 docs: list Orca under plugins/, fix a typo in the Onshape README 2026-09-24 14:36:10 -05:00
Chris Lee 3ba73ec1a4 Update references to the old plugin/ path
- README.md: plugin section, layout block
- plugins/prusa/README.md: install path, run-tests.sh location
- plugins/prusa/tests/add_fin_test.lua: run instructions
- .dockerignore: plugin/ -> plugins/
2026-09-24 14:35:55 -05:00
94668035f2 Sway braces: optional support that keeps tall parts from drifting and wobbling (#29)
* sway: add optional sway braces for tall parts

A tall, slender part drifts, sags and wobbles as it grows, and each movement
leaves a layer line. Sway braces are tapered buttress ribs standing edge-on to
an upright side, tied on by one-layer tines spaced evenly all the way up.

- web/sway.js: rib geometry (depth and thickness scale with height, exact
  obstruction test, foot flange), even tine comb with a grip floor, auto
  placement on the tallest columns of up to 4 faces, and a height-by-height
  clash check so braces on facing walls can't fuse into one bar.
- Options panel: "Sway braces (tall parts)" with Brace grip from / Brace tine
  spacing / Brace depth, shown only while on (same pattern as Tines).
- Auto places them; Draw adds one per click on an upright side. Click a
  hand-placed support to select it, Delete / "Remove selected" removes it,
  Undo restores it.
- Gap and bite are passed in opts because the build runs in a Worker that
  never sees the page's FIN/PROP changes.
- tests/sway.test.js: 10 tests on plain blocks (no stress models needed).

Not yet test-printed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* readme: describe sway braces for tall parts

What they are, how to use them in Auto and Draw, the three settings, the
clash rule, why the shape differs from the Brace fin, and their status
(first trial on a 249mm fence cap; defaults unconfirmed until a print).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: add handoff for a Fusion add-in that inserts sway braces

A self-contained brief for starting the Fusion add-in as its own project:
scope (sway braces first, overhang fins later), where the design lives,
the full brace spec and algorithm, Fusion decisions and gotchas, the fence
cap reference results, tests to port, and open items (print pending).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: update handoff commit IDs after rebasing onto main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: bring the Fusion handoff up to date with the add-in's progress

Copies in the add-in project's current handoff: status of the working
add-in (v0.2.1), where the build departed from the plan, the Fusion
gotchas found, ported tests, and revised open items and next steps.
Adjusted for this repo: the add-in's own repo is referred to generically,
and its file paths are marked as belonging to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: sway braces are print-confirmed

Two test prints (2026-09-22), both clean, so the defaults are the printed
numbers rather than first estimates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record what the sway-brace test prints actually were

Fence cap in ASA, braces hand-placed (three on one print, five on the
other), snapped off by hand, tines left small bumps, drift gone. Notes
that ASA has no material profile yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: note the test prints ran on the PLA profile at stock defaults

ASA released cleanly with PLA's gap/bite, tine spacing 6mm, depth 15%
and 0.2mm layers, all unchanged from the shipped defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* sway: give each auto brace a fin record so the per-fin filter keeps it

Per-fin removal (#15) makes the Auto view draw and export only the triangles a
built.fins record claims. Sway braces were appended to built.triangles with no
record, so after rebasing onto main they were counted in the readout but never
shown or exported. Each brace now carries its triRange and a kind:'sway' record,
which also makes it removable like any other Auto fin.

* sway: the gap/bite comments no longer blame the Worker

#30 sends the material's clearances with every build request, so the Worker now
sees them. Passing gap/bite to sway.js as options is still how it gets them; the
comments just said why wrongly.

* sway: keep braces clear of the other supports, not just of each other

Two overlap cases from review, both able to fuse a brace to something it
must break away from:

- Hand-placed braces weren't checked against AUTO's braces. Auto builds in
  the Worker, so the page never knew where they stood: a clicked brace could
  land on one, or face it across a channel. The brace outlines now come back
  with the build (built.sway.braces) and app.js feeds the visible ones --
  minus any the user removed -- into what swayAtFace avoids.
- Braces weren't checked against prop walls or wedges at all. buildSwayBraces
  now takes their centrelines (every fin record already carries `line`) and
  skips any spot whose foot lands within clearance of one; a hand-placed
  brace gets the same test and says "a support already stands there".

Both compare footprints at the bed, where each support is widest and tapers
inward with height, so clearing there clears the whole way up.

Also fixes a crash this uncovered: buildSwayBraces' early return had no
`ribs` list, and the per-fin records added in 859d7c8 map over it -- so any
part too short to brace threw with the option on.

Tests: a wall laid along a brace's own foot reads as a clash, a brace on top
of one is refused, a distant wall doesn't interfere, and a too-short part
builds quietly. Swept the stress models at 0/25/40 degrees: 77 braces
placed, closest brace-to-wall clearance 1.21mm against a 1mm floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* sway: refuse a brace that stands too far up before its first tine

Raised on #29: a part tilted onto a corner puts every side high off the
plate, so the brace below its lowest grip is a lone wall -- printing for
hours holding nothing, held by nothing, free to wobble next to a part at
its most delicate.

A rib may now stand at most 40mm, and at most 40% of its height, before its
first tine. Measured from the plate, or from "Brace grip from" when that is
higher: a user who asks to grip only above 80mm has chosen that stilt and
this is not the place to overrule them. What it catches is the stilt the
geometry imposes, and the refusal says which.

On the fence cap this also improves the result rather than only refusing:
the column at the gable peak, whose face starts high, is refused and the
nudge finds a neighbouring column that grips lower -- same 4 braces, 102
tines -> 120.

Tests: a block on a 70mm pedestal is refused (auto places nothing), and
"grip from" 80mm still builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* sway: the stilt limit binds auto, not the hand

Refusing a hand-placed brace where the face starts high was the wrong call:
the person picked the pose and can see what the software can't, and every
other placement decision here follows that split.

So auto still won't stand a rib more than 40mm (or 40% of its height) below
its first tine, but swayAtFace builds one and returns `stilt`. The readout
then says what it is doing -- "one brace stands up to 70mm before gripping
the part -- that much of it prints as a lone wall" -- above a 20mm quiet
threshold. allowStilt:false still holds a hand-placed brace to auto's rule
for anyone who wants it.

Checked on a block raised 70mm on a pedestal: auto places its props and no
braces; a click builds a 149mm brace with 14 tines and the note appears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: drop the Fusion add-in handoff from this PR

Matthew's review asked for it, and it belongs with the add-in rather than
here: it is a brief for a separate project, not a spec for this repo. The
document lives on in that project's own repo, where it is maintained.

Nothing else references it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Mitch Milam <mitch@parittagroup.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: matthewtrahan <mathyout@pm.me>
2026-09-24 07:50:01 -05:00
Matthew Trahan 44c7cd3d1f Merge pull request #26 from gittrahan/feat/3mf-production-import
3MF import: read the production extension + pick which object to fin
2026-09-22 10:48:34 -05:00
mintech-deandClaude Opus 5 293ecbffe0 3mf: open a 3MF
The app exported 3MF but only read STL (accept=".stl", STLLoader the only
loader in the tree), so anyone modelling in Fusion, Bambu Studio or
FreeCAD had to re-export first -- backwards, since 3MF states its units.

zip.js gains a reader. We emit STORE but real 3MFs are DEFLATE, so
inflate comes from DecompressionStream, which Deno has too, letting the
tests exercise the browser's path. ZIP64 is handled: writers enable it
regardless of size, and its extra field holds only the fields that
overflowed, so assuming all three mis-parses the offset-only case.

threemf.js gains readThreeMF(). Three things make it work on a real file
rather than only on our own export:

  - UNITS: the unit attribute is authoritative and not always millimetre;
    everything downstream is mm.
  - TRANSFORMS: <build><item> and <component> matrices compose with the
    object's own, or the part imports offset from the plate.
  - MULTIPLE BODIES: a plate can hold several and this tool fins one, so
    they are merged and the status rail says so, along with any unit
    conversion and any support body left out.

Parsed with a tag scanner, not DOMParser: the tests run outside a browser,
and a scanner cannot resolve an external entity from a stranger's file.
Objects typed support, surface or other are skipped -- not closed bodies,
and they would poison the overhang analysis.

app.js sniffs the ZIP magic, not the extension, so a renamed file still
parses. The reader returns the same flat array STLLoader does, in mm, so
setPart and the engine are untouched.

tests/threemf.test.js covers the round trip, units, composed transforms,
DEFLATE, both ZIP64 shapes, support exclusion, and malformed packages. A
3MF-sourced part analyses identically to the same part loaded as STL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 09:00:52 +02:00
kodin00 d13b8f505e dev: configurable bind address for dev-server (--host)
Add a --host option to both dev-server.py entry points so the dev
server can be reached from other devices on the LAN (e.g. a headless
Raspberry Pi behind a firewall). Default stays 127.0.0.1 (localhost);
pass --host 0.0.0.0 (or ::) to expose it. When bound to a wildcard,
the script also prints the machine's LAN address to open.

Both scripts move to argparse, so --help documents the options and bad
input errors cleanly instead of tracebacking. The positional [port] is
unchanged for backwards compatibility (python3 dev-server.py 8080).

web/dev-server.py previously bound "" (all interfaces) by default; it
now defaults to 127.0.0.1 to match the safe-by-default principle and
the other entry point. Use --host 0.0.0.0 for the old behavior.

stdout is flushed before serve_forever() so the startup lines (and the
LAN hint) land immediately in a redirected log.

README documents the new option.
2026-09-22 13:07:02 +07:00
matthewtrahan 645441ed2d docs: explain the benign 'no config, geometry only' slicer notice (#3)
Bambu Studio and PrusaSlicer both show a notice and import geometry-only
for any 3MF without an embedded slicer profile -- which ours is, on
purpose. The geometry imports correctly (right orientation + size, fins
intact); the notice is cosmetic. Document that in the landing steps, the
Export 3MF tooltip, and the README, rather than embedding per-slicer
profiles: doing so would silence the notice but overwrite the opener's
own printer/print settings on load, and would need re-authoring per
slicer and per slicer version -- a worse trade for a public tool.
OrcaSlicer opens the file with no notice at all.
2026-09-21 14:17:42 -05:00
kodin00 b401a656d1 docker: add compose + nginx image for one-command local deploy
The web app is a buildless static SPA (three.js is vendored in web/), so the
deploy image is just nginx serving web/ on :80. This adds:

- Dockerfile: nginx:stable-alpine, copies web/ + LICENSE, OCI labels.
- nginx.conf: port 80 site config that mirrors web/_redirects
  (/index.html and /index 301 -> /) via a $request_uri guard that cannot
  loop with the `index` directive's internal redirect; cache policy mirrors
  dev-server.py (no-store on the app's own JS/CSS/HTML) while long-caching
  the vendored three.js tree (immutable per image tag); absolute_redirect
  off so the redirect stays relative behind a reverse proxy.
- docker-compose.yml: one `web` service, host 8731:80 so the URL matches
  `python3 dev-server.py`, with a wget healthcheck.
- .dockerignore: excludes prototype/, plugin/, tests/, docs/, the dev
  servers and model files so the build context is just the web artifact.
- README: "Run with Docker" block pointing at the new files.

Validated: image builds, `nginx -t` passes, GET / -> 200 no-store,
GET /index.html|/index -> 301 relative /, GET /vendor/... -> immutable
long-cache, no redirect loop on /.
2026-09-21 20:29:14 +07:00
matthewtrahan b9216939e7 readme: mark the PrusaSlicer plugin as exploratory / not currently working 2026-09-11 09:23:27 -05:00
matthewtrahan 0e38ae2e1d readme: update for shipped web app + plugin; add live link, local-run, Ko-fi 2026-09-03 13:51:24 -05:00
matthewtrahan 90a19e0c1c Combined fin mode: re-expose the tined support in the UI + fix its export
The tined 'combined support' (fins.js mode 'stabilize' -- wall + ellipse base +
horizontal tines that fuse laterally into the part) was fully built but demoted
from the UI in early v1, and activeAdded() hardcoded finMode==='prop' so even if
reached its geometry never exported. Re-add it as a third Placement option
('Combined fin -- tined, grips the part') and gate the export on 'not draw'
instead of 'prop only', so both auto modes bake their walls into the STL/3MF.

This is the Slant3D 'support fin' proper: a tine printed as one continuous layer
line, horizontal to the plate, so it fuses and snaps clean -- vs a perpendicular
tine on a leaning face, which is the failure mode he demos. Verified live
(readout '1 fin - 14 tines', fin renders, receipt grams, export +364 tris, no
console errors) and headless across the dev matrix (watertight, standoff
0.200mm, every tine exactly 0.300mm tall in Z = single layer horizontal).
2026-08-13 16:27:35 -05:00
matthewtrahan 6e98499622 Name the project Support Fins; clarify license scope
Drops the Edgewise working name in favour of the repo name. Adds an explicit
note that the MIT license covers the tool and not its output, since the first
question anyone asks is whether running a model through it encumbers the model.
2026-07-24 19:25:22 -05:00
matthewtrahan 19b80e639a Initial commit: fin spec, prototype, project skeleton
Prototype validates the geometry engine against third-party STLs: overhang
detection, region segmentation, bed-reachability, a fin wall swept along a
curved contact line, and orientation scoring with a load-direction model.

docs/FIN-SPEC.md records the verified fin geometry — standoff, tine
dimensions, tine count, base, chamfer — with the reasoning for each.

Known gap: the prototype has no tines yet, which is the fused connection that
makes a fin a combined support. Without it a fin only holds the part in one
direction. That is build item #1.
2026-07-24 19:25:22 -05:00