Skip to main content

Make stalagmites, not stalactites: mechanical enforcement of FDM print physics on STL geometry, with classified violations, prescriptive repairs, and constraint-aware orientation.

Project description

STALAGMITE — The transition IS the design

Make stalagmites, not stalactites. Stalagmites grow from the ground and stand; stalactites hang and need support. Stalagmite mechanically enforces that rule on your STL — then tells you how to fix the part, not where to put the scaffolding.

stalagmite interactive report: a failed flange ring shown in red with its repair suggestion alongside

The interactive 3D report (--report): click a defect, the camera flies underneath it, and the fix is spelled out with coordinates — "replace the flat with a ≥45° cone at least 10.1mm tall."

Start with HANDOFF.md for the full story; design principles are in DFAM_RULES.md; the literature behind every threshold is in LITERATURE.md.

The core rule as one test: every slice must lie within a dz·tan(max_angle) dilation of the slice below it. This single containment check subsumes overhang detection, floating features, and unsupported starts. Slicers paint overhangs red and scaffold around them; this tool exists to tell you the design is wrong, and (eventually) how to fix it.

Install

pip install .
# or manually:
pip install numpy trimesh shapely networkx scipy

(networkx and scipy are quiet trimesh requirements — slicing fails without them.)

Quick start — no command line needed

stalagmite-gui

Opens a local page in your browser with three tabs, so the whole toolkit works without the command line:

  • Audit — drop an STL, get the full interactive 3D report (open or download it)
  • Orient — find a better build pose, see the support reduction, download the rotated STL
  • Compare — drop two revisions, see what your change resolved / introduced

Runs entirely on your machine (localhost); nothing is uploaded.

Quick start — command line

# audit any part -- thread helices are detected automatically
stalagmite part.stl --auto-ex

# the clean reference part, with manual exclusion zones instead
python3 dfam_audit.py fixtures/06_clean_final.stl --ex 0:16.5:11 --ex 54:65:13

# write a copy of the mesh with violating faces painted red
# (open the .ply in any mesh viewer - MeshLab, f3d, Blender, PrusaSlicer)
python3 dfam_audit.py part.stl --ex 0:16.5:11 --export part_violations.ply

Diff two revisions (stalagmite-diff)

Iterating on a part? Compare the old and new STL and see exactly what your change did:

stalagmite-diff old.stl new.stl --auto-ex

