Skip to content

feat(cad): draw G-code toolpaths of 3D printers and CNC machines - #1461

Open
Ugoyo wants to merge 4 commits into
KnifeLemon:mainfrom
Ugoyo:claude/gcode
Open

Ugoyo wants to merge 4 commits into
KnifeLemon:mainfrom
Ugoyo:claude/gcode

Conversation

@Ugoyo

@Ugoyo Ugoyo commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator

What does this change?

Filee can now read G-code, the files slicers (PrusaSlicer, Cura, OrcaSlicer, Bambu Studio, SuperSlicer …) write for 3D printers and CAM programs write for CNC machines, and turn them into a picture, an SVG or a PDF. Today nothing previews a .gcode without opening a slicer; people want a quick picture for a print log, a shared folder or a printer farm, and CNC users want to see a program before running it.

  • Extensions: .gcode, .gco, .g (RepRapFirmware macros) and .ngc (LinuxCNC). .nc is left out on purpose: as [Format]: .nc #562 showed, it is also NetCDF (Format request: .grib #244). .tap is left out too (it is also a cassette image of old home computers).
  • Drawing: the toolpath from above, or isometric (a new preset option, see below), at its real size. Extrusion (printer) or cuts (CNC) are coloured by height from dark to light, so layers and depths read apart; travel moves are thin blue lines, CNC rapids thin red lines. The start G-code before the first layer (the purge line at the edge of the bed) and the parking moves after the last extrusion are not drawn, so the picture is framed on the part.
  • The slicer's preview: most slicers save a preview in the file (; thumbnail begin … ; thumbnail end, PNG, JPG or QOI). With the new preset option set to it, pictures are that preview (the largest one); a file without one gets the toolpath from above, with a notice. SVG and PDF always show the toolpath.
  • Read: G0/G1, G2/G3 arcs (I J K or R, G17/G18/G19 planes, helical), G20/G21, G90/G91, M82/M83, G92, G28, modal moves, N line numbers, checksums, ; and ( ) comments. Klipper and RepRapFirmware commands are skipped. Details and limits are in docs/ENGINES.md (new "G-code" section).

Fields (docs/FORMAT-RULES.md): G-code is a read-only CAD format belonging to the 3D models and CAD fields (slicers for printers, CAM for CNC), graded specialist. It adds an input, no new output, so rule 5 doesn't block it. It opens on the CAD donut, whose slots (PDF, PNG, SVG, DXF, DWG, and JPG, WEBP, TIFF, Merge PDF on the back) all work for it: DXF/DWG go through the SVG, as the toolpath's lines. The new preset card is hidden while both of its fields are off, like its files' slots.

Preset option: GcodeOptions.View (Top, Isometric, Thumbnail), saved by name and read leniently like CadOptions. Shown for pictures, SVG and PDF, after the CAD card, with its texts in all 22 languages.

Migration: existing libraries learn the four extensions on the CAD donut in settings schema 87 (custom claims stay put). Main moves fast, so this number may need renumbering when merging.

