Skip to content

02 — Engine Modules

The C++ engine (src/Engine/) is one C++ library today, with internal modules physically declared in headers under src/Engine/internal/ and value-type PODs under src/Engine/. The target tree splits each module into its own folder (see docs/architecture.md "Target tree") — that is a TARGET refactor, not the current shape.

What this page covers

For each module:

  • why — what it owns and why it is its own module
  • what — the class / file that does the work
  • how — concrete entry points, with code refs
  • when — call-site that triggers it
  • does not — explicit non-responsibilities

Public surface: nps::Engine (L1)

File: src/Engine/engine.h (279 LOC); definitions in src/Engine/engine.cpp (~7000 LOC).

This is the only C++ class the host cares about (via the C ABI). It owns the canonical session state in four private members (engine.h:261-276): lastErrorMessage_, MeshStore meshStore_, GcodeStore gcodeStore_, and std::unique_ptr<AnalysisManager> analysisManager_ (dtor emitted in engine.cpp where the type is complete). Every C ABI export is a thin null-checked delegate into this class.

Why

Single stateful facade that funnels every domain call through one class. The C ABI delegates to it, the tests construct it directly, and internal modules see it as the only allowed caller from above.

Public methods (all)

Non-copyable / non-movable: Engine(const Engine&) = delete + operator= deleted (engine.h). Refs are engine.h decl / engine.cpp def.

Lifecycle

Backing module: the facade itself; analysisManager_ construction is the only real work.

Method Ref Why / when Side effects
Engine() h:82 / cpp:469 Session construct via CreateEngine Constructs analysisManager_ (cpp:479); prints to stdout
~Engine() h:83 / cpp:482 DestroyEngine Print only
void Initialize() h:91 / cpp:486 Post-construct hook, hosts call once None (print only, cpp:487)
int GetVersion() const h:92 / cpp:490 Build version probe None; constant 1

Mesh loader (3MF)

Backing modules: zip reader → 3MF parser/resolver → color parser → MeshStore.

Method Ref Why / when Side effects
LoadError LoadModelFromFile(const char*) h:100 / cpp:540 Default-options load entry Delegates to options overload (FULL, maxBytes=0)
LoadError LoadModelFromFile(const char*, const LoadOptions&) h:101 / cpp:550 Full 3MF pipeline: preflight + magic sniff (cpp:613-641), zip open (cpp:657), model XML extract (cpp:666-679), ParseModelXmlObjects (cpp:718-726), baseExtruders (cpp:748-793), palette (cpp:800-804), MeshResolver::resolve (cpp:871), per-tri colors (cpp:890-898), AABB (cpp:908-940), thumbnail (cpp:955-958) Clears mesh store + all three gcode buffers on every attempt (cpp:567-577); on success repopulates meshStore_ (cpp:961-966); sets lastErrorMessage_ on every failure
const char* GetLastErrorMessage() const h:105 / cpp:971 Diagnostic after failed Load/parse; "" when no error None

Post-load mesh queries

All are read-only views into meshStore_.loadedModel; returned pointers stay valid until the next Load or Destroy. Backing module: MeshStore (+ Plate from engine_data.h).

Method Ref Semantics
const char* GetModelUnit() const h:115 / cpp:988 Defaults "millimeter"
uint32_t GetPlateCount() const h:116 / cpp:997 plates.size()
uint32_t GetTotalVertexCount() const h:117 / cpp:1001 Cached total
uint32_t GetTotalTriangleCount() const h:118 / cpp:1005 Cached total
const float* GetVertexPositions(uint32_t, size_t*) const h:123 / cpp:1009 Per-plate xyz triples; nullptr on bad plate
const uint32_t* GetTriangleIndices(uint32_t, size_t*) const h:124 / cpp:1023 Per-plate 0-based indices
uint32_t GetTriangleCountForPlate(uint32_t) const h:125 / cpp:1037 indices.size()/3
const uint8_t* GetTriangleColorIndices(uint32_t, size_t*) const h:129 / cpp:1045 Per-triangle palette indices
bool GetMeshBounds(uint32_t, float[3], float[3]) const h:134 / cpp:1059 AABB; thin delegate to the typed overload (cpp:1067). C ABI export is test-hook gated (cpp:2765-2778) — production hosts compute bounds from the vertex stream
bool GetMeshBounds(uint32_t, Position*, Position*) const h:138 / cpp:1077 Typed overload; outs zeroed first (cpp:1085-1086), false when no data (cpp:1091-1093)
uint32_t GetPaletteColorCount() const h:142 / cpp:1105 palette.size()
uint32_t GetPaletteColor(uint32_t) const h:143 / cpp:1109 0xAARRGGBB, 0 out of range
size_t TestGetPlateThumbnailSize(uint32_t) const h:147 / cpp:1116 Test-harness only (h:145-146): thumbnail PNG bytes without widening the production ABI

