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>
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>
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>
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.
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.
* 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>
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>
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.
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.
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 /.
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).
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.
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.