Table of Contents

Upgrading from 3.1.175 to 3.2

This page is the long form of the 3.2 release-note entry. It exists because 3.2 is not an increment on the last release most callers hold — it is the accumulation of everything that landed after the 3.1.175 package set, delivered in one step.

Read it in order the first time. The two sections that decide whether an upgrade is a recompile or an afternoon are Breaking changes and Results that change on upgrade; everything after them is new capability you can adopt when you need it.

Important

Read the Adoption status note on the release-note page before planning around this. The short version: 3.2 is published for review, verification is still in progress across most areas, and NC optimization is not finished on this line — work that depends on it should stay on 3.1, serviced as 3.1.175.<patch>.

The package line

master develops the 3.2 package line as of 2026-08-19. All ten packages — HiGeom, HiLicense, HiDisp, Hi.WinForm, Hi.WpfPlus, HiCbtr, HiMech, HiUniNc, HiNc, HiNc-Resource — moved to 3.2 together and restarted their build counters.

Three consequences worth stating plainly:

  • A 3.2 build number starts low. 3.2.6 is newer than 3.1.204; the two counters are not comparable, and the gap between the last 3.1 number a feed served and the first 3.2 number is expected rather than a missing upload.
  • The 3.1 line is closed at the 3.1.175 set and is serviced only as 3.1.175.<patch>. A reference left on 3.1.175.x receives correctness fixes for that line and none of the capability on this page.
  • The versions between — 3.1.176 through 3.1.204 — were never published as a release set. That is why the release note carries one 3.2 entry where it might have carried a dozen: for a caller moving off 3.1.175, they were never separate releases.

Breaking changes

In the order they will bite an upgrading host.

1. Registration, before anything else

XFactory.Generators changes from a plain Dictionary to a ConcurrentDictionary, so parallel Reg() calls no longer corrupt the registration map. The property is public, so a caller that declares its type explicitly stops compiling.

Carried over from 3.1.172 and still the first thing an upgrading host hits: Reg must be called once at startup, before any project XML is deserialized. Registration no longer happens by accident when a type is first touched. See XML IO.

2. The message channel

Message reporting is rebuilt on a unified model. Every notification carries a Severity, a Category and a filterable id (SimpleMessage), and arrives on one of three typed sinks: ShellProgress for session-lifecycle messages, StepDiagnosticProgress for step-anchored diagnostics, and NcDiagnosticProgress for NC-parsing diagnostics.

MixedProgress0, MultiTagMessage and MultiTagMessageUtil are removed. Every message parameter across the API — the XFactory deserialization chain included — is retyped from IProgress<object> to IProgress<IMessage>. Category.General is deleted, MessageUtil becomes id-first {Category}{Severity}, and NcDiagnostic.Text renames to Notification. See Message Management.

3. The session surface

LocalProjectService.SessionShell is created by BeginSession() and nulled at EndSession() — it is null outside a session and no longer lazily created. ShellProgress is recreated per session, so hold no long-lived reference to either: subscribe once through OnShellMessageAdded / OnShellMessageCleared, or buffer one call's messages with MessageCollector.

MachiningSession takes an injected IMachiningService host in its constructor, and IMachiningService replaces PlayerCancellationToken / PausePlayer with a single PacePlayer property.

4. The play verbs

Nc becomes the umbrella term for any playable control program, and BrandNc names the famous-brand controller-code group as a sibling of Cl and Csv.

Was Is now
PlayNcFile(file) — brand G-code only PlayNcFile(file, NcKind kind = NcKind.Auto)
RunNcFile(file) RunNcFile(file, NcKind kind = NcKind.Auto)
the narrow brand-only file verbs PlayBrandNcFile / RunBrandNcFile
IControlRunner Hi.Numerical.INcRunner
RunControlLines RunNcLines
ControlKind NcKind
IsRunningControlLines / BeginControlRunner IsRunningNcLines / BeginNcRunner
the interim PlayControlFile / RunControlFile mirrors removed

The same rename applies on LocalProjectService, SessionShellController and MachiningSession. Only .cl / .cls / .clsf / .csv arguments change meaning — DetectByPath treats those as closed extension sets and everything else falls back to brand G-code, so an exotic brand extension can never be misrouted.

5. Renames with no shim

Was Is now
WorkpieceService.GetRuntimeGeom / ReadRuntimeGeom / WriteRuntimeGeom / SetRuntimeGeom / ResetRuntimeGeom / IsRuntimeGeomInit / ScanRuntimeGeomInfDefect GetOrBuildMeshedGeom / ReadMeshedGeom / WriteMeshedGeom / SetMeshedGeom / ResetMeshedGeom / IsMeshedGeomInit / ScanMeshedGeomInfDefect
MachiningEquipmentCollisionIndex.WorkpieceRuntimeGeomGetter WorkpieceMeshedGeomGetter
IContourTray / UniformContourTray / FreeContourTray IFluting / UniformFluting / FreeFluting
MillingCutter.FluteContourTray Fluting
Hi.Common.ResourceUtil (the HiNc one) ResourceLayout
NativeTopoStld / NativeTopoStlfr / NativeCarveTopoStl3wfr NativeTopoStl3d / NativeTopoStl3wfr / CarveStl
Solid.NativeSmoothTopoStl / Sweptable.NativeTopoStl SmoothTopoStl3d / NativeTopoStl3d
ITimeGetter and its Time member Hi.Physics.ITimecoded and Timecode
ClStrip.DrawingRefreshing ClStrip.DrawingRefreshed
CbtrPickable.CleanLinked* CbtrPickable.CleanAttached*
SoftNcRunner.NcDependencyList PipelineNcDependencyList

Four notes on that table.

The SessionShell script names for meshed geometry keep hidden [Obsolete] aliases so existing player scripts still run; the service-level members do not. WorkpieceService.ResetRuntimeGeom also drops its ClStrip parameter.

ClStrip.DrawingRefreshed was renamed because both invocations always fired after the work — the -ing name told subscribers the opposite of when they are called. A subscriber that misses the rename silently detaches.

Project and cutter files written before the fluting rename keep loading: each Reg() registers the ContourTray-era XName beside the older aliases, and the cutter element reader tries Fluting, then FluteContourTray, then FluteContourTrackTray. A cutter that nevertheless fails to resolve its fluting machines as a plain bounding shape rather than failing loudly, so verify the load rather than assuming it.

SoftNcRunner.PipelineNcDependencyList is the raw list; machine-config consumers read the resolved view through GetEffectiveNcDependencyList. Legacy <NcDependencyList> XML still loads and migrates.

6. Removals

  • CSVCsvRunner0, LocalProjectService.EnableSoftCsvRunner, and the earlier RawCsvRunner and CsvRowSemantic. CSV playback has one path, GeneralCsvRunner, and CsvRunner returns the CSV suit's SoftNcRunner directly.
  • CarriersIndexedSentence (wrap a bare Sentence in your own ISentenceCarrier if you passed one as a sourceCommand), SimpleSessionCommand, HiCbtr's [Obsolete] LsStl. The packed MixedIndex file-line key is replaced by typed FileLineIndex comparison, so file and line positions compare by type rather than through a packed integer.
  • GUI-layer compositionMachiningProjectDisplayee, IsoCoordinateEntryDisplayee, HeidenhainCoordinateEntryDisplayee, UserConfig, UserService, PlayerDivConfig. Construct LocalProjectService with the ILogger-only constructor and copy the displayees from any app project — Hi.Sample.Wpf/Disp/ ships them. They compose only public API (IDisplayee over LocalProjectService), so tailoring them is the point.
  • Managed physics kernel types — the class FluteZData, MillingForceUtil.RuntimePack / LayerPack / AnglePack, the LayerMillingEngagement constructor that built an engagement from a z-to-dz list (the default and BinaryReader constructors stay), and MillingPhysicsBrief.YieldStressMinHeight_mm.
  • Culture declarationCultureUtil.SupportedCultureNames and CultureUtil.SetCurrentCulture(string), deleted outright with no [Obsolete] shim. What remains is English and SetCurrentCultureEn. A host that enumerated supported cultures must enumerate its own manual or resource folders instead.
  • Dead P/Invoke declarations — eight gl* methods on HiDisp's public GL class (glFenceSync, glGetDoublev, glGetDoublei_v, glGetDoubleIndexedvEXT, glIglooInterfaceSGIX, glPNTrianglesfATI, glPNTrianglesiATI, and the already-commented glDebugMessageCallbackAMD). They had no backing export and threw EntryPointNotFoundException when called.
  • NcFileListCommand — a list of NC files is just a List of Program File commands, so the dedicated type is gone. Loading a project that contains one converts it in place: each <File> entry becomes a single-file NcFileCommand (kind Auto, the same per-file extension dispatch) inside a ListCommand, and re-saving persists the converted form.
  • Post-Execution meshed-geometry outputPostExecutionCommand loses EnableWriteMeshedGeom and MeshedGeomFileTemplate (and their pre-rename …RuntimeGeom… spellings) together with the enable-write-meshed-geom and meshed-geom-file-path routes. A geometry snapshot can be taken at any time-spot, unlike the run-derived outputs that command manages, so the carriers are now the placeable RecordMeshedGeomCommand and ExportMeshedGeomToStlCommand. Loading an old project with the pair enabled emits PostExecution--MeshedGeomOutputRetired, and re-saving drops the elements.