Defensive reading (as asked on #1384): a damaged file fails with a clear InvalidDataException (no moves, only moves, a coordinate past 1 km, binary data, a .bgcode renamed .gcode). Budgets are checked before allocating: file size (1 GB), points to draw (12 million after merging points closer than 0.02 mm), line length (4096 characters, the rest of a longer comment skipped), arc segments (720), thumbnails (4096 × 4096 and 32 MB of base64, the image header read before it is decoded). Cancellation is checked while reading.

Speed: SVG is written as text; PDF and pictures are drawn with SkiaSharp directly (a large print's SVG is too slow to read back), and segments hidden exactly under a later one are left out. A 35 MB PrusaSlicer file (1.3 million lines) takes about 9 s to PNG with 320 MB of memory; a 285 MB file about 28 s and 760 MB.

SvgRenderer.Rasterize gets an overload that takes a recorded SKPicture; no other engine changes. No new dependency.

Not in this PR (planned as a follow-up): Prusa's binary G-code .bgcode ↔ .gcode. G-code → TXT isn't added either: other text formats (JSON, YAML) don't have a plain TXT route, so it wouldn't fit.

Type of change

  • Bug fix
  • New feature or improvement
  • New format or engine
  • Translation
  • Documentation
  • Build, CI or dependencies

How was it tested?

  • dotnet format Filee.slnx --verify-no-changes --severity warn and dotnet build Filee.slnx -c Release
  • dotnet test: new GcodeTests (33, generated files: PrusaSlicer-like and Cura-like prints, CNC programs with modal moves, inches and arcs, thumbnails in PNG / JPG / QOI, broken and oversized thumbnails, damaged and binary files, every target and extension, DXF/DWG through the SVG) and GcodeFormatTests (detection, fields, migration, option); FormatFieldsTests counts updated; a preset editor render test with screenshots. Core: all pass except the two tests that need a non-root user. Engines: the same 18 failures as on main in this environment (Pandoc, FFmpeg, LibreOffice not installed, root). App (filtered): the slice-label tests crash the test host here on main too.
  • Real files (not committed): G-code from PrusaSlicer 2.6, 2.7 and 2.9, SuperSlicer 2.3, Cura, and LinuxCNC / CAMotics .ngc programs. The previews were compared with the thumbnail each slicer saved in its file, and the CNC programs (arcs included) with an independent rendering made with pygcode; they match. A 3× Benchy sliced at 0.1 mm (35 MB) and an 8-fold copy of it (285 MB) were used for time and memory.
  • Tried it in the app: converted with the CLI (Linux) and in the engine test fixture; the app itself was only checked through the headless render tests.

Not tested: OrcaSlicer and Bambu Studio files (no free sample found; they use the same thumbnail block as PrusaSlicer), and real QOI thumbnails (tested with QOI written by ImageMagick).

Screenshots (UI changes)

3D printers: the preview each slicer saved in its file, next to Filee's drawing of the same file.

File Slicer's own preview Filee, from above Filee, isometric
3DBenchy, PrusaSlicer 2.7 (no preview in the file) 1-benchy-top 1-benchy-isometric
PrusaSlicer 2.6 (JPG preview) 2-prusaslicer-slicer 2-prusaslicer-top 2-prusaslicer-isometric
Cura (JPG preview) 3-cura-slicer 3-cura-top 3-cura-isometric
SuperSlicer 2.3 (PNG preview) 4-superslicer-slicer 4-superslicer-top 4-superslicer-isometric
PrusaSlicer 2.6 (PNG preview) 5-prusaslicer-hexnut-slicer 5-prusaslicer-hexnut-top 5-prusaslicer-hexnut-isometric

CNC programs (arcs, modal moves): an independent rendering with pygcode next to Filee's.

Program pygcode (reference) Filee, from above
CAMotics heart.ngc 6-cnc-heart-pygcode 6-cnc-heart-filee
LinuxCNC axis.ngc 7-cnc-linuxcnc-pygcode 7-cnc-linuxcnc-filee

The new preset card (shown for pictures, SVG and PDF while the 3D or CAD field is on):

French, light English, dark
8-preset-card-fr 8-preset-card-en-dark

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (none added)
  • The PR does one thing (unrelated changes go in a separate PR)

🤖 Generated with Claude Code

Ugoyo and others added 2 commits November 12, 2026 22:02
G-code (.gcode, .gco, .g, .ngc) is read and drawn as PDF (vector), SVG and
pictures: from above or isometric, extrusion and cuts coloured by height,
travel and rapids as thin lines. A preset option can use the preview the
slicer saved in the file instead (PNG, JPG or QOI). .nc stays out (KnifeLemon#562).

G-code is a read-only CAD format of the 3D and CAD fields, on the CAD donut;
libraries learn its extensions in schema 87.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5sVP9ruexgBngAnMKK
The SVG of a G-code toolpath lost the quote that ends each path's d
attribute, so it could not be read back (SVG → DXF / DWG on the CAD donut
failed). Tests read the SVG back and convert to DXF and DWG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5sVP9ruexgBngAnMKK

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @Ugoyo. The reader is careful work: budgets are checked before allocating, damaged and binary files
get a clear reason, the arc maths is right in all three planes (I checked the R-form centre side by hand), and the
thumbnail handling (header first, format sniffed from the bytes) is exactly what we want for untrusted files. The
comparison table with the slicers' own previews was really helpful too.

CI is red because of one string, so a few changes before merging:

  1. CI: engine.gcode.name is still "G-code (built-in)" in languages that translate the suffix.
    PortugueseLocalizationTests.Descriptive_engine_names_are_localized_and_brands_are_preserved fails for pt-PT and
    pt-BR on every platform (no engine name may contain "built-in"). Please use the same suffix as engine.cad.name
    in each of these: pt-PT (motor integrado), pt-BR (mecanismo integrado), de (integriert), it (integrato),
    id (bawaan), tr (yerleşik), vi (tích hợp), fa (داخلی), bn-BD (বিল্ট-ইন). The other languages keep
    "(built-in)" for CAD too, so they can stay. The two Ubuntu 22.04 package jobs only failed because the portable
    artifacts were missing.
  2. Unknown G-codes with coordinates are drawn as moves. In GcodeToolpath.Machine.Run, a line with coordinates and
    no M/T word continues the last motion, and the skip list only has G4/G10/G11/G53/G80–G89. So G30 X100 Y100,
    G29 … X Y and G38.2 Z-10 (probing) are drawn as G1 moves. Could you treat any G word outside the known modal set
    as "not a move", or at least add G29, G30 and G38.x to the skip list?
  3. Stream the SVG. GcodeDrawing.ToSvg builds the whole document in a StringBuilder and WriteSvg then calls
    File.WriteAllText. Near the 12 M point budget, that's a few hundred MB of UTF-16 held twice. Writing to a
    StreamWriter on the output file avoids it.

Smaller things, take or leave:

  • MaxArcSteps = 720 never binds: with 5° steps an arc has at most 72 segments, so docs/ENGINES.md ("at most 720
    segments") should say 72, or drop the constant.
  • if (radius < 1e-6 || radius > MaxCoordinate) lets NaN through (e.g. R1e300 with a horizontal chord). It ends up
    harmless only because (int)NaN saturates to 0. if (!(radius >= 1e-6 && radius <= MaxCoordinate)) says what's
    meant.
  • With the slicer's preview selected, GcodeThumbnail.Largest reads the file to the end, and the 1 GB size check isn't
    applied on that path. The slicers all write the block in the header, so stopping at the first G0/G1 move (and checking
    the size first) saves a full extra pass on big files without a preview.
  • Korean: the new ko.json strings use 합니다체 (그렸습니다, 있습니다, 쓰입니다). The rest of the file uses 해요체
    (그렸어요, 있어요, 쓰여요, 보여 줘요).

On .g: please leave it out, like .nc and .tap. It's also used by ANTLR grammars and GAP, and RepRapFirmware macros (config.g, homeall.g) rarely have a toolpath to draw, so the migration shouldn't add it to everyone's CAD donut.

The schema number: #1444 has just taken 87, and other open PRs want the next ones. Don't worry about it, we renumber when merging.

Ugoyo and others added 2 commits November 13, 2026 13:00
- Drop the .g extension (also ANTLR grammars and GAP; RepRapFirmware macros
  rarely have a toolpath), like .nc and .tap. Registry, migration, docs, tests.
- Localize engine.gcode.name where the "(built-in)" suffix is translated
  (pt-PT, pt-BR, de, it, id, tr, vi, fa, bn-BD), fixing the Portuguese
  localization test.
- A G-code line with any G word outside the known modal set is not a move, so
  G29, G30, G38.x probing and canned cycles are no longer drawn as G1 moves.
- Stream the SVG to the output file instead of building the whole document in
  memory.
- Only read the thumbnail block in the header: stop at the first move, and
  check the 1 GB size limit on that path too.
- Korean strings use 해요체 like the rest of ko.json.
- Guard the arc radius against NaN; drop the unreachable MaxArcSteps and fix
  the "720 segments" line in docs (5° steps, 72 for a full circle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5sVP9ruexgBngAnMKK
Main took schema 87 (Arduino and Processing sketches) and 88 (NuGet and
Android packages); the G-code extensions are learned in 89. Specialist count
in FormatFieldsTests: 124 with main's new formats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPxh5sVP9ruexgBngAnMKK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants