Derive per-path line height and extrusion width from the moves - #462
Draft
sophiedeziel wants to merge 8 commits into
Draft
sophiedeziel wants to merge 8 commits into
sophiedeziel wants to merge 8 commits into
Conversation
This was referenced Sep 7, 2026
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
from
September 7, 2026 07:14
388e983 to
69c7723
Compare
|
Visit the preview URL for this PR (updated for commit c68460c): https://gcode-preview--pr462-cura-derived-dimensi-p415l5p1.web.app (expires Wed, 07 Oct 2026 21:19:34 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 59bd114ae4847b32c2bba0b68620b9069a3e3531 |
sophiedeziel
marked this pull request as draft
September 7, 2026 07:17
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
2 times, most recently
from
September 7, 2026 16:46
a4cb4d8 to
b2b36be
Compare
3 tasks
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
from
September 7, 2026 17:05
b2b36be to
ab12f4f
Compare
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
from
September 7, 2026 17:19
ab12f4f to
f5a118b
Compare
sophiedeziel
added a commit
that referenced
this pull request
Sep 7, 2026
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
from
September 7, 2026 18:01
f5a118b to
032f5bc
Compare
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
from
September 7, 2026 18:29
24c77b0 to
cabb24c
Compare
- SlicerMetadataParser gains derivesExtrusionDimensions and parseFilamentDiameter, both defaulting to no-ops; dialects that announce exact ;WIDTH:/;HEIGHT: comments are unaffected. - CuraMetadataParser opts in: Cura emits no dimension comments at all, so per-path width and height can only be reconstructed from the moves. The volumetric UltiGCode flavor is excluded (its E values are mm3 of material, not mm of filament). - The filament diameter is read from the header ;FLAVOR: line only: Griffin is exclusive to Ultimaker's 2.85mm machines. Cura's own material_diameter setting sits in the ;SETTING_3 blob at the end of the file, after every move it would apply to; using it would reach a one-shot parse before any move executes but a streamed parse only after every move already ran, breaking streamed/one-shot equivalence, so it is deliberately not used. - Parser.parseGCode forwards both as metadata.deriveExtrusionDimensions and metadata.filamentDiameter, evaluated once on the chunk that identified the slicer, like detectSlicerName.
- Job.deriveMoveDimensions runs on every extruding linear move when the metadata asked for derivation, before continuePath — so a derived change breaks the path exactly like a ;WIDTH:/;HEIGHT: comment would, and the derived values take the same render-time precedence over the global fallback (they are the path's own values). - Layer height is the Z step between consecutive extrusion moves; the first move's Z stands in for the first layer. Comparing extrusion Zs only makes z-hop travels invisible; a zero step (ironing) or a step outside 0.01-1.0mm (spiralize's micro-climb, sequential objects) keeps the current height while re-anchoring the reference Z. - Width comes from conservation of volume with a rectangular deposit cross-section — width = deltaE x filament cross-section / (length x height) — matching the box profile ExtrusionGeometry extrudes. The filament cross-section comes from metadata.filamentDiameter (1.75mm when unannounced). Results are quantized to 0.01mm and held within a 2% relative tolerance of the current width so E-value rounding noise cannot shatter the model into micro-paths; implausible results (outside 0.1-2.0mm, or over a segment shorter than 0.05mm) are discarded so the path falls back to the global setting. - All derivation state (extruder position, last extrusion Z, current derived dimensions) lives on Job/State, which persist across streamed chunks: the ingestion-equivalence harness gains a Cura fixture whose layer change and width shift land mid-chunk in every chunked mode. - Round-trip tests generate Cura-style gcode from known target dimensions via the inverted volumetric formula and assert the derived per-path values recover them, covering adaptive layer heights, z-hops, G92 E resets, relative extrusion (M83), ironing and spiralize.
Security review finding: coordinates and E values that are individually finite (and so survive the parser's param validation) can overflow the derivation's intermediate arithmetic — Math.hypot to Infinity, or NaN via Inf - Inf — and Infinity/Infinity makes the width NaN, which a plain less-than range check waves through and latches into the state, breaking the path on every extruding move after it. Guard the length with Number.isFinite and write the width plausibility check in inclusive form (matching the height check) so NaN is rejected.
The filament diameter and the derive-dimensions flag are read from the ;FLAVOR: line, which Cura writes first -- but the comment that identifies the slicer comes a few lines later, so a streamed parse could identify on a chunk that no longer held the flavor. The answers then depended on chunk size: Griffin lost its 2.85mm diameter, scaling every derived width by (2.85/1.75)^2 = 2.65x, and an UltiGCode file wrongly enabled the derivation its volumetric E values do not fit. The parser now retains the file's first 200 comments across chunks and asks the header questions against that sample.
Deriving dimensions from the moves is useful for any gcode that does not announce them -- Simplify3D, Slic3r without verbose comments, hand-written files -- so it is now the default, and a dialect only opts out when the arithmetic does not hold (Cura's UltiGCode, whose E is cubic millimetres). Gating on "the file announced no dimensions" would have been the obvious way to say that, but it decides mid-stream: a Prusa file whose only fingerprint is ;LAYER_CHANGE is identified after its start gcode has already extruded a prime line, so a streamed parse would derive for that path and a one-shot parse would not. Nothing flips instead -- derivation always runs, and an announced dimension outranks the derived one path by path (State.resolvedExtrusionWidth), which is chunk-order independent. The width derivation is skipped while a width is announced, since it would lose the comparison anyway: without that, deriving cost a third of the interpret time on 3DBenchy for byte-identical output.
sophiedeziel
force-pushed
the
cura-derived-dimensions
branch
from
September 7, 2026 18:33
cabb24c to
78e9b2b
Compare
The docs and test names still framed deriving as something Cura opts into, which stopped being true when it became the default for every dialect. The remaining mentions are the ones that earn their place: the Cura parser itself, and Cura as the concrete example of a dialect whose E is volumetric or whose header arrives before its fingerprint.
Asking for an extrusion width through the public API is a decision; deriving one from the moves is an inference, and the decision should win. The derivation now skips whichever dimension the preview was constructed with, so those paths carry no dimension of their own and the supplied value applies at render time. The slicer's own ;WIDTH: / ;HEIGHT: comments still outrank both. A supplied height still feeds the width derivation, which needs the height the material was laid at rather than an inferred one, and the Z anchor keeps advancing even when the height is not being derived -- a test now covers that, since hoisting the width shortcut above the height block silently passed the whole suite before. Also pins State's surface in the public API test: it is reachable as job.state, so its dimension fields are public whether or not the class is exported.
The derived width and height sat on State next to the announced pair, but nothing outside Job ever read or wrote them: the derivation writes them, breakPath and continuePath read them, and that is the whole story. On State they were public surface -- reachable as job.state -- and a rename would have been a breaking change for a value that is really the Job's working note. Two fields are still worth keeping over one plus a provenance flag: announcements arrive from beginCommand and derived values from the move handler, so separate slots let them be written in any order without one clobbering the other. The derivation tests now assert through the dimensions a path actually carries rather than through those fields, which is the behaviour they meant to describe anyway.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A file that announces no
;WIDTH:/;HEIGHT:comments gets one global width and height for the whole print, however much its extrusions actually vary. Everything needed to reconstruct them is already in the moves, so this derives them:ΔE × filament cross-section / (length × height)— using a rectangular deposit, matching the box profileExtrusionGeometryactually extrudes.Deriving is the default for every dialect, not a Cura feature: Cura is the motivating case, but Simplify3D, Slic3r without verbose comments and hand-written gcode all lack dimension comments too. A dialect only opts out when the arithmetic itself does not hold (Cura's UltiGCode flavor, whose E is cubic millimetres of material rather than millimetres of filament).
Precedence per dimension is announced > supplied > derived > built-in default. The slicer stating what it asked the printer for outranks everything; a value you pass to the constructor is a decision and outranks the inference, so the derivation simply does not run for it; deriving fills what is left. That keeps
extrusionWidthandlineHeightmeaningful options rather than ones the derivation quietly takes over.That precedence is also what keeps streaming honest. Gating on "this file announced no dimensions" would decide mid-stream: a PrusaSlicer file whose only fingerprint is
;LAYER_CHANGEis identified after its start gcode has extruded a prime line, so a streamed parse would derive for that path while a one-shot parse would not. Nothing flips here, so streamed and one-shot agree by construction.Stacked on #461, which supplies the true extruded lengths this needs.
Behavior changes
;WIDTH:, now carry a derived width, unless the caller supplied one.;FLAVOR:before the line that identifies it, so under streaming those could land in different chunks: Griffin silently lost its 2.85 mm diameter, scaling every derived width by (2.85/1.75)² = 2.65×, and UltiGCode wrongly enabled derivation.Implausible results are discarded rather than rendered: heights outside 0.01–1.0 mm, widths outside 0.1–2.0 mm, and segments under 0.05 mm where E quantization dominates. Derived widths are quantized to 0.01 mm with a 2% tolerance so rounding noise cannot shatter the model into micro-paths.
Deriving costs nothing on files that announce their widths — the volume arithmetic is skipped while an announced width is in force, which is worth about a third of the interpret time on 3DBenchy.
Public API changes
Two new overridable hooks on
SlicerMetadataParser:derivesExtrusionDimensions(defaults totrue; a dialect returnsfalseto opt out) andparseFilamentDiameter.Metadatagains optionalderiveExtrusionDimensionsandfilamentDiameter.Stateis unchanged: the derived dimensions live insideJob, since nothing outside it reads them. Its surface is now pinned in the public API test all the same, being reachable asjob.state.JobacceptsextrusionWidth/lineHeight, the dimensions the caller supplied, andJob.deriveMoveDimensionsis new. No renames or removals.Screenshots
Five first-layer lines printed at 1.2 / 0.9 / 0.6 / 0.4 / 0.25 mm, same camera:
Heights come out of the Z steps, so the tower's three tiers stack visibly differently — 0.3 mm at the bottom, 0.2 mm in the middle, 0.1 mm on top:
Checks
npm run checkpasses (test + typeCheck + lint)npm run test:coverage—src/still at 100%enhancement)Assisted by Claude Code - Fable 5