7. Signature and shape changes

  • MachiningToolHouse derives from Dictionary<int, IMachiningTool>: SetToolId takes an int and CreateStickMillingTool returns KeyValuePair<int, MillingTool>. Any (int)entry.Key cast stops compiling. Siemens T="name" string tool calls are unaffected — they still resolve to an int at the semantic layer.
  • ActualTimecode and ActualDateTime become get-only views onto the new optional ActualTime (StepActualTime) — their setters are gone. AccumulatedTime is superseded by EndTimecode, kept as an [Obsolete] alias; step CSVs write the new header and still read the old.
  • PreSettingCommand becomes a legacy bundle. A saved bundle expands on load into MachiningResolutionCommand, MachiningMotionResolutionCommand, CollisionDetectionCommand, PauseOnFailureCommand and PhysicsCommand (plus a Read-mode RecordMeshedGeomCommand), and is never written back. Anything that located the bundle element in a saved .hincproj must look for the split commands.
  • ToPresentDto wire keys change with the obfuscation fix: geometry DTOs use Type / Min / Max / PairZrs / Z / R / SourceFile / FileIndex / LineIndex (Vec3d keeps lowercase x / y / z), transformer DTOs use Trans, Angle_deg, CosTheta / SinTheta, Axis, Pivot, Scale, Rotation, Translation, Step, Stack, Matrix. A front-end reading those payloads must be updated in lockstep.
  • defaultFontFile changes value from "Font/WCL06.ttf" to "(embedded)". It is a public const, so an assembly compiled against 3.1.175 already carries the old literal and keeps passing it — Init still accepts it — but a rebuild changes what it passes, and no font file is extracted to the working directory any more.

8. Defaults and gates that changed

  • EnableSoftNcRunner defaults to true. The SoftNc pipeline is the NC engine; HardNcRunner is the opt-out fallback for the shrinking set of features still bound to it.
  • EnableNativeMillingPhysics defaults to true, and in a shipping build setting it to false throws InvalidOperationException at the setter — the managed reference implementation lives only in a non-shipping assembly. Code that flipped it off for an A/B comparison now fails at configuration time.
  • YieldingStressRatio and YieldingStressRatio report NaN instead of 0 when no beam section qualifies. A caller treating 0 as “no yielding constraint” — as both feed solvers did — must add a NaN branch or it will pass NaN into downstream queries.
  • New licence feature NcComposition (id 22). Registering any non-built-in processing unit into a SoftNcRunner pipeline, or executing an NC-embedded C# script, requires it. Degradation is silent and functional: external units are skipped for the session with one Composition--NotLicensed naming them, an external segmenter falls back to SingleLineSegmenter, and scripts are skipped with Script--NotLicensed. An unlicensed installation therefore produces a different simulation, not an error. Calling the public API from your own application or session script needs no extra licence; composing the interpretation pipeline does. See NC Parsing Engine.
  • The four SnapshotSyntax entries in the Fanuc preset default to IsEnabled = false, so projects stop serializing enabled debug snapshots. A project saved by an earlier build keeps what it serialized until its pipeline list is refreshed from the current preset.
  • Server side: HiNcServer pins request localization to English, so an Accept-Language: zh-Hant request falls back to en. HiNcRcl removes the HiNC:DisplayEngine:FontFile configuration key, which never had any effect — delete it from appsettings. FontFile remains for a custom font.
  • Localization: HiMech's MachiningStep.zh-Hant / .zh-Hans resx are deleted. Step presentation strings now come from the HiNc-Resource present catalog (catalog.en.json, catalog.zh-Hant.json, catalog.zh-Hans.json) that a host overlays; a host that ships neither loses the localized step labels it used to get for free.
  • Packaging is x64-only: HiDisp drops the win-x86 runtime identifier and its Sentinel payload, HiNc-Resource drops the x86 platform.
  • The shipped machine-tool packages are renamed. They now carry the .default marker and neutral names: MachineTool/Table-B1.default and MachineTool/CT-350.default. The table-type package was renamed outright — its .mt, its .general-mech and its STL headers travel with it — and the duplicated nested STL set inside the CT-350 package is deleted. A project, script or .mt that refers to a shipped machine-tool package by its earlier path must be repointed at the name above.

Results that change on upgrade

None of the following breaks a build. All of them change what a simulation produces, so a byte-for-byte comparison against 3.1.175 output will differ — usually because 3.1.175 was wrong.

Silent wrong geometry, now fixed. Each of these produced a plausible-looking simulation of the wrong thing.

  • The kinematic pivot anchor. The pivot-transform chain entry was built as K(0)·K(abc)⁻¹, folding the whole machine-zero forward kinematic — its linear part included — into the pre-pivot anchor. That linear part encodes each axis' motion sense and tool/workpiece-side ownership, so it mirrored the program components of every workpiece-side linear axis before the IK ran. On a table-side chain whose machine-zero linear part is diag(1,1,-1), every program Z was mirrored and a near-180° swing amplified it into metres of machine-Z error, floating the toolpath above the workpiece. The anchor is now the translation alone, matching what HardNc has always kept. Machines whose linear axes all ride the tool side are unaffected.
  • G68 2D coordinate rotation was a silent no-op. TiltTransformUtil judged the active mode from the current block but always took the matrix from the previous one, so every G68 activation past the first block had its freshly authored rotation overwritten with an identity that then propagated. G68 rotation did nothing at all while the block's term still read G68, and the simulation machined the unrotated pattern. Re-running an existing G68 program now gives different — correct — geometry.
  • A blank line reset G90/G91. A piece with no parsing section — a blank line, a comment-only line, %, an O-number — left the next block's single-step lookback empty, and it fell back to the G90 default. A program that was incremental throughout silently flipped to absolute mid-file. Such pieces now carry the positioning section forward like every other modal syntax.
  • A Siemens D offset with no $TC_DP row resolved to zero length, in silence. A whole TRAORI program machined one tool length low. The read point now falls back to the generic tool-number-keyed height and emits SiemensToolOffset--TcdpRowMissing.
  • G43.4 with an unresolvable H word activated RTCP with a zero-length tool. It now reports Comp-ToolHeight--001 as a warning and keeps processing the block. An absent H stays silent — re-activating G43.4 on the modal offset id is legitimate input.
  • Heidenhain DIN/ISO arc centres. I / J / K are absolute circle centres on Heidenhain — the ISO face of the klartext CC pole — not start-to-centre offsets. Reading them incrementally turned arcs into near-full phantom circles. Fixed in all three engines: the HardNc reader (IsIjkAbsolute), the SoftNc reader (IsIjkAbsolute) and the optimizer's write-back. Also delivered on the 3.1.175.x service line.
  • A K0 word on a HardNc G02/G03 saturated the turn count. Under the default G17 plane a written-but-zero plane-normal word divided the axial travel by zero, the additional-turn count saturated to int.MaxValue, and one arc block became a spiral act of roughly 302 simulated years — the play appeared to hang on a single NC line. The reading now falls back to the closed-circle rule for a zero pace, matching the guard the SoftNc side already had.
  • Indexed-rotary programs folded the pivot into plain moves. A block with no active G68.2 or G43.4 no longer folds the kinematic pivot transform into a plain XYZ move, which is what a real controller does with a table-side rotary program.
  • The radius-compensation arc transient cache landed on the wrong block. Any non-motion line between the corner and the arc orphaned the cache, so the arc lost its leading linear bridge and began at the corner intersection instead of on the offset arc.
  • The G68.2 tool-axis IK fallback probed the mirrored tool axis. Both normal-only fallbacks in IsoG68p2TiltSyntax read the transposed third column instead of the third row. On the no-hint path this only skewed a warning gate, but the explicit A/B/C path seeds its hint blend from that solve, so a machine with fewer than three rotary axes composed a mirrored tilted plane.
  • A Fanuc WHILE forward jump bound to the wrong END. Sequential loops idiomatically reuse DO 1, so a second WHILE's falsy-condition exit bound to the first loop's END 1 and redirected execution to a point before the second WHILE — an unbounded loop the iteration watchdog cannot see, because it only ticks on END reverse jumps. The jump now uses the anchored label scan the Siemens loop family already used.
  • Heidenhain G28 is MIRROR IMAGE, not a reference-point return. On the Heidenhain preset the shared pipeline had been reading it as the Fanuc reference return, so a G28 X block minted a phantom rapid to home while the mirror silently vanished. It is now simulated as a program-to-MC transform, ReferenceReturnSyntax leaves the Heidenhain logic list, and klartext CYCL DEF 8 records the same mirror statement so one program mirrors identically in either dialect. HardNc keeps the Fanuc reading, so the two engines are deliberately divergent on Heidenhain G28 files and any parity comparison must account for it.
  • MathUtil.Convert_inchdmin_To_mmds returned mm/min, not mm/s. The body multiplied by 25.4 and never divided by 60, so a caller trusting the name got a 60× feedrate; the sibling Convert_mmdmin_To_mmds divides as its name demands. No shipped path called it — the CL/APT feedrate route converts inches-per-minute to mm/min itself — so this changes nothing inside the product, but a caller who had compensated for the old behaviour must remove that compensation.
  • GetRByZ(List<PairZr>) never interpolated. The list overload looked up its ceiling node with the floor lookup, so floor equalled ceiling and the method degenerated into a step function. For a sharp cone whose inner-beam Z–R list has no node between apex and rim, that pinned the inner radius to 0 across the whole cone face and produced NaN flute vertices — the root cause of the transparent cone-tip flute that 3.1.180 addressed at the display layer. It affects every consumer of radius-by-Z interpolation, force geometry included.