G-code generation — DEMO TOOLPATH (see TARGET vs REAL below)

Backing modules: gcode generator + GcodeStore.

Method Ref Why / when Side effects
Result GenerateGcode(const GcodeGenParams&) h:160 / cpp:1215 Demo toolpath, NOT slicing (h:150-157, cpp:1197-1214): square perimeter + 1 diagonal infill per layer from real mesh bounds; emitted header says so (cpp:1314-1317). Called by the generate button in both apps; the C# facade honestly names it EngineSession.GenerateDemoToolpath Clears lastGeneratedGcode + shared lastGcodeMoves, does NOT touch lastParsedGcode (cpp:1216-1223); populates text + shared moves (cpp:1432)
const char* GetLastGeneratedGcode() const h:165 / cpp:1436 Gen output; "" when nothing generated None
Result WriteGcodeToFile(const GcodeGenParams&, const char*) h:169 / cpp:1593 Gen + write convenience GenerateGcode effects + file write; lastErrorMessage_ + IO_ERROR on failure (cpp:1602-1611)

G-code parsing

Backing modules: gcode parser (ParseGcodeTextImpl / decodeBgcodeToText / ParseGcodeMetadata) + GcodeStore.

Method Ref Why / when Side effects
Result ParseGcode(const char*, size_t) h:178 / cpp:4011 Parse ASCII G-code on file-open/paste; no-op success on null/empty (cpp:4025-4027) Clears parsed text + shared moves + analysis (cpp:4015-4020); runs ParseGcodeMetadata (cpp:4029-4030); stores source in lastParsedGcode; moves into shared vector on success (cpp:4041); does NOT touch lastGeneratedGcode
Result ParseBgcode(const uint8_t*, size_t) h:179 / cpp:4049 Binary BGCode; decodes via decodeBgcodeToText (cpp:4065) then identical to ParseGcode Same clears; INVALID_ARGUMENT on null data, PARSE_ERROR on decode failure
const char* GetLastParsedGcode() const h:185 / cpp:1453 Parsed/decoded source; sentinel "No G-code parsed" when empty. Split buffer — independent of GetLastGeneratedGcode; only the move array is shared None

Shared structured moves

Backing module: GcodeStore (lastGcodeMoves).

Method Ref Semantics
size_t GetLastGcodeMoveCount() const h:194 / cpp:1460 Shared move vector — populated by GenerateGcode OR ParseGcode/ParseBgcode
const GcodeMove* GetLastGcodeMoves() const h:195 / cpp:1464 Engine-owned contiguous array; nullptr when empty; valid until next Generate/Parse/Load/Destroy

Analysis integration

Backing modules: AnalysisManager + the six analyzers; inferred-* reads come from GcodeStore.