Reports each defect feature as RESOLVED (gone), PERSISTS (still there, with any severity change), or NEW / NEW-FAIL (introduced), and a verdict of IMPROVED / REGRESSED / CHANGED / UNCHANGED. Exit code is 1 only if the change regressed (introduced or worsened a failure), so you can gate a parametric-CAD workflow on "my edit didn't break anything." It assumes both revisions share a coordinate frame (they're iterations of one part, not re-orientations).

Automating a "before I ship" gate

Machine-readable output for scripts and CI, three equivalent ways in:

Bash — exit code only (simplest gate):

stalagmite part.stl --auto-ex || { echo "not printable"; exit 1; }

Exit codes: 0 = printable (PASS / PASS_WITH_LIMITS / REVIEW), 1 = FAIL, 2 = could not audit (unreadable/empty mesh — with --json this is an {"status":"ERROR",...} object, never a traceback). Treat both 1 and 2 as "do not ship."

Bash — full JSON to log/parse (--json prints one object; exit code unchanged: 0 = PASS/PASS_WITH_LIMITS/REVIEW, 1 = FAIL):

stalagmite part.stl --auto-ex --json > audit.json
# audit.json: {status, printable, exit_code, profile, thresholds,
#   counts:{fail,judge,tolerable}, features:[{class,severity,z,
#   centroid,ledge_mm|roof_width_mm,repairs}], exclusions,
#   auto_zones:[{zlo,zhi,rmax,center,step_deg,lobes}], health}

Python — the same dict, plus the live objects:

import stalagmite
r = stalagmite.check("part.stl", auto_ex=True)
if r.failed:
    raise SystemExit(f"{r.status}: " + "; ".join(r.defects()))
r.to_dict()      # same shape as --json
r.to_json()

The JSON shape (and AuditResult.to_dict()) is the stable contract — gate and log against those keys.

Result states

The audit reports one of four truthful states — a deliberate bridge does not read like an unprintable floating boss:

state meaning exit
PASS every layer supported; no concerns 0
PASS_WITH_LIMITS prints as-is; only within-allowance ledges / surface notes 0
REVIEW printable, but contains judged bridge(s) — eyeball first 0
FAIL will not print as oriented; needs a fix or reorient 1

Only FAIL is a nonzero exit, so it's safe in CI while still distinguishing the printable-but-noteworthy cases in the status line and report.

Process profiles

There are no universal thresholds: the slice-containment principle is universal, but the numbers (overhang angle, bridge span, wall, ledge) depend on nozzle, layer height, material and machine. Profiles bundle them under a name, with provenance for each value.

stalagmite part.stl                              # conservative generic-fdm (default)
stalagmite part.stl --profile generic-fdm-fine   # 0.2mm-layer variant
stalagmite part.stl --profile-file myprinter.json  # your calibrated setup
stalagmite --list-profiles

You never have to pick a printer — the default is a documented, cautious generic-FDM profile. A named or custom profile just makes the advice machine-specific. Individual flags (--angle, --dz, --ledge-max, --bridge-max, --warn-angle, --min-wall) override any profile value. See docs/profiles/ for the JSON format.

Flags: --angle (default 45), --dz layer height (default 0.4mm), --ex zlo:zhi:rmax cylindrical exclusion zone for thread helices (repeatable), --export out.ply colored violation mesh, --suggest parametrized repair suggestions (Tier 3), --warn-angle 30 surface- quality lint (prints, but degraded downskin — Saunders' yellow band), --min-wall 0.8 thin-feature lint (Hinchy FFF minimum). Lint warnings never fail the audit.

Bridge features report the roofed width measured on the merged multi-layer region (the physical span being crossed); a roofed width beyond 10mm escalates the feature to fail severity.

Interactive 3D report (--report)

stalagmite part.stl --auto-ex --report report.html

One self-contained HTML file, openable in any browser, shareable as a file, fully offline (three.js is vendored inline). A rotating 3D view of the part with defect faces coloured by severity, beside a clickable defect list — selecting a defect flies the camera underneath it (defects are undersides) and reveals the Tier-3 repair suggestions. The clean part shows a green PASS.

Helix auto-detection (--auto-ex)

Thread helices legitimately migrate sideways along their flanks and would otherwise false-positive. --auto-ex recognises them from a first audit pass by their signature — many consecutive layers of small constant-area lobes whose centroids lie on a circle and advance by a consistent per-layer angle (the helix pitch) — then excludes the fitted cylinders and re-audits. Real defects don't share the signature (flat ledges are single wide layers; boss undersides are mirror-symmetric pairs; bridges last a couple of layers), and thread-runout ledges just above a helix are absorbed into its zone with a bounded growth cap. On the six-fixture regression suite, auto-detection reproduces the hand-tuned baseline exactly, and prints each zone in --ex form for reuse. New users: see GETTING_STARTED.md.

Tier 3: repair suggestions (--suggest)

Each defect feature gets concrete, parametrized fixes rather than "add supports". Fail-severity features are grounded by a reachability search: the highest solid a 45° hull can descend onto, reported with coordinates —

[fail] steep-growth  z 14.3  ledge 8.6mm
    -> morph the transition: replace the flat with a >=45 deg
       chamfer/cone at least 8.6mm tall (the transition IS the shape)
    -> or gusset down to the solid at (-1.3,10.2) z=13.1

Repair taxonomy: ground-it (hull/gusset to nearest solid or bed pillar), morph the transition, teardrop/diamond the opening, flatten/chamfer, accept (judged bridge / functional flat per DFAM_RULES #4 and #7), or reorient (Tier 4). Always re-audit after applying a repair — fixes can create new overhangs (Adam & Zimmer 2014).

From telling to doing: sculptable repair geometry

Fail defects don't just get advice — they get real, adjustable repair geometry, built by lofting between contours taken from the actual audited slices (a hex flange gets a hex-to-neck cone; a rectangular ledge gets its true rectangular footprint — never an invented primitive).

In the report, selecting a fail defect shows a translucent green repair body with three sliders — Height, Easing (straight chamfer → concave fillet) and Flare — so you sculpt the fix to taste. Physics is a hard constraint, not a suggestion: the height slider cannot go below what the profile's angle permits, a live badge shows the worst slope, and nothing can extend below the build plate. Download repair STL exports your sculpted body, union-ready.

From the CLI: --emit-fix repairs.stl writes the default (minimum, straight-chamfer) bodies for all fail features. Union them into the part in any CAD, then re-audit — on the regression suite this turns the worst fixture from FAIL into REVIEW with zero fail features left.

Repair primitives: the fix must respect the design

The repair generator picks its move from the defect's anchoring, not one-size-fits-all:

defect signature primitive what it builds
reachable support below loft morph down to real support contours
nothing below pillar ground the footprint to the bed
flat internal ceiling, anchored all around (bottle-cap roof) roof chamfer a corbelled cone ring climbing from the wall to the ceiling

The roof chamfer is the real-world bottle-cap case: a downward loft would plug the whole cavity and swallow the spigot bore. Instead the chamfer closes the opening inward layer by layer at the profile angle — the cavity stays hollow and bores stay open (it stops at the bore's edge). It's exactly what a designer would model by hand: chamfer the roof into a gentle cone. In the report the part goes x-ray while you sculpt the funnel; extra small roof holes that the cone may cover are called out in the notes.

Through-holes survive the chamfer: the main bore bounds the closure, and every secondary roof hole (vents!) gets a vertical clearance shaft drilled through the applied body — a vent that stops at the chamfer is not a vent. (True booleans via manifold3d, now a dependency; if drilling ever fails the notes say so honestly instead of covering the hole silently.)

The chamfer also respects functional wall geometry: exclusion zones (the threads you --ex'd or auto-detected) and keep-clear zones on the opening's wall clamp the chamfer's attachment ABOVE them. If a full cone no longer fits, a partial closure is used when its residual ledge stays within allowance — proven by re-audit. If even that doesn't hold, the automated fix refuses (never-worse) and tells you the truth: this cap needs its roof raised or its threads shortened — a design change (stalagmite-autofix can search for it if the design is parametric). The sculptor still shows the flagged partial ghost with a red badge so you can judge the droop yourself.

Repairs are not only for failures: in the report, every defect card is sculptable — tolerable ledges and judged bridges get a gold "Optional quality repair" panel (same sliders, same STL download) for when within-allowance still isn't the surface finish you want. Automation never applies optional repairs; they exist for you.

Fix it for me: stalagmite-fix

The full apply-and-prove loop in one command:

stalagmite-fix part.stl -o part_fixed.stl --auto-ex

It audits, builds the repair bodies, boolean-unions them into the part (manifold3d if available, graceful fallbacks otherwise), re-audits the result under the same profile, and issues a matched-feature verdict:

verdict meaning
VERIFIED every fail resolved, nothing regressed — proven by re-audit
PARTIAL fails reduced but some remain
NOTHING_TO_FIX no fail defects to begin with
NOT_IMPROVED repairs didn't help — output withheld (--force to override)

That last row is the never-worse guarantee: stalagmite will not hand you a "fixed" file that audits worse than what you gave it. --height / --easing / --flare shape the repairs (same parameters as the report's sculptor), --report fixed.html writes an interactive report of the fixed part, --json emits the machine object, and exit codes follow the audit contract (0 printable / 1 still failing / 2 error).

In the GUI, the report view has an Auto-fix & re-audit button: one click swaps in the fixed part's report with a verdict banner and a Download-fixed-STL button. Python: stalagmite.fix(part) returns the same result object (.verdict, .mesh_fixed, .write(path), .to_dict()).

On the regression suite: the worst fixture goes FAIL → REVIEW, 0 fail features, FIX: VERIFIED — the tool repairs the part and proves its own repair.

Fix the design, not the mesh: stalagmite-autofix

stalagmite-fix welds repair geometry onto a finished mesh. stalagmite-autofix goes one level up: hand it your parametric build function and the knobs you're willing to move, and it searches for the smallest move away from your intended design that makes the part printable — every candidate is rebuilt from source and fully audited.

A design file is plain Python (CadQuery, trimesh, whatever stalagmite.check accepts):

PARAMS  = {"shelf_len": (3.0, 10.0), "gusset": (0.0, 1.0)}
NOMINAL = {"shelf_len": 10.0, "gusset": 0.0}   # what you *wanted*

def build(shelf_len, gusset):
    return make_my_part(...)                   # CadQuery or trimesh

# then:
stalagmite-autofix my_design.py -o best.stl --budget 32

Search = the same numpy-only GP Bayesian optimisation as the orientation solver, followed by a homing pass that pulls each parameter individually back toward its nominal value, shaving off every bit of design change the physics doesn't actually demand. The winner is rebuilt fresh and re-audited before anything is written.

verdict meaning
NOTHING_TO_DO your nominal design already audits printable
FOUND a printable parameter set located and proven — the moves are listed
NONE_FOUND nothing printable in the box — output withheld (--force to override)

Output tells you exactly what moved and how far (shelf_len: 10 → 7.6 (−2.4), design moved 0.57 on a 0–1 scale). Python: stalagmite.autofix(build, PARAMS, NOMINAL) returns the same result object. Try it: stalagmite-autofix examples/autofix_demo.py — a design whose parameter box contains a trap (shrinking the shelf can never fix it; the search has to discover the gusset).

Design-intent tags: --keep

An exclusion zone (--ex) says "ignore violations here — I know better" (thread helices). A keep-clear zone says the opposite: "this region is functional — a seal face, a bore, a mating surface — and no generated repair may touch it." Same cylinder syntax:

stalagmite part.stl --keep 25:33:6:17.4:0        # zlo:zhi:rmax[:cx:cy]
stalagmite-fix part.stl --keep 25:33:6:17.4:0 -o fixed.stl

The audit flags any defect sitting on a tagged surface ([ON KEEP-CLEAR ZONE] in text, intent_hits in JSON, a purple badge in the report) — so you know a defect is on functional geometry before deciding how to fix it. stalagmite-fix then honours the tag twice: anchor walk-downs skip landings on tagged material, and any repair body that would still poke into a zone is dropped with a note instead of welded onto your seal face. If that leaves a fail unfixed, the verdict says so honestly (PARTIAL / NOT_IMPROVED) — intent never silently degrades the audit's truthfulness.

In the GUI there are no coordinates at all. Run an audit, and every defect card in the report gets a protect toggle. Click it and that defect's own geometry becomes the keep-clear zone; the viewer bar confirms "1 defect(s) protected — Auto-fix will keep clear", and Auto-fix reports any withheld repairs right in the verdict banner ("1 repair(s) withheld (protected)").

From Python, intent can live next to the design instead of in flags — including straight from CadQuery named faces:

zone = stalagmite.keep_zone(part.faces(">Z"), pad=1)   # the gland seat
stalagmite.fix(part, keep=[zone]).write("fixed.stl")

keep_zone() also accepts a trimesh, an (n,3) point array, or explicit bounds.

Tier 2: violation classification

Every violation is classified by in-plane anchoring and given a severity (thresholds are literature-sourced, see LITERATURE.md, and boundary- condition dependent — treat them as defaults, not physics constants):

class meaning severity
starts-in-air new body appears with nothing below fail
island unsupported below AND unattached in-plane fail
steep-growth cantilever ledge, one-sided anchor tolerable ≤1.8mm (Adam & Zimmer 2014), else fail
bridge anchored on opposing sides judge ≤10mm free span (Hinchy 2019), else fail

Per-slice violations are aggregated into physical defect features (consecutive layers, overlapping regions), so a six-fixture regression part reports "1 defect: judged bridge" instead of three slice records. The colored export encodes severity: red = fail, orange = judged bridge, gold = tolerable ledge. Bridges are still reported for human judgment — a deliberate design choice (see DFAM_RULES.md #7 and #10); note the per-layer free-span figure measures corbelling steps, which understates the physical hole diameter being roofed.

Tier 4: orientation solver (dfam_orient.py)

Searches build poses minimising support volume subject to what the part is for — the constraints plain auto-orienters don't know:

# thread must stay vertical; helix zones excluded from the proxy
python3 dfam_orient.py part.stl --axis-vertical 0,0,1 \
    --ex 0:16.5:11 --ex 54:65:13 --save oriented.stl

# a seal face must print as the floor
python3 dfam_orient.py part.stl --face 0,0,1:floor

Selecting a defect in the report also draws its transition diagram — a 2D cross-section showing the supporting slice below, the allowed 45° envelope grown from it, this slice, and the material poking past the envelope in red. It shows why the feature fails and what shape would have been allowed, not just which facets are wrong. For fail features it adds a one-line fix: morph the red back within the dashed envelope (the envelope outline is the exact allowed shape for that cross-section), or ground it to the solid below.

Face modes: floor (normal ends up facing down), up, wall (vertical), not-down (never support-scarred). Search is Gaussian- process Bayesian optimisation (Matérn 5/2, LCB), ~35 evaluations, after Goguelin, Dhokia & Flynn 2021; the objective is their support-ray-length proxy plus heavily weighted constraint penalties (15° of violation ≈ the worst-case support cost).

The proxy proposes, the audit disposes. By default the solver then re-ranks its 3 best distinct poses with the FULL containment audit (--verify-top K, 0 to disable): the winner is the pose with the fewest fail features, then fewest judged bridges, then the proxy score — so a pose that merely looks cheap to support can't beat one that actually passes physics. Exclusion zones are remapped into each pose's build frame when the pose keeps their axis vertical; tilted-axis poses are audited without them (conservative) and say so. The chosen pose's audit evidence rides along in the result (chosen_by, audit, verified).

Python API — import stalagmite

One call audits a part and hands back a result. It accepts a CadQuery object, an STL/OBJ/PLY/3MF path, a trimesh, or a (vertices, faces) pair:

import stalagmite

r = stalagmite.check(part, auto_ex=True)   # part = any of the above
print(r.status)          # PASS / PASS_WITH_LIMITS / REVIEW / FAIL
print(r.printable)       # True unless FAIL
for d in r.defects():
    print(d)
if r.failed:
    r.write_report("audit.html")
exit(r.exit_code)        # 0 printable, 1 FAIL -- gate a CI/build step

CadQuery: the design loop in code

Because a CadQuery model is a Python object, stalagmite audits it directly — no STL export, no round-trip. Build → audit → tweak a parameter → re-audit, all in one script:

import cadquery as cq, stalagmite
part = make_bracket(gusset=False)
if stalagmite.check(part).failed:
    part = make_bracket(gusset=True)      # change a parameter
stalagmite.check(part).write_report("ok.html")

See examples/cadquery_demo.py for a runnable version that lets the audit drive the fix — sweeping a taper parameter until the part prints (pip install stalagmite[cadquery]).

Lower-level

import trimesh
from dfam_audit import audit_mesh, export_colored
mesh = trimesh.load("part.stl", force="mesh")
bed_area, violations = audit_mesh(mesh, max_angle=45, dz=0.4,
                                  exclude=[(0, 16.5, 11)])
export_colored(mesh, violations, 0.4, "part_violations.ply")

Robustness

load_mesh() sanitises input (drops infinite/NaN coords and degenerate/ duplicate faces, merges coincident vertices, concatenates scenes, and auto-flips inside-out meshes) and mesh_health() caveats non-watertight meshes, inconsistent winding, suspicious units, and very high slice counts — so real-world and messy STLs are audited without crashing, with an honest note about how much to trust the result. A dedicated torture battery (test_robustness.py) keeps it that way: open meshes, inverted normals, NaN/inf vertices, self-intersections, disconnected shells, zero-height plates, garbage files, ASCII STLs and 20k-face organics all either audit cleanly or fail with a clear error (exit 2), never a traceback.

Tests

pip install pytest
python3 -m pytest test_fixtures.py

The six STLs in fixtures/ are the genuine failure history of one real part (a threaded pH-probe holder, v2→v7) with known defects — the regression baseline any refactor must reproduce. See fixtures/FIXTURES.md.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stalagmite-0.36.0.tar.gz (288.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stalagmite-0.36.0-py3-none-any.whl (278.4 kB view details)

Uploaded Python 3

File details

Details for the file stalagmite-0.36.0.tar.gz.

File metadata

  • Download URL: stalagmite-0.36.0.tar.gz
  • Upload date:
  • Size: 288.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for stalagmite-0.36.0.tar.gz
Algorithm Hash digest
SHA256 4479979755a08831a1666e7c800af154d2b0ff86bcb483e76740db438b59b352
MD5 61d69673152ff1e0c37a21989ad153a6
BLAKE2b-256 b5e01a8ae451b0022f8d2b0bc3072e9ec00f0ee77c6b3bc708385eae1df8cc4d

See more details on using hashes here.

File details

Details for the file stalagmite-0.36.0-py3-none-any.whl.

File metadata

  • Download URL: stalagmite-0.36.0-py3-none-any.whl
  • Upload date:
  • Size: 278.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for stalagmite-0.36.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2bbf01ae23db2d3f7484d0914dc06f085db7ddcb2d42b7f460ef2981ff301aa1
MD5 4820403ae557ab2a1221853f3b84151a
BLAKE2b-256 49fb5f1f3b06a7a829eedb9ce74c69e804ac55d5c5c8669e639d3f7bf0ad7eac

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page