Numerical results move even where nothing was renamed.

  • Five-axis IK is roughly 1000× tighter. The rotary solver behind XyzabcSolver now runs coarse→polish: the coarse stage keeps the original dot residual so solve success and failure semantics are unchanged, then a [dot, cross, rr-per-axis] system polishes. The dot criterion is quadratically blind to angle error — an envelope of about 1.4e-3 rad, and the resulting tip error is that envelope times the tool length — so adding the cross term restores quadratic convergence and drops the envelope to about 1e-6 rad. Failed solves no longer pollute the implicit seed, and the measure-zero perfect-saddle case is escaped by a deterministic retry offset instead of by leftover seed pollution.
  • ActMcXyzLinearContour steps by euclidean tip travel instead of the largest per-axis component, so LinearResolution_mm now caps actual tool-tip travel per step and a diagonal move produces up to √3× more steps at the same setting.
  • The SoftNc pipeline is the default engine.
  • Executed SyntaxPieces freeze to UTF-8 (below), so their JsonObject is a fresh read-only snapshot per call rather than a retained live graph.
  • SyntaxPiece.SentenceIndex becomes a session-global execution-order counter and is no longer contiguous per file.
  • Repeated NC diagnostics fold into per-run summaries at the run boundaries.
  • HardNc tool changes fire on M06 / Heidenhain TOOL CALL rather than on a changed T word — a bare T is magazine pre-selection, and a same-tool M06 still runs the changer cycle — and unset HardNcEnv tooling defaults become the three-axis shape (XYZ = NaN, NaN, 0; ABC all NaN). The old defaults swung all three rotary axes home on every M06, which no post expects.

Brand NC language coverage

The SoftNc pipeline is the default NC engine, and this is where most of the release went.

Siemens SINUMERIK

Real .mpf / .spf programs replay end to end, not an ISO subset. At 3.1.175 a program that declared its tool as T="NAME", shifted with SUPA, computed with R-parameters, called L-subprograms or looped with WHILE was not interpreted past that point.

  • Modal vocabularySUPA / G153 suppress all frames for one block; T="NAME" string tool calls with D cutting-edge offsets resolved through SiemensToolOffsetTable ($TC_DP lengths and radius plus additive wear); G70 / G71 units; path smoothing (G60x / G64x, FNORM / SOFT / FFWON / COMP* / UPATH, CYCLE832); MSG() and STOPRE; CR= and TURN= arcs. Tail comments became quote-aware, so a ; inside MSG("A;B") no longer truncates the block, and the preset stops misreading L and G74 as Fanuc-family codes.
  • EvaluationSiemensExpressionParser feeds the shared expression engine, so Z=R63+150 and X=SIN(R10)*20 drive motion. SiemensRParameterTable holds R0–R999 as per-case project data, DEF REAL/INT declarations lower into assignments, and $P_UIFR[n,axis,TR] binds both ways to SiemensFrameTable. Any other $-variable is recorded with an unsupported note rather than dropped.
  • Five axisSiemensProgrammableFrameSyntax simulates TRANS / ATRANS / ROT / AROT (with RPL=) into the tilt-transform chain in Sinumerik RPY order; SiemensTraoriSyntax makes TRAORI a real RTCP mode, the sibling of ISO G43.4, with TRAFOOF handing the offset back; SiemensCycle800TiltSyntax decodes CYCLE800's MODE bits for all four swivel modes. ROTS / SCALE / MIRROR are recognized and reported, not simulated.
  • Calls — L-prefixed and named subprogram calls resolve against SubProgramFolderConfig ({name}.SPF, then .MPF, then the bare name) and inline with their P repetition count; M17 / RET pop a frame; REPEAT re-runs a labelled slice; MCALL CYCLE81/82/83/85 maps onto the shared canned-cycle machinery; PROC headers and labels are claimed whole.
  • Control flowGOTOF / GOTOB, IF / ELSE / ENDIF, and WHILE / FOR / REPEAT-UNTIL / LOOP. Runaway programs are bounded rather than hanging the session: SiemensGotoIterationDependency caps jumps per (file, label) and SiemensLoopIterationDependency caps iterations per (file, loop-entry line); over the cap the construct warns and falls through.
  • Per-word coordinate functionsAC() / IC() / DC() / ACP() / ACN(), including on I / J / K circle centres. G90 C=IC(360/17) is one incremental index inside an absolute program. Direction resolution lives in McAbcCyclicPathSyntax: ACP() takes the [anchor, anchor+360) window, ACN() the (anchor-360, anchor] window, DC() the shortest swing, with the exact 180° tie going negative.
  • Coded positionsCAC / CIC / CDC / CACP / CACN take a 1-based indexing position number rather than a coordinate, resolved against IIndexingPositionConfig, implemented by SiemensMachineDataTable from the real machine data (the MD30500 axis assignment, the MD10910 / MD10930 position tables, the equidistant MD30501–30503 definition).
  • G74 / G75 fixed-point return is claimed as a whole block, so its dummy axis values no longer mint a rapid to the coordinates written in the block and its F never reaches the modal feedrate.
  • OEM auxiliary M-codes — the preset declares M12 / M13 / M22 / M23 and M330 / M331 note-only, so each occurrence voices DeclaredMCode--UnmodeledEffects instead of an unknown-code warning, without inventing simulated effects. A machine's own table overrides a declaration when the real effects are known.

Heidenhain