Method Ref Why / when Side effects
void AnalyzeGcode(const MaterialProfile* = nullptr) h:206 / cpp:1479 Run the 6-analyzer suite over shared moves; null profile → inferredProfile (cpp:1483) Early-return with no moves; analyze clears prior findings first (cpp:4911)
const char* GetInferredMaterialType() const h:211 / cpp:494 gcodeStore_.inferredProfile.type None
const char* GetInferredSlicerType() const h:212 / cpp:498 gcodeStore_.inferredSlicer None
const char* GetInferredPrinter() const h:213 / cpp:502 gcodeStore_.inferredPrinter None
const MaterialProfile& GetInferredProfile() const h:214-216 (inline) C++-only by-ref accessor — no C ABI export None
size_t GetLastAnalysisFindingsCount() const h:220 / cpp:1490 Full 300B-row path count Lazy-ensures analyzers (cpp:4958)
void GetLastAnalysisFindings(AnalysisFinding*, size_t) const h:221 / cpp:1497 Copy full findings into caller buffer None
size_t GetLastAnalysisFindingsSummaryCount() const h:231 / cpp:1513 Preferred path: 16-byte summary rows first None
void GetLastAnalysisFindingsSummary(FindingSummary*, size_t) const h:232 / cpp:1520 Stamps 16B rows (severity/layer/moveIndex/findingId); findingId = source index for O(1) detail lookup (cpp:1531-1534); FindingSummary declared h:225-230 None
bool GetAnalysisFindingDetail(int32_t, AnalysisFinding&) const h:237 / cpp:1538 300-byte detail fetched only on selection by stable id None
float GetLastOverlayValue(const char*, int32_t layer, int32_t moveIndex) const h:243 / cpp:1550 Per-move overlay scalar for heatmap + stats; 0 when unavailable Triggers lazy ensure (cpp:4994)
size_t GetSupportedOverlayCount() const h:247 / cpp:1557 Advertised overlay ids count for UI toggles None
Result GetWarpGridInfo(int32_t, NpsWarpGrid*) const h:253 / cpp:1572 Warp grid header copy; NO_DATA/INVALID_ARGUMENT explicit Triggers thermal ensure (cpp:5052)
const float* GetWarpGridRisk(int32_t, size_t*) const h:254 / cpp:1578 Engine-owned float[w*h] warp risk grid; nullptr when absent Triggers thermal ensure

Error polarity

  • Load entry points return LoadError (NpsLoadError); 0 = SUCCESS.
  • Non-load ops return Result (NpsResult); 0 = OK.
  • No C++ exceptions cross the surface; failures set lastErrorMessage_ retrievable via GetLastErrorMessage().
  • Pre-load queries return safe defaults — never crash.

Does not

  • Does not own C++ standard library structures in its public surface (those live behind the value members MeshStore, GcodeStore).
  • Does not parse G-code itself; that is the gcode parser module + engine_gcode_store.h.
  • Does not own any UI / Avalonia dependency.

L0 module map

All internal headers live under src/Engine/internal/, reachable only from engine.cpp + the Catch2 harness via a PRIVATE include path (engine.cpp:3-9).

Zip / OPC reader (internal/engine_zip.{h,cpp})

Why. A 3MF file is a ZIP container (OPC packaging). The loader needs raw access to entries (3dmodel.model, object_1.model, project settings config XML) without a full OPC stack.

What. nps::detail::ZipArchiveReader, pImpl-owned file handle + central directory table (engine_zip.cpp:50-58).

How. open (engine_zip.cpp:65) — EOCD + central directory scan with ZIP64-locator support and size limits; extract (engine_zip.cpp:374) — stored/deflate with declared-vs-actual size verification; findEntry (engine_zip.cpp:348, case-insensitive by default); listEntries (engine_zip.cpp:461).

When. During Engine::LoadModelFromFile to open the container and pull the model XML + metadata XML.

Does not. No 3MF XML parsing, no mesh calculation.

3MF parser + resolver (internal/engine_threemf.h, impl in engine.cpp)

Why. Turns the extracted XML into the build-item / component graph the mesh resolver walks. Owns all 3MF wire-format knowledge.

What. ParseModelXml (count-only streaming scanner, engine.cpp:2084), ParseModelXmlObjects (engine.cpp:2472, body parseObjectsStreaming cpp:2325), MeshResolver::resolve (engine.cpp:2477) — recursive component/p:path resolution with composed Matrix4 transforms (cpp:2237-2284), cycle detection (cpp:2486-2499), per-triangle paint decode via ColorParser::DecodePaintCode (cpp:2559). Hot-path single-pass attr readers parseVertexAttrsOnce/parseTriangleAttrsOnce (cpp:1860/1896).

