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 viaGetLastErrorMessage(). - 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:43 → 0xFFRRGGBB), 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 = Vec3alias (:73),Color3(:80),BedGeometry(:91),CameraState(:102, defaults rotX 0.6 / rotZ 0.4),OverlayState(:116),EngineSnapshot(:137). No ABI presence.engine_data.h—TriangleMesh(:38),Plate(:62, thumbnail + bounds),PaletteEntry(:80),LoadedModel(:85).engine_analysis_result.h—FindingSeverity(: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.mdc-abi.mddocs/architecture.md(target tree)docs/modules/session.md,docs/modules/mesh.md,docs/modules/viewport.md,docs/modules/gcode.md