Both dialects play on one preset, HeidenhainNcRunner.

  • Klartext motion and setup — the L statement and its axis words, FMAX, M91 as a one-shot machine-coordinate move, TOOL CALL wired to tool change and spindle speed with the table height and DL. Datum handling follows TNC semantics: CYCL DEF 247 sets the preset and CYCL DEF 7 is an additive shift on top of it, composing as separate transform-chain entries instead of replacing each other, resolved against HeidenhainDatumTable. Arcs (CC pole plus C statement, DR- = CW, implicit centre, closed arc = full circle), RL / RR / R0 radius compensation, the M126 / M127 rotary-wrap state, M140 MB retract, and CYCL DEF 32 TOLERANCE.
  • Q-parametersHeidenhainExpressionParser lexes Q / QR / QL / QS, the DIV keyword of FN 4 and the prefix SQRT of FN 5, so FQ1 reaches the feedrate, L X+Q2 reaches the program XYZ and TOOL CALL SQ3 reaches the spindle speed. HeidenhainQParameterTable holds Q0–Q99 free and QR0–QR499 permanent parameters as per-case project data. Unimplemented opcodes (FN 14 / 16 / 18…) are claimed and reported rather than half-read, so an FN 18 SYSREAD target stays vacant instead of taking a fabricated value. FN 9–12 conditional jumps execute, with a (file, label)-keyed iteration cap.
  • Tilt and RTCPPLANE (SPATIAL fully composed with SEQ / COORD ROT / TABLE ROT and STAY / MOVE / TURN positioning; VECTOR structurally captured; EULER / POINTS / RELATIV / AXIAL / PROJECTED consumed and warned with the previous tilt retained, so a PLANE AXIAL B+45 B word can never be mistaken for a rotary axis command), FUNCTION TCPM, and real M128 / M129 tool-centre-point control.
  • Cycles and callsCYCL DEF 2xx bodies with their Q parameters mirrored into the block assignments, cycles 200 / 232 / 251 / 252 / 253 mapped onto the shared G81 / G82 / G83 slots, CYCL CALL / CYCL CALL POS / M99 / M89 splitting call-once from modal firing, CALL LBL inlining up to LBL 0, CALL LBL n REP m as a section repeat, and CALL PGM resolved by file name. The multi-line tilde continuation form is joined at segmentation (JoinTildeContinuations, on by default).
  • Post-processed spellings — some post-processors write a whole klartext body with no separators at all, which used to yield zero motion. Glued line shapes (LX-26.3Y+43.1, FMAXM03M08, …R0FMAX) and the detached feed spelling (F 20000) now parse, with M140, M128 and PLANE MOVE widening their own F capture so a retract or feed-limit value cannot leak into the modal feedrate. BLK FORM is recorded as a brand-neutral stock declaration without replacing the project workpiece setup, and the klartext STOP word joins M00 / M01.
  • DIN/ISO dialect% tape header, N block numbers, T + M06, absolute I / J / K arc centres with the modal pole carried forward, the ISO label family (G98 L<n> definitions and the head-anchored L<n>,<m> call mapping the comma count onto REP), G247 Q339 stamping the same datum preset as CYCL DEF 247, G54 with axis words read as a datum-shift declaration, and G70 / G71.

Fanuc and ISO common

  • Polar coordinate interpolationG12.1 / G13.1 on the SoftNc pipeline. Before this a polar section parsed silently wrong: the X word (a diameter) and the C word (a hypothetical Cartesian axis in mm) were consumed as ordinary XYZ and rotary degrees. ProgramRxczSyntax halves X from diameter, resolves G90/G91, writes the polar and derived Cartesian positions and the machine C angle, and classifies motion into polar linear and polar arc — the latter emitting ActMcPolarSpiralContour, which keeps spiral geometry in central polar coordinates and stays continuous across ±180°. G41 / G42 compensation is resolved on the hypothetical plane, a C-axis speed clamp applies, and YA / ZB axis pairs are supported. PolarGCodeCheckSyntax scans for incompatible G codes before the mode syntaxes consume them.
  • Custom Macro B#var assignment with range-routed stores (#1#33 local per macro frame, #100#499 volatile cleared on M02/M30, #500#999 retained and persisted in the project, #3000#3999 system-control), boolean and logical operators, IF[..]GOTO n, IF[..]THEN <stmt>, WHILE[..]DO m / END m with a bounded-loop watchdog, and position and tool-offset system variables. M98 P_ L_, M198 external call, M99 return and M99 P{seq} early return; G65 one-shot macro call with A–Z → #1#26 argument binding, and G66 / G67 modal macro.

Cross-brand

  • A shared DwellSyntax consumes G4 and G04 with the dialect held on the instance: Fanuc-family X / U seconds, P milliseconds, S spindle revolutions; the Siemens instance reads F seconds and S revolutions. Capturing the G04 spelling fixes a real defect — the un-captured spelling fell through to the flag and axis syntaxes, where a Fanuc G04 X0.5 dwell time became a ghost X motion word.
  • Machine-declared M-codesIMCodeDeclarationConfig and MCodeEffects let a machine state what its own OEM codes do (composite spindle+coolant codes, a tool-change trigger, turret T-word semantics), and MCodeExpansionSyntax expands a declared code into the canonical ISO flags the shared consumers already understand.
  • Custom spindle M-codes drive the spindle direction through a machine-level ISpindleControlConfig; ISO M03 / M04 / M05 remain the built-in fallback. An S greater than zero with no direction ever issued assumes clockwise and emits SpindleDirection--AssumedCw, so the physics gate no longer silently produces zero mechanics for a whole file.
  • Tool changes synthesize their axis travel. ToolChangeMotionSyntax overlays the per-axis tooling position from IToolingMcConfig onto the current pose (a NaN or missing axis stays put) and stamps a one-item rapid compound motion, and ToolChangeSemantic moves behind CompoundMotionSemantic so the tooling step lands at the tooling point. Programs that retract on their own overlay to a zero-length move and emit nothing extra.
  • Per-brand pivot gates. PivotTransformationSyntax reverts to the ISO/Fanuc vocabulary (G43.4 plus the G68.x family), SiemensPivotTransformationSyntax gates TRAORI / CYCLE800, and HeidenhainPivotTransformationSyntax gates the M128 / PLANE vocabulary. All three compose the identical entry through the shared PivotTransformUtil. Exactly one brand gate belongs in a pipeline list — never register two.
  • Controller presets are writable and shipped. A controller resource file is one serialized SoftNcRunner — the whole pipeline that decides how a brand's NC code is interpreted. ControllerPresetWriter serializes the built-in brand presets (CreateBrandPreset, WriteBrandPresetFile, WriteAllBrandPresetFiles) under Resource/Controller/ with the .Controller extension, and HiNc-Resource ships all five so the load browser starts populated. The static brand properties remain the source of truth; the files are regenerable snapshots. Writing needs no XFactory registration — reading one back does, because the loader drops unregistered pipeline entries silently rather than failing the load.
  • Saved pipelines back-fill their system-wired dependencies on load. A project saved before a system-wired dependency existed never self-healed by round-tripping, because re-saving stamped a fresh API version on the same incomplete list. The SoftNcRunner XML constructor now appends the missing ones after the legacy version patches. A runner rehydrated from an older file still keeps the syntax list it was saved with, though — take the regenerated preset, or a fresh NcRunnerSuit built from it, rather than expecting an old file to grow new syntaxes.
  • Machine-coordinate and tilted-plane failures report. G53 and G53.1 record their source G-code on the parsed block and emit Coord-MachCoord--005 / --006 / --007 on paths that used to fail silently. A G68.2 tilted plane the machine cannot reach emits Coord-Tilt--001 / --002, with a tool-axis-only IK retry that avoids a spurious warning on a machine with fewer than three rotary axes.
  • Session-global sentence indexing. SyntaxPiece.SentenceIndex used to be assigned by two independent sequences, so indices collided as soon as a call was inlined mid-stream. The new SentenceIndexCounterDependency supplies every index at one chokepoint, so values are session-globally unique and strictly increasing in execution order, including nested and repeated calls.
  • Inline plays can loop and jump, and playing a file no longer holds it open. Inline NC-code plays stamp their pieces with the command title as a pseudo-path, and every control-flow re-segmentation re-read the host file by that path — the existence check always failed, so loops fell through without looping. RunNc now registers the raw lines on NcLineSourceDependency and LabelScanUtil reads memory first, disk second.

NC optimization and writeback

Important

NC optimization is not finished on the 3.2 line. What follows describes the leg as it stands, and it is published for review, not for production. Work that depends on optimized output should stay on the 3.1 line, serviced as 3.1.175.<patch>.

  • The optimizer runs on the SoftNc pipeline by default. OptimizeToFiles keeps its signature, its script snippet and its HTTP route, but when EnableSoftNcRunner is on and the session holds played SyntaxPieceLayers, it delegates to the new OptimizeNcFiles; otherwise the frozen HardNc path runs unchanged. The new leg classifies the final SyntaxPiece layer, solves the per-step feed adjustments from milling physics, and regenerates text as anchored token edits over the verbatim source block — lines the optimizer does not touch round-trip byte-identically instead of being re-synthesized. Per-file results are retained in NcOptimizations. EnableIndividualStepAdjustmentLog drives both legs.
  • Depth splition re-interpolates through a planned fragment path, with separate modal chains for the pre-build feed and the emission endpoint, per-step machine-to-program-frame inversion, and arc IJK recomputation including R-to-IJK conversion. Two cases where the frozen HardNc optimizer is silently wrong are downgraded to a non-split rewrite rather than reproduced: a G91 incremental block (the old fragment rewrite always emitted absolute coordinates), and a Heidenhain klartext block (the rewrite would splice ISO-shaped I/J/K words into a block whose arc centre comes from a preceding modal block).
  • The compensation stage is no longer dead on the SoftNc leg. The HardNc baseline wrote the compensation into the step contexts while its output read the piece packs, so it never emitted a compensated coordinate at all. Each fragment endpoint is now offset — XYZ only, rotary words untouched — by the tool-tip deflection rotated into the leaf program frame the endpoint lives in, so a G68.2 tilted setup is compensated in the right direction. It is consumed on re-interpolated splition fragments only, and CompensationMask defaults to 0, so with no mask set the stage is a strict no-op.
  • Output follows the source's decimal digits. Both legs used to write coordinates through a fixed F4 and every F word through F2, so a program stated to three decimals could come back with a fourth — an alarm on controllers strict about their least input increment. Digit counts are now scanned per word family over the played source texts with comment spans masked, floored at 3/3/0 and capped at 9, and threaded through the whole write path. The word-suppression tolerances and the F comparison grid derive from the resolved digits instead of the old fixed literals.
  • Optimized NC is written back in the source file's encoding. NC play reads and optimized writes go through DetectRoundTripEncoding — BOM, then strict UTF-8, then Latin-1 — so an ANSI-family file (GBK, Big5, Shift-JIS) re-encodes to its original bytes instead of having every undecodable byte replaced. Comments in those encodings survive the round trip.
  • A feed change no longer mints a one-step carrier fragment. Under an arc split that fragment is a degenerate arc whose start and end nearly coincide, which an incremental-IJK reader — or a control that treats begin == end as a full turn — expands into a full circle. An F re-statement is now handed to the feed run's next emitted fragment, the one that actually runs at that feed.
  • A splition fragment at the head of the stream writes only the axis words the source stated, so it can no longer invent a Z0. whose value under the source's silence is just the home-fallback modal.
  • Unparsable NC lines survive. A line the parser could not read used to be dropped from the runner's line list, so the optimized file silently lost it. HardNcRunner now rebuilds a parse-failed line as an opaque no-op — the empty-text parse inherits modal state like a blank line, and the raw text is restored for writers — and the optimizer mirrors the fallback, so IF blocks, #-variable macros, G68 R# and GOTO come through verbatim. A BuildNcLines--ParseFailed diagnostic still reports the line, and the line stays un-simulated. The output writer is also closed in a finally, so an exception can no longer leave a half-written file locked. First delivered on the 3.1.175.x service line.
  • A Siemens CYCLE800 swivel counts as a macro line in the piece classifier, so it is preserved rather than treated as an optimizable motion block.
  • The host key HiNC:OptCoreNum governs both legs again. It used to set only the legacy NcOptProc.CoreNum; once the SoftNc optimizer became the default path the key silently stopped governing anything. It now assigns both (0 = derive from the processor count).
  • NC text writeback regenerates NC from a program that has already been played, in two stages over the session's finished syntax pieces: a converter turns the source piece stream into a destination stream plus a bidirectional source↔destination map keyed by sentence index, and a reverse segmenter serializes that stream back to lines. The data→text seam is a brand-agnostic sentence composer, first implemented for Fanuc. A patch-mode writer performs positional token edits — NaN deletes a word absorbing one separator space, insertion follows conventional order, trailing zeros trim keeping the dot — and refuses to rewrite variable, bracket and keyword values rather than corrupting them, reporting Writeback-Patch--VariableValue, --KeywordValue, --CommentOnlyText and --EditUnmatched.
  • A second diagnostic home. NcManipulationDiagnosticProgress is a sibling of the play-time NcDiagnosticProgress, dedicated to NC-rework operations — writeback conversion and optimization — so a play reset never discards manipulation results and vice versa.

Milling physics, training and measured data

  • Physics runs in the native kernel. The per-step milling physics migrated into core.dll in stages: the engagement is scan-converted natively at the substraction completion point, the force kernel is reached through the same handle with no managed marshal, and the sequential cutting-temperature and wear chain runs from a native thermal session held per (tool, cutting parameter) pack. The switch is EnableNativeMillingPhysics, surfaced runtime-only (not persisted to project XML) as EnableNativeMillingPhysics and EnableNativeMillingPhysics, and it now defaults to true. Public entry points that used to reach the managed kernel still work: without a session pack they build an ad-hoc physics pack per live (cutting parameter, tool) pair and run natively.
  • MillingToolPhysicsPack is an immutable record holding one tool's scalar derivations for one cutting-parameter set — spindle-buckle-to-tip length, observation height, effective cutting diameter, the bending/Z-deflection pair, the simplified rake angle, the minimum uncut chip thickness. MachiningSession owns the packs keyed by tool id (GetToolPhysicsPack) and invalidation is explicit at the points that know the state changed: every run-op start and the tool-change act, plus InvalidateToolPhysicsPacks. The corresponding MillingTool / MillingCutter members are now deliberately uncached pure computations.
  • Milling-force waveforms are reproducible again. The parallel per-step force build read lazily built scalar caches on the shared tool objects; a thread could pass a cache guard and then read a value a concurrent writer had stored in between, so two plays of the same NC exported different forces in the thin-chip window of each tooth pass. The caches were first republished as single immutable references and then removed in favour of the frozen session pack.
  • Thermal gating and seeding. The sequential cutting-temperature and wear build now checks EnablePhysics (spindle temperature deliberately keeps running), and the tool-change thermal seeding re-arms whenever the incoming chain state has no flute temperature list — which covers fault and cancel re-seeds, stop-then-replay residue, and EnablePhysics being switched on mid-session. The shank temperature list is seeded to the exact node count the thermal FEM builds, so trailing shank nodes no longer sit at 0 K after a tool change.
  • Cutter geometry is validated up front. GetUpperBeamGeometryIssues collects upper-beam and shank configuration problems as keyed messages — for example an extended-cylinder beam whose full length sits below the flute height (Cutter-UpperBeam--BelowFluteHeight), which inverts the shank solid and makes the shank thermal model unbuildable. They are reported once per tool at BeginSession and at each tool change, instead of surfacing later as a null-reference cascade inside the thermal physics with nothing naming the beam.
  • RakeFaceCuttingPara3d no longer throws on a six-field parameter string (the guard read the seventh element behind a >= 6 check), and the published coefficient index mappings are corrected: the LocalProfileMillingPara(Vec3d, Vec3d) constructor maps (x,y,z) to (Ksr, Kst, Ksa) / (Kpr, Kpt, Kpa), and the 2d element index range is 0–3 with 0=Ksc, 1=Ksn, 2=Kpc, 3=Kpn.
  • Training diagnostics name their cause. The per-step warnings split into Train-StepLuggage--Unreadable (the step luggage row could not be read back) and Train-StepEngagement--Missing (the row is present but the engagement was never built because physics was inactive at simulation time). The gather pass counts both against the eligible steps: silent at zero, one summary warning at or below MissingEngagementAbortRatio (default 0.25), and a configuration error above it. A gather pass that produces no samples at all now reports immediately rather than throwing inside the SVD solve, separating “not one step touched the workpiece” from “touched steps whose mapped force data yielded no usable shots”.
  • New training knobs. EnableDesignMatrixSolver (default false) solves the least squares on a thin QR of the design matrix instead of forming the normal equations, which square the condition number; DesignMatrixSvdRelativeTol is its truncation cutoff. EnableCwePhasePairing (default false) determines each step's rotation phase with a cutter-workpiece-engagement block-pairing detector instead of the self-bootstrapped lead parameter, for one-flute and symmetric two-flute cutters in light radial side cuts. ReTrainAnchorOutputScale exposes the virtual anchor weight. LastMillingParaTrainResult captures the outcome — kind, sample flags, outlier ratio, success, output file, parameter name and note, correlation R, filtered sample count, parameter XML, timestamp — so a caller reads it without re-opening the .mp file.
  • Time mapping is reworked around absolute wall-clock time. AddTimeDataByFile accepts DateTime windows, stored as IFileTimeSection forms, and the project-scoped MappingAnchorDateTime — seeded set-once from the date of the first controller instant seen — converts controller timestamps onto one run-relative axis. EndTimecode replaces AccumulatedTime as the canonical end-of-step time.
  • CSV timing survives midnight. Step durations derive from full date-bearing instants, so a multi-day recording no longer produces negative durations and a negative chart time axis, and a non-physical duration from a spliced recording is clamped with a validation warning instead of stalling physics evaluation.
  • Wall-clock time is dense. The trio moved into one optional sub-object, StepActualTime (Timecode / Instant / IsInterpolated), reached through ActualTime. On CSV plays every built step is stamped: steps built from a controller row re-anchor, and the steps between extrapolate along the machine timeline and are marked interpolated, which makes the actual-time mapper window per-step exact instead of sparse-anchor scaled. Pure NC plays keep null stamps.
  • An empty step-shot pairing window is a data gap, not something to interpolate across. The window builder used to expand outward to the rows bracketing the gap, silently pairing such steps with force values that were never measured — a training run over a file whose transients had been carved out produced a plausible correlation and a full set of coefficients derived entirely from fabricated rows. Such a window now skips its step and one Map-ShotGap--StepsSkipped warning per mapping call carries the count; a window-edge row is interpolated only when its bracketing rows span at most two spindle revolutions.
  • “No cut / No data for step” on freshly-simulated steps is fixed. The bulk step-data readers cached the absence of rows the writer had not committed yet, so a step that had just been simulated could report no data until the program was re-run. A covered-but-missing index now drops the stale segment and re-reads.
  • New end-of-play warnings, each once per session: Play-Touch--None (the play finished without any step touching the workpiece), Tool-FluteCount--Zero (physics is on and a milling cutter resolves to zero flutes, so feed per tooth is undefined), and Play-Physics--None (physics is on and at least one step touched the workpiece but no touched step carries a physics brief — naming the three things that gate it: a tool bound to the spindle, the spindle actually rotating, and the workpiece cutting parameter). That last state previously surfaced one process later, as a training run gathering zero samples.
  • Performance is collected in its own section below.

Cutter-location (CL) playback and CL-to-NC

  • Replay an NX CLSF / APT-source toolpath directly. PlayClFile reads MSYS / FROM / GOTO / CIRCLE / RAPID / FEDRAT / SPINDL / COOLNT / TLDATA / LOAD. The parser is the NxClRunner preset — a SoftNcRunner composition — and the project holds a third runner suit ClsfRunnerSuit beside the NC and CSV suits. A TLDATA record creates the tool geometry when the tool house has no matching id. Run-ops are first class: ClRunner, MachiningSession.PlayClFile / RunClFile. See Cutter-Location (CL) Playback.
  • A CL file can now drive a machine-tool chain, not only a ClMillingDevice. When the pipeline's kinematics dependency resolves to a live solver, ClToMcTransformSyntax inverse-solves every CLSF motion endpoint at parse time and expresses the result in the same ProgramToMcTransform vocabulary the NC pipeline uses — a tool-height entry from the active tool, a pivot entry anchored to the workpiece frame, and the solved rotary axes in raw degrees for the cyclic wrap tail-pass — so McLinear and the wrap are reused unchanged. Played onto a ClMillingDevice the same file is still pure cutter-location motion, which is what you want for verifying a CAM toolpath before any post-processor is involved.
  • CL moves resample along the true path. GetClSteps walks IClPath.At for each intermediate step rather than linearly interpolating between the path's begin and end, so a CL arc actually curves and the tool axis rotates along the path instead of through it. ClLinear gained two guards on the same path: the near-parallel begin/end normal case short-circuits instead of falling into a degenerate cross product that yielded a NaN rotation axis, and the interpolation reads its rotation delta through the lazily-built property rather than a still-null backing field.
  • CL tool changes teleport instead of stepping. A stepping change stamps collision detection and volume removal at the chain's current pose, and at CL session start that pose is identity — the tool sitting at program zero inside the workpiece. The CSV pipeline's teleporting semantic is promoted to the shared ToolingTeleportSemantic (the old CsvToolingTeleportSemantic element name is kept as a load-only alias), and the first GOTO after a LOAD / TOOL is forced into a reposition so a new tool never sweeps a cut from the previous operation's endpoint.
  • Convert a played CL program into Fanuc NC files. ConvertClToNcFiles, its HTTP action, and the LocalProjectService / MachiningSession entry points walk the session's final syntax-piece layer, group pieces per source file and write one NC file per source, template-substituting [NcName] (default Output/[NcName].nc). It requires a prior play on a machine chain — a pure-CL device leaves no machine-solved sections to serialize — and reports ConvertClToNc--NoPlay otherwise. Stage-one results are retained in NcConversions as the hook for source↔output cross-navigation. A mission can declare the writeback through EnableConvertClToNcFiles and ClToNcFileTemplate.
  • CL→MC hardening. Tool-offset resolution walked back to the distant LOAD block for every motion (O(N²) on production-scale files) and is now O(1) through a modal active-tool section; the documentary program-to-Pn stamp is stamped once per run rather than walked per motion; the program-zero query no longer deep-clones the whole equipment assembly per motion block; the warned-tool-id set is run-scoped, so the ClToMc--NoToolOffset warning is no longer suppressed on every run after the first; and MachineAxisConfig gains a public Clear() so a machine switch rebuilds the axis table instead of accumulating stale rotary axes.

Session, project and command model

  • A parser and its per-case data travel as one file. NcRunnerSuit bundles a runner with the dependency data a particular job needs, as a single file-loadable unit. The project holds three suits — NcRunnerSuit, CsvRunnerSuit and ClsfRunnerSuit — and ReadNcRunnerSuit / WriteNcRunnerSuit switch the active parser mid-project from a suit file. A switch attempted while a program is playing is refused with ReadNcRunnerSuit--Refused.
  • One NC runner configuration is shareable across projects. Per-case data — tool offsets, work-coordinate offsets, Siemens frames, Heidenhain datums, retained macro variables, seeded brand parameter tables — travels as proxy placeholders inside SoftNcRunner and resolves against the owning project's per-case list, so a controller configuration is no longer welded to the job it was first built for. Machine-config consumers read the resolved view through GetEffectiveNcDependencyList; legacy <NcDependencyList> XML and NcEnv-based projects still load and migrate automatically.
  • HTTP guards and one envelope. RequireActiveSessionAttribute answers a session-scoped action with HTTP 409 and an ApiActionResult.NoActiveSession() body when no session is active, instead of the previous null-reference 500; RequireLoadedProjectAttribute does the same for the project-level controller. Both are applied at the controller level and honour opt-out markers (AllowNoActiveSessionAttribute, AllowNoLoadedProjectAttribute). Mutating actions inject a fresh MessageCollector and return the collected notifications inline in the shared ApiActionResult envelope, so a REST or AI caller sees progress, success and error messages in the response instead of only out of band. LocalProjectServiceController exposes the project-level (session-independent) surface parallel to SessionShellController.
  • Session commands declare themselves. CommandCatalogAttribute marks an ISessionCommand as user-addable and places it in a CommandCategory (Setup / Program / Optimization / Output / Flow, declaration order = display order) with an Order sort key and an optional wire kind (default: the class name minus the Command suffix, lower-cased). CommandFieldAttribute marks a bool / int / double / string property as a directly editable scalar with an optional label, unit and physics-licence flag, so a generic editor renders and updates it without a hand-written form. A command without the catalog attribute stays loadable from project files but is not offered for creation.
  • Program File dispatches by kind. NcFileCommand gains an NcKind property (XML element NcKind, absent = Auto for legacy projects) and its mission label becomes “Program File”; each command resolves its own file, so a List of Program Files can mix brand NC, CL and CSV.
  • Two session commands saved but could never be read back. ListCommand.Reg chained every type except NcOptOptionCommand and RecordMeshedGeomCommand, so any XML round-trip of an entry holding one threw KeyNotFoundException out of the XFactory generator lookup — reloading a saved project containing an NC Optimization Config command failed. Both are now chained.
  • Naming. Title is an optional name shown in place of the type name, so nested command lists can be named in the mission tree. PreSettingCommand displays as “General Config” and NcOptOptionCommand as “NC Optimization Config” — display strings only, so serialization and endpoints are untouched. A bare non-list PlayerCommand root is normalized on read into a single enabled entry of the default ListCommand. Command titles, catalog categories and field labels are now localized, and a zh-Hans resource set was created (none existed before). Default-script template keys stay untranslated on purpose — they compose the C# comments and script title written into the user's .hincproj, which travels to other machines.
  • Script faults are keyed errors. A CompilationErrorException or a faulted script task used to surface as an anonymous warning; ScriptCommand now reports ScriptCommand-Compile--Failed with the full diagnostic list and ScriptCommand-Run--Fault with the exception, both at Error severity.
  • A fresh session re-homes the machining chain. ResetRuntime wrote the configured XYZ home but hard-coded ABC to 0, and it only ran on project switch or pace-player reset — never before a plain Play. A freshly loaded project therefore started from whatever pose the .mt happened to serialize, while the act stream interpolated from the home seed, so the first contour swept from a pose the machine was never at and cut along the way. The re-home now reads the rotary homes from the same home configuration and also runs at BeginSession; a mid-session replay is untouched.
  • What a reset actually resets. ResetRuntime now also rewinds the NC-runner session state, so the next play restarts file and line indexing from scratch the way PowerReset does, and it resets the CL device pose to identity. It no longer clobbers MachiningResolution_mm: the runtime resolution is seeded from the workpiece's initial resolution only when a project loads, so an explicit override survives a runtime reset and a workpiece swap.
  • Mixed runner kinds in one session are refused. NcRunnerSessionState remembers the runner that initialised it and RunNcLines refuses a different one with RunNcLines--RunnerMismatch — reachable now that NcKind.Auto makes mixed-kind missions a first-class flow.
  • Stale state on a chain or project switch. Building the coordinate converter nulls the rotary solver when the chain is not an IXyzabcChain, so a solver built for the old machine no longer keeps converting after a switch to a CL device; and ClearCache now also calls ClearIdealGeomCache, so loading another project stops rendering the previous project's target geometry.
  • Project-file operations are serialized through a zero-wait gate. A New / Load / Save / Reload arriving while another is in progress throws ProjectFileBusyException immediately instead of racing into a file-in-use IOException.
  • AlignWorkpieceProgramZeroToIso computes in the machine-zero state. It reflects the assembly and zeroes every dynamic axis before querying displacements, so the alignment is correct even when the live machine's axes are displaced.
  • RunCount counts runs started, incremented synchronously before the run's task launches and never reset. Pairing it with IsFinished in one snapshot lets a polling client distinguish “the run I started has finished” from a stale Finished left by a previous run.