When. Mesh load, after the XMLs are extracted from the ZIP.

Does not. No decompression, no project-settings parsing, no viewport upload.

Color decoder (internal/engine_color.h, engine_color.cpp)

Why. 3MF wire format uses #rrggbb hex; Bambu AMS uses a paint-code bitmask. Both decode to ARGB ints / palette indices.

What. Stateless ColorParser: ParseHexColor (engine_color.cpp:430xFFRRGGBB), ParseFilamentColours (:72, filament_colour=[...] from project settings), DecodePaintCode (:113, → 0-based palette index), DefaultFixturePalette (:135, 4-color Benchy fallback).

When. Palette population during load + paint mapping inside MeshResolver::resolve.

Does not. No global state, no analysis.

G-code parser (ParseGcodeTextImpl, engine.cpp:3621)

Why. In-place pointer-walk state machine (no copies) over ASCII G-code, plus BGCode container decode and header-metadata inference.

How. Layer markers (;LAYER:, Bambu CHANGE_LAYER, LAYER NUM, S3D, LAYER_CHANGE — cpp:3689-3788), M82/M83/G92/M204 (cpp:3805-3834), G0/G1 + G2/G3 arc tessellation (cpp:3905-3954), Z-fallback layer increment (cpp:3870-3879). BGCode: decodeBgcodeToText (cpp:3407) with crc32 / deflateDecode / heatshrinkDecode / meatpackDecode. Metadata: ParseGcodeMetadata (cpp:176) — slicer banner + printer_model + filament_type + temperature fallback.

When. Engine::ParseGcode / ParseBgcode.

G-code generator (demo toolpath)

Why. Engine::GenerateGcode (cpp:1215) + helpers appendMove (cpp:1151) and sampleScalarFieldAt (cpp:1172). Auto layer count from triangle count clamped 2..12 (cpp:1236-1246). Owns the generated text + its half of the shared move vector. See TARGET vs REAL.

G-code parser test hooks (internal/engine_gcode.h)

Why. Test-only entry points so Catch2 can exercise the parser without an Engine session — all delegate to the production analyzers, not stubs.

What. GcodeAnalysisResult + TestParseGcodeToMoves (cpp:3965), TestComputeThermalGrid (cpp:4092), TestComputeStructuralScore (cpp:4118).

When. Catch2 harness only. Production goes through ParseGcode / ParseBgcode / AnalyzeGcode.

Does not. Mutate engine state.

G-code state store (engine_gcode_store.h)

Why. Holds last generated / parsed text and the shared move array in one value-owned member so engine.h stays free of parser internals.

What. GcodeStore (:77): split text buffers lastGeneratedGcode / lastParsedGcode (:84-85), shared lastGcodeMoves (:86), inferred profile/slicer/printer (:94-96).

Mesh state store (engine_mesh_store.h)

Why. Same rationale — keeps loader internals off the umbrella header. Public root by design; Engine holds it by value.

What. MeshStore (:61): LoadedModel loadedModel + cached loadedVertexCount / loadedTriangleCount. Cleared on each Load.

Viewport (internal/engine_viewport.h, impl engine.cpp:5711-6308)

Why. CPU-side render-buffer prep for parsed/generated G-code layers and analysis overlays. GPU submission (VAO, draw calls) lives in the C# UI layer.

What. GcodeViewport + GcodeLayerRenderData types.

How. SetGcodeMoves (cpp:5714) copies moves into ownedMoves_ (clear + assign at cpp:5733-5736) — the viewport survives later engine Load/Parse/Generate; SetCamera (cpp:5761/5773); BuildRenderBuffers (cpp:5861) O(N) bucket pass + per-layer bake (buildLayerGeometry cpp:5908, 7-float interleaved verts); ComputeMVP (cpp:6102); statics GetHeatmapColor (cpp:6186, blue→red ramp), GetExtrudeColorForType (cpp:6152), GetTravelColor (cpp:6179). kDefaultOverlayModeId = "motion-type" (engine_viewport.h:54) — the host "None" radio maps here, not to a no-op. Free helper PrepareViewportFromEngineLast (cpp:6216); ProjectOrbitXY (cpp:5400) is a bit-exact port of deleted UI math.

When. The C ABI Viewport* exports after gen/parse + host ViewportSetGcodeMoves + ViewportBuildBuffers.

Does not. Owns no GL objects; no file I/O.

C++-only (no C ABI export): SetClipPlane (cpp:5783), InvalidateBuffers(), GetLayerMoveOffsets(...), color helpers above.

Analysis manager + analyzers (internal/engine_analysis.h, impl engine.cpp:4157-5103, 6313-7001)

Why. Coordinates six analyzers into one execution suite with lazy per-engine ensure and overlay discovery.

What. AnalysisManager (ctor cpp:4868; 6-row factory table cpp:4881-4890; analyze cpp:4909; lazy ensureEngine/ ensureAllAnalyzed cpp:4922/4938; getAllFindings cpp:4957 sorted by severity; overlay map discovered via dynamic_cast to IOverlayProvider cpp:4896-4902). Analyzers: ThermalAnalyzer (cpp:4320; warp grids per layer cpp:4388), StructuralAnalyzer (cpp:4650), MotionAnalyzer (cpp:4803; junction velocity cpp:4785), HoleDetectorAnalyzer (cpp:6606; BFS flood fill, findings-only), RetractionAnalyzer (cpp:6839), FlowAnalyzer (cpp:6923).

When. Engine::AnalyzeGcode.

Does not. Mesh construction, file ops, rendering.

Material catalog (engine_material.h, material_catalog.cpp)

Why. Static catalog of default filament profiles. The C# MaterialProfileOptions mirrors these.

What. Grouped MaterialProfile (type + MaterialThermal :77 + MaterialMotion :97 + MaterialFlow :108); MaterialCatalog::forType (material_catalog.cpp:134) over a 10-row static table (:54, PLA row 0 = fallback; uppercase + _- key normalize :120); entries()/entryCount(); legacy GetDefaultMaterialProfile wrapper (:157).

When. Default profile lookup; slicer banners drive the inferred material type from G-code headers.

Does not. Load from config files. Default catalog only.

Value types + result PODs

  • internal/engine_value_types.h (header-only) — Vec3 (:58), Position = Vec3 alias (:73), Color3 (:80), BedGeometry (:91), CameraState (:102, defaults rotX 0.6 / rotZ 0.4), OverlayState (:116), EngineSnapshot (:137). No ABI presence.
  • engine_data.hTriangleMesh (:38), Plate (:62, thumbnail + bounds), PaletteEntry (:80), LoadedModel (:85).
  • engine_analysis_result.hFindingSeverity (:59, ABI-pinned), FindingCategory (:72), AnalysisFinding (:105).

TARGET vs REAL gaps

# Gap Evidence
1 GenerateGcode is a demo toolpath, never slicing. Real slicing is deferred to the planned slice/ module — no slice/ directory exists under src/Engine engine.h:150-157, engine.cpp:1197-1214, header text cpp:1314-1317; facade EngineSession.GenerateDemoToolpath
2 GetSupportedOverlayIds C export is a hardcoded stub string, not manager-derived engine.cpp:5578-5579
3 Single plate only — Load hardcodes one plate "plate_1" with a "future plates would loop" note engine.cpp:883-885, cpp:959
4 MotionAnalyzer::inferProfileFromGcode is a stub returning the base profile unchanged engine.cpp:4851-4866
5 GcodeViewport::SetClipPlane has no C ABI export (C++-only) engine.cpp:5783
6 Overlay default is "motion-type", not empty engine_viewport.h:54, fallback cpp:5802
7 Index-validation loop in Load is a deliberate break-on-first-bad no-op engine.cpp:943-950
8 Viewport owns its move copy after ViewportSetGcodeMoves — independent of later engine Load/Parse (this is why the shared-array handoff is safe) engine.cpp:5714-5741

See also

  • layers-and-contracts.md
  • c-abi.md
  • docs/architecture.md (target tree)
  • docs/modules/session.md, docs/modules/mesh.md, docs/modules/viewport.md, docs/modules/gcode.md