Geometry, rendering and native stability

  • Disposing a display or geometry object still in use no longer crashes the process. A client disconnect, a meshed-geometry reset or an app shutdown could take the process down with an access violation. DispEngine and CubeTree native calls are now gated against a concurrent dispose, and disposals run serialized on a single background chain — IsDisposed reports the state and DisposeBackground enqueues a tree, or a collection of its attachments, onto that chain.
  • Large geometry no longer freezes the UI. Solid builds its display topology off-thread and draws a wire bounding box with a “Loading” mark until it is ready.
  • ClStrip raises a Cleared event after Clear, pairing with the existing PosAdded.
  • A P/Invoke correctness sweep reconciled the managed declarations against the real export table. It fixed a non-existent export (substraction_ExpandToBox3d), two void natives declared as struct returns (a garbage register read), two ToString marshals that made the CLR free foreign memory, and a log-callback delegate nothing rooted against GC — native could call a dead thunk long after the P/Invoke returned. These are the crashes a user reports as “it crashes randomly”.
  • Milled step colours no longer go stale at random. Step colours are baked into the native cube-tree attachment, but the refresh only flagged the strip while the display cache was freed immediately, so a frame landing in that window rebuilt the cache from stale colours and nothing cleaned it again. The service now cleans the cache once more after the re-stamp. A paired fix stops an empty strip display window swallowing a pending recolor indefinitely.
  • The dimension bar reads on any background. It was a single hard-coded near-black stroke left over from the light-host era, invisible once the front end went dark. It is now drawn as a wider dark halo under a white main stroke, with the halo pushed back whole depth steps rather than the white pass nudged forward by half a depth LSB — which intermittently lost the depth test.
  • New geometry API. CarveStl is the managed face of the native exact triangle-CSG container. Add is the boolean-union counterpart of Substract, returning UnmanagedAddition, and AddBySweepingVolume mirrors RemoveBySweepingVolume — the geometry-layer symmetric point for additive processes. There is no deposition step in the machining runner yet, so this is a geometry-level API only.
  • FreeformBottomContour is the user-editable point-list bottom contour, mirroring the side contour with the key axis switched to radius.

Performance and footprint

Every figure below is a measurement with its conditions stated, and where a change cost time to buy correctness that is said too. Read the ratios rather than the absolute times: several campaigns were run on Debug builds or on small machines, deliberately, because a paired A/B on one machine answers “did this get faster” far more reliably than an unpaired Release number on a fast one.

Milling physics in the native kernel

The per-step physics moved into core.dll in stages — engagement scan conversion, the force kernel, then the sequential temperature and wear chain. Measured as a same-day paired A/B, managed leg against native leg, on one circular test program:

Per touched step managed native
Engagement build 46.0 ms 5.03 ms ~9×
Force (GetMillingFoce) 17.1 ms 0.75 ms ~20×
Temperature and wear chain 3.97 ms 0.24 ms ~16×
Whole play 59.3 s / 16.1 GB allocated 40.6 s / 3.4 GB allocated −32% wall, −79% allocation

Allocation is where the migration bites hardest: engagement construction alone fell from about 5.2 GB to 29 MB per step.

Conditions. Debug x64, EnablePhysics on, collision off, one force worker, a two-core / four-thread laptop, ±10% thermal-throttle noise, single paired run per leg. Absolute times are not representative of a customer machine — the ratios are the claim.

Numerical parity. Engagement, force and brief are bit-identical between the two legs on Windows. The Linux build is not bit-identical (different libm), so a cross-platform comparison should use a tolerance, not equality.

Playing a long program stays linear

NcOptOption.Equals ended on a null-propagating comparison of a dictionary that is created on demand and is null on virtually every option, so the whole comparison collapsed to false — an option compared unequal even to a copy of itself. Both record-on-change guards built on it were therefore dead: the session appended an option-map entry for every played act instead of only at change points, and the step rewrote unchanged entries. Reading the last recorded option through a LINQ LastOrDefault over a SortedList<,> — which has no indexed fast path — then walked the whole map each time, so the two defects together made a long play quadratic.

Measured on a 2.35-million-line Siemens program: the option map now holds 1 entry instead of one per line, and the per-100,000-line rate stays flat instead of degrading from 73 s at the start of the file to about 11 minutes by 1.9 million lines.

This applies to every runner and to sessions doing no optimization at all, because the call site is the session-level play loop. GetHashCode drops the dictionary in the same change, since it hashed by reference and would otherwise disagree with Equals — relevant if you use NcOptOption as a dictionary key.

Where the time actually goes

Worth knowing before you tune anything. After the migration, on the measured workload the whole parallel physics stage is about 3.3% of wall time, while the single-worker volume subtraction is about 77% — and that subtraction is single-worker as a correctness requirement, not as an oversight. Raising the force-worker count therefore buys nothing on any machine; the bottleneck moved rather than disappearing.

What did change in the worker derivation is narrower than it sounds. An unmeasured six-core ceiling was removed, but it only ever governed the sweep workers, and only machines with nine or more logical processors see a different count; force workers are unchanged everywhere. The throughput benefit on such a machine has not been measured — the development machines are smaller — so this is a ceiling removal, not a claimed speedup.

Queue depths became fixed item budgets (120 geometry, 3840 physics) rather than scaling with the core count, because those queues bound per-item memory: uncapped, a 64-core machine would have been handed 40,960-deep physics queues. On machines with fewer than six cores this is a small increase in bounded-queue memory (from 80 / 2560), which is the honest cost of the change.

Loading a large STL workpiece

Building the topology from an STL was quadratic in triangle count — a pointer-derived hash collapsed into a handful of buckets, so lookups degenerated into linear scans. On one 935,000-triangle binary STL, topology construction was 99.6% of the entire load; reading the file off disk was 0.16% of it. With a multiplicative hash mix the build is linear:

Triangles before after worst bucket
100,000 8,711 ms 1,305 ms 6.7× 2,245 → 21
300,000 89,036 ms 4,750 ms 18.7× 6,593 → 23

Per-triangle cost is now flat (0.013 → 0.016 ms/tri across a 3× size increase), which is the real result: the cost grows with the mesh instead of with its square. Extrapolated to the full 935,000-triangle mesh that is roughly 14 minutes → 15 seconds.

Deduplication and the resulting topology are unchanged — the equality predicate was not touched, and the 300,000-triangle case produces an identical 899,997 lines before and after. Separately, the managed-to-native STL handoff dropped from three full copies of the buffer (about 86 MB each, plus around twenty doubling reallocations) to two.

Conditions. Native test harness, debug CRT — which inflates container-operation constants, so the absolute milliseconds are an upper bound. The composition breakdown and the complexity change are build-configuration independent. The full-mesh figure is an extrapolation, not a measured run.

Re-triangulating after a cut

The marching-cubes step gained a lookup table, and produces fewer triangles for the same surface:

Tree before after triangles
17 MB diagnostic 0.63 s 0.29 s 2.17× −35%
30 MB demo 1.29 s 0.60 s 2.15× −35%
309 MB customer part 14.68 s 6.51 s 2.25× −43%

Scope. This lands on the rebuild burst after a cut invalidates cached geometry, not on steady-state rendering, which draws from the display cache and is unchanged.

It is also an approximation change, not purely a speedup: a non-finite cut drops its triangles, so a sub-voxel feature vanishes at that level of detail instead of being capped. That is what fixed the broken-face slabs seen on RTCP paths. The contact-loop extraction used by milling physics deliberately still uses the previous walk, so physics results are untouched.

Session memory: a long program no longer exhausts the client

A session retains every executed NC block for its whole lifetime. Once a block leaves the executing window its piece is now frozen to compact UTF-8.

Measured on a 25,018-block play: session retention 406 MB → 142 MB, about 2.9×.

The encoding itself is smaller than that ratio suggests — roughly 12 KB per line live against 1.6 KB frozen, about 12.9× — because a retained piece carries more than its JSON. The 2.9× is the figure that matters for whether a program fits in memory.

The trade is explicit: after the freeze the JsonObject getter re-parses on every call and returns a fresh read-only snapshot, with no caching and no write-back. Code that reads the same piece repeatedly should hold the snapshot in a local. The switch is FreezeExecutedPieces, on by default.

Sizing a meshed workpiece

A cube tree costs roughly 3.2× its file size in RAM while loaded — a 10 GB .wct at 0.125 mm resolution is about 95 million nodes, holding around 25 GB of tree plus 6 GB of index. Tearing down a tree that size used to block the caller for over two minutes; disposal now runs serialized on a background chain, so the thread that dropped it does not wait. The remaining cost is genuine work: the live renderer must not be left showing geometry that no longer exists.

Cutter-location files at production scale

Three costs were removed from the CL-to-machine path, and on a production-scale file they are the difference between replaying and appearing to hang: tool-offset resolution walked back to the distant LOAD block for every motion (O(N²), now O(1) through a modal section), the documentary program-to-Pn stamp did the same walk (now stamped once per run), and the program-zero query deep-cloned the whole equipment assembly on every motion block (now a cached per-run matrix over the live assembly). These are complexity changes; they have not been separately timed.

Smaller footprint

The embedded default font is handed to the display engine from memory, so an 11 MB .ttf is no longer written into the process working directory on startup. The packages are x64-only, and HiNc-Resource no longer ships a duplicated nested copy of the CT-350 STL set.

Things that cost more, on purpose

Five-axis inverse kinematics. Tightening the orientation envelope from about 1.4e-3 rad to about 1e-6 rad — measured maxima 1.5e-8 rad on the hot path and 2.1e-8 on teleport, a tip deviation of 0.05 µm on a 50 mm tool — costs roughly eleven solver iterations where one used to do, so a posture-changing call went from about 23 µs to about 251 µs, and a teleport from 418 µs to 1607 µs. Only posture-changing RTCP and arc steps pay it: three-axis programs and constant-posture segments are exempt through the McLinear downgrade. In absolute terms a 1,432-step five-axis replay spends about 0.36 s in the solver. (Debug build including measurement overhead, so those microseconds are an upper bound.)

Machine-coordinate linear stepping. ActMcXyzLinearContour derives its step count from the euclidean length of the machine XYZ delta rather than the largest single-axis component, so LinearResolution_mm caps actual tool-tip travel per step. A diagonal move therefore produces up to √3× more steps than before at the same setting — more work, for a sampling density that now means what the setting says. Lower the resolution if the old step count was what you were budgeting for.

A tuning cliff worth knowing about

MillingCycleDivisionNum saturates. Raising it past roughly 180 buys no additional training accuracy while the cost keeps climbing: a training run that takes about four minutes at 180 takes hours at 720 and needs on the order of 100 GB of RAM to do it. The default of 36 is for ordinary simulation; raise it for training, but not past the point where the curve flattens.

Packaging, resources and hosting

  • Shipped resources carry an ownership marker. ResourceDefaultMarker introduces the .default convention: files carry the marker before the final extension (AlTiBN.default.CoatingMaterial) and machine-tool packages carry it on the folder (MachineTool/Table-B1.default/). Marked items are system territory the seeder may refresh or delete; unmarked items belong to the user and are never touched. Seed copies the shipped defaults into the admin resource root at startup — anchored on the application base directory rather than the process working directory, version-stamped so steady-state boots do no work. A save-as flow must strip the marker so a user file cannot masquerade as a shipped default.
  • HiNc-Resource ships more presets: Resource/Controller/<Brand>.default.Controller for all five brands, and StandardForcedAir / StandardWaterSolubleCoolant / StandardOilBasedCoolant coolant conditions, so those load browsers start populated instead of empty.
  • CoolantHeatCondition gains name/note, the three static presets, preset find/match/apply, and side-file IO following the workpiece-material pattern — CoolantHeatConditionFile externalizes the condition to a .CoolantHeatCondition file.
  • Obfuscation-safe DTOs. Obfuscar renames compiler-generated anonymous types and strips their constructor parameter names, so System.Text.Json threw on every ToPresentDto serialization in released assemblies — the geometry, cutter and transformer editors all returned HTTP 500 while Debug builds were fine. Every implementation now returns Dictionary<string, object> with nameof keys, which compile to string literals obfuscation cannot touch, and IToPresentDto documents the rule. See the wire-key list under Signature and shape changes.
  • The embedded default font is Noto Sans CJK TC (SIL OFL 1.1) in place of the Big5-only HanWang WCL06, fed to the native engine from memory instead of writing an 11 MB Font/WCL06.ttf into the process working directory. Traditional, simplified and kana render from one face with zh-Hant glyph conventions. Explicit font paths still go through the file route.
  • Third-party notices ship with the two packages that actually redistribute third-party bits: HiDisp (the native files under runtimes/ and the embedded font) and HiNc-Resource (the offline documentation site among its content files). HiLicense gained a real package description.
  • Dependency updatesSQLitePCLRaw.bundle_e_sqlite3 is pinned to 3.0.3 (SQLite CVE-2025-6965), Microsoft.Data.Sqlite to 10.0.9 and Dapper to 2.1.79.
  • Small public additionsDetectRoundTripEncoding, CompactNanOptions and named float literals in GetDouble, CycleUpperInclusive (the (l,u] cyclic window the existing overloads could not express), and AddAndGetIndex. IndexSegment no longer leaves an open run end when the match sits at either edge of the list.

New diagnostics you may now see

Several failure modes that used to be silent now report. These ids are searchable and filterable — they are the fastest way to find out what a run actually did.

Id Means
Play-Touch--None the play finished without any step touching the workpiece
Play-Physics--None physics is on and steps touched, but no touched step carries a physics brief
Tool-FluteCount--Zero physics is on and a milling cutter resolves to zero flutes
SpindleDirection--AssumedCw an S word greater than zero with no direction ever issued
Composition--NotLicensed external pipeline units were skipped for this session
Script--NotLicensed an NC-embedded C# script was skipped
SiemensToolOffset--TcdpRowMissing a Siemens (T, D) pair had no $TC_DP row; the generic tool height was used
Comp-ToolHeight--001 a G43.4 H word could not be resolved
Coord-MachCoord--005 / --006 / --007 a G53 or G53.1 machine-coordinate move failed on a path that used to fail silently
Coord-Tilt--001 / --002 a G68.2 tilted plane the machine cannot reach
BuildNcLines--ParseFailed a line could not be parsed; it survives verbatim but is not simulated
RunNcLines--RunnerMismatch a second runner kind was attempted inside one session
ReadNcRunnerSuit--Refused a suit switch was attempted while a program was playing
PostExecution--MeshedGeomOutputRetired an old project still carries the retired meshed-geom output pair
ConvertClToNc--NoPlay CL-to-NC conversion ran with no prior play on a machine chain
ClToMc--NoToolOffset a CL motion resolved no tool offset
Map-ShotGap--StepsSkipped steps were skipped because their shot pairing window held no measured row
Train-StepEngagement--Missing a step's row is present but its engagement was never built
Train-StepLuggage--Unreadable a step's luggage row could not be read back
Writeback-Patch--* a writeback edit refused a variable, keyword or comment-only value, or matched nothing
HeidenhainPlane--Unsupported / SiemensFrame--Unsupported / HeidenhainCycl--Unsupported the construct is recognized and consumed safely, but not simulated
DeclaredMCode--UnmodeledEffects a machine-declared OEM M-code occurred; no effects are simulated for it