What We Shipped

Changelog

Every commit. No marketing spin.

1–30 of 57 commits
1 / 2
Wire up the JetFlight USB HID protocol so JetCad3 can talk to ESP32-S3
hardware. This implements the complete host-side communication stack
without disrupting the existing simulator path used by the Plasma workspace.

Architecture:
- Protocol layer (protocol.js): Pure encode/decode for all 13 packet types
  defined in PROTOCOL.md — PKT_INIT, PKT_POS (1kHz heartbeat), PKT_ACK,
  PKT_INIT_DATA (chunked joint/IO config), PKT_IO_CMD, PKT_IO_QUERY,
  PKT_PIN_QUERY, PKT_PID_TUNE, and all firmware responses (READY, FAULT,
  WARN, IO_REPORT, PIN_REPORT). Zero I/O, fully testable.

- Hardware SharedArrayBuffer (HW_SLOTS.js): Second SAB (separate from DRO)
  bridges worker thread and main process. Worker writes step positions +
  velocities every tick; main process reads at 1kHz for heartbeat packets.
  Same proven pattern as the existing DRO SharedArrayBuffer.

- HID Transport (HIDTransport.js): node-hid wrapper with lazy loading,
  device enumeration, vendor-defined interface filtering (usage page 0xFF00,
  usage 0x01). Replaces the old empty HIDHeartbeat stub.

- JetFlightController: Full orchestrator handling device discovery, INIT
  handshake (PKT_INIT → PKT_INIT_DATA chunks → await PKT_READY), 1kHz
  heartbeat loop, incoming packet dispatch, fault handling with auto-stop,
  I/O commands, live PID tuning, and fault recovery via hot-reconnect.

- MotionExecutor integration: Surgical addition at the existing TODO —
  writes step positions and velocities to hardware SAB when connected.
  Additive only; simulator path completely untouched. Both paths can run
  simultaneously (sim visualization + hardware output).

- IPC bridge: window.api.hardware.* namespace in preload with enumerate,
  connect, disconnect, getState, sendIOCmd, queryIO, setPID, queryPins,
  recover, and push events (onFault, onWarning, onIOReport, onDisconnect).

- Machine profiles: All four profiles (plasma, mill, laser, lathe) extended
  with optional hardware section containing placeholder pin assignments,
  PID defaults, VID/PID, watchdog timeout, and estop configuration.

New files:
  src/main/JetFlight/hardware/protocol.js
  src/main/JetFlight/hardware/HIDTransport.js
  src/main/JetFlight/constants/HW_SLOTS.js

Removed:
  src/main/JetFlight/hardware/HIDHeartbeat.js (replaced by HIDTransport)
Replace the monolithic gcodeSimulator.js with a modular JetFlight system
under src/main/JetFlight/. This serves dual purpose as the JetCad3 G-code
simulator and foundation for a real hardware motion controller (ESP32-S3
over USB HID).

Architecture:
- core/ — GCodeParser, MotionPlanner, MotionExecutor, SubStepAccumulator,
  ExpressionEvaluator (stub), GCodeHandlerRegistry
- handlers/ — Self-registering G/M code handlers (G0-G4, G20/G21, G90-G92,
  M0/M2/M3/M5/M30). All dispatch goes through the handler registry — no
  parallel switch statement
- machine/ — MachineConfig validation + profiles (plasma, laser, mill, lathe)
- constants/ — STATE enum, SLOTS (16 Float64 SharedArrayBuffer layout)
- hardware/ — HIDHeartbeat and JetFlightController stubs for future ESP32

New features:
- 6-axis XYZABC support (A/B/C rotary axes in degrees, never unit-scaled)
- 16-slot SharedArrayBuffer (128 bytes): XYZABC position, feed, state,
  is_rapid, unit_mode, and per-axis XYZABC velocity
- SubStepAccumulator for float→integer step conversion with carry-forward
- ExpressionEvaluator stub for future #variable and [...] expression support
- Handler registry pattern for easy G/M code extension (10-line pattern)

All motion math (calcTrapezoidTimes, buildTrapezoid, optimizeLookahead,
planLinear, planArc, planDwell, tick, pause/resume, interpolate, distAt)
preserved exactly — only adapted for 6-axis position objects.

Worker message protocol unchanged (INIT, STREAM, STREAM_LINE, PAUSE,
RESUME, RESET, SET_CONFIG, SET_SPEED).
- Fix sketch import into plasma: check `closed` flag, filter construction entities, convert splines
- Fix chaining tolerance (use shared CLOSE_TOL) and algorithm (adjacency-graph loop tracing)
- Fix identifyParts: ray-casting PIP instead of cavc winding, centroid test point, no stale cache
- Fix RDP simplification dropping vertices on closed polyline wrap-around
- Fix CW arc merge in explodePolyline producing zero-sweep degenerate arcs
- Fix DXF export not checking `closed` flag on polylines
- Fix bridge tool: add closing point for clipper result before arc refit
- Clear `_nested` flag on stale sketch reference update so parts can be re-nested
- GCode simulator: GRBL-style two-pass look-ahead for smooth velocity through arc junctions
- GCode simulator: proper feed hold deceleration ramp and resume acceleration
- Fix trapezoid interpolation for triangular profiles (use vPeak not cruiseSpeed)
The cached lead re-validation block was checking the pierce bleed circle
only against nearby obstacles (other parts), not against the part's own
offset polyline. A stored lead whose pierce point was very close to the
part's own cut edge would pass re-validation and cause a pierce bleed
defect on the part. Matches the fix already applied to the isLeadValid
fresh-placement path.
Multi-sheet nesting browser panel:
- When auto-nesting overflows onto a new sheet, master parts stay on Sheet 1
  while their clones move to Sheet 2+. The browser panel was showing correct
  part counts (e.g. "Sheet 2 (118)") but rendering nothing because the master
  lookup only searched within that sheet's parts.
- Fixed by adding proxy master resolution: for each clone group on a sheet, if
  the master is on a different sheet it is pulled from allParts and used as the
  category header. Children shown under each master are scoped to only those
  on the current sheet, so each sheet renders its own subset correctly.

Auto-generate toolpaths before quoting:
- After multi-sheet nesting the last overflow sheet had no toolpaths generated,
  causing the Quote dialog to show $0.00 for that sheet until the user manually
  closed, clicked Generate, and reopened the dialog.
- Added _generateAllSheetsForQuote() which iterates every sheet, identifies
  parts with missing toolpaths, resolves each sheet's own cut chart, and runs
  the toolpath generator targeted to just those parts.
- _showQuoteDialog() is now async and calls _generateAllSheetsForQuote() before
  opening the dialog if any sheet has un-toolpathed parts. The generate button
  updates to show progress and completion state during the auto-generate pass.
- Fixed null onProgress crash (null bypasses default param; changed to undefined).
Rule Engine:
- New `pierce_only` action type. When enabled on a rule, matching closed
  contours are pierced at their geometric center instead of being cut.
- `computeContourCenter()` computes arc-aware bounding box centroid for
  accurate hole center detection on circles and non-circular shapes.

Toolpath Generator:
- Pierce-only contours skip kerf offset and lead generation entirely.
- Single-vertex toolpath created at contour center point.
- `_pierceOnly` flag carried through operations to toolpath items.

Post Processor:
- `PlasmaPost.onOperation()` handles `pierce_only` operations: rapid to
  center, move to pierce height, torch on, dwell, torch off, retract.
  No Z feed to cut height, no cut path emitted.

Viewport:
- Pierce-only contours render as a cross marker (x) at the pierce point
  using the rule's color, instead of a full toolpath visualization.

UI:
- "Pierce Only (center mark, no cut)" checkbox in rule edit dialog actions.

Use case: bolt holes, drain holes, center marks — any circle that needs a
pierce mark but no cut. Controlled via the rules system (e.g., "if hole
diameter < 0.25in then pierce only").
Changed dialog auto-tiling from column-first (down then right) to
row-first (right then down). New dialogs now stack horizontally to
the right of existing ones, wrapping to a new row only when
horizontal viewport space runs out.
Cut Chart Storage:
- Machine profiles now store cut charts in a `cut_charts` object keyed by
  cutter config (e.g. "powermax-45xp:cartridge"). Factory data is copied
  into the machine on first use, becoming fully editable.
- Switching cutters preserves old chart data so switching back keeps edits.
- "custom_none" key for machines with no manufacturer — users build charts
  from scratch.
- Drop `chart_overrides` entirely — edits are made directly on chart entries.

Cut Chart Browser:
- Title shows "MachineName — Manufacturer Model — Torch"
- Edit button opens dialog with editable speed, kerf, voltage, pierce,
  torch-to-work fields. Material/consumable/thickness are static (identity).
- "+" button adds new entries with free-text material, consumable set, and
  thickness. Prevents duplicates (same material+consumable+thickness).
- Delete button with confirmation dialog removes entries.
- Tree materials are dynamic from stored charts (not hardcoded).

Machine Dialog:
- "Reset Cut Charts to Factory" button in Cutter tab with confirmation
  overlay explaining user data will be lost.
- Auto-populates cut charts on OK when cutter config has no stored data.

Other:
- "Settings..." renamed to "Parameters...", Overrides tab removed from
  cut parameters dialog (no longer needed with direct chart editing).
- `resolveEffectiveCutChart` simplified — no more `machine` arg or
  chart_overrides merge.
- Browser panel Material dropdown is dynamic from stored charts.
- Confirmation dialog supports custom action button label.
Post Processors:
- Remove default-plasma.jcpst (replaced by controller-specific posts)
- Add linuxcnc-plasma.jcpst: G64 P<tolerance> path blending (configurable,
  default 0.001), G90 G40 G49 G17 preamble, .ngc extension
- Add mach3-plasma.jcpst: G64 constant velocity, % tape markers, .tap
- Add mach4-plasma.jcpst: G64 P<tolerance> (configurable), % tape markers, .tap
- All three: non-modal output, IJ incremental arcs, M3/M5 torch control,
  linearizeArcs toggle, G20/G21 units, standard pierce sequence via base class

Locked Part Fixes:
- _markToolpathsDirty skips locked parts (toolpaths preserved for nesting)
- _executePost dirty check skips locked parts (doesn't block posting)
- _rebuildToolpaths skips locked parts (no visualization for already-cut parts)
Parts with toolpaths are automatically locked after a successful post,
marking them as "already cut." Locked parts act as obstacles for the
auto-nester and are skipped by the generator, so new parts added to
the sheet will be placed around them without re-generating existing
toolpaths.
- Add Override Feedrate dialog: shows cut chart default, input in current
  units, Reset to Default, Apply to children for master parts. Writes to
  contour.feedrate_override which resolveRecipe already picks up.
- Add Override Pierce Delay dialog: same pattern, input in seconds. Writes
  to contour.pierce_delay_override. Disabled for open contours (no pierce).
- Override Lead disabled for open contours (no leads on cut-on-line).
- Open contours get empty leads in generator (no lead generation at all).
- Reverse Cut Direction now works for open contours by reversing vertex
  traversal order.
- Fix place mode stuck on open-contour parts: switched from mousedown to
  pointerdown capture phase so it fires before the interaction system's
  pointerdown handler, preventing drag race condition.
- Duplicate and Multiple Duplicate now act on all selected parts (not just
  the right-clicked part). Works from both viewport and browser panel
  context menus. Includes locked parts in selection.
- Cloned parts are always unlocked (clone.locked = false).
- Multiple Duplicate dialog shows "Quantity (each)" and total count in
  done message when duplicating multiple selected parts.
- Viewport context menu Duplicate/Multiple Duplicate match browser panel
  selection-aware behavior.
- Add persistent activity log badge to task indicator that appears when
  the indicator auto-hides but has uncleared log entries. Badge shows
  entry count and opens log panel directly when clicked (without showing
  the indicator bar). Badge disappears when log is cleared or empty.
- Add TaskIndicator.log() method for silent log entries that record to
  the activity log without showing the indicator bar.
- Lead placement messages (arc radius overrides, failed placements) now
  use silent log entries instead of error indicator, keeping the UI clean
  while still recording activity for user review.
- Show "Generation complete!" on task indicator after toolpath generation
  finishes successfully.
- Separate cut chart data (read-only manufacturer values) from cutting profiles (user preferences)
- PROFILE_DEFAULTS split from RECIPE_DEFAULTS — leads, cut direction, speed mode in profile
- Factory recipes generated on-the-fly from data files, never stored in machine config
- Factory recipes shown with lock icon in Recipe Manager, read-only, clone to customize
- Cutting profile CRUD in MachineProfileManager (getProfilesForMachine, addProfile, etc.)
- resolveEffectiveRecipe merges cut chart + profile at generate time
- Profile dropdown added to browser panel
- Recipe dropdown filters by machine's configured torch type process labels
- Metric plate size crossover table for thickness labels (1/4 → 6.0mm, etc.)
- Recipe Manager shows cutter header (e.g., "Hypertherm Powermax 45 XP — SYNC")
- Recipe Manager details in tabs: General, Cut, Pierce, THC
- Manufacturer.md specification for adding new plasma cutter data files
- Scale dialog for parts (factor, percentage, quick inch↔mm conversion)
- DXF import auto-scales when preferred units is mm (÷ 25.4)
- Part properties dialog: static text, unit-aware, weight in lb/kg based on preferred units
- Duplicate and Multiple Duplicate added to browser panel right-click menu
- Remove "Move to Origin" from viewport context menu
- Properties added to both browser and viewport context menus
- Fix post processors writing leads twice (leads already embedded in op.toolpath)
- Fix dialog:exportSave returning full dialog object instead of file path string
- Add linearizeArcs post option with configurable arc deviation (tessellates G2/G3 to G1)
- Add linearizePolyline() to base PostProcessor class for any post to use
- Add "Edit G-code" to post dropdown menu (opens posted file in monaco)
- Fix _runPost to fall back to Save As when output path is invalid/locked
- Reorganize machine edit dialog into tabs (Machine, Torch, Post, Advanced)
- Fix lead point picking: use _splitPolylineAt for exact polyline surgery instead of dense-sample snapping
- User-picked lead points bypass findLeadPoint entirely — split + generate leads directly
- Add returnToStart vertex in toolpath assembly for closed polylines
- Add dismiss button to task indicator done state, 15s auto-hide for nest summary
- Copy fixed posts to user data directory
- Update AGENTS.md with comprehensive Plasma workspace documentation
## PolylineResampler integration (toolpath-generator.js)
Replaced the crashy explode/merge dense-sampling implementation with the new
PolylineResampler class (polyline-resampler.js). The new workflow:
  1. denseSample() — subdivides arcs into equal sub-arcs carrying correct bulge,
     lines into equal-spaced vertices. Geometrically identical to original.
  2. Search loop reads directly from dense array (no per-candidate array copies),
     with PiP polygon pre-tessellated once for the entire search.
  3. Winner: shiftStart() + cleanResample() produces a minimal bulge-encoded
     polyline with the pierce point at vertices[0], plus a closing duplicate so
     the toolpath assembly (lead-in → loop → lead-out) stitches correctly.

## Arc angle cap in PolylineResampler (polyline-resampler.js)
cleanResample() now refuses to merge arc sub-segments if the combined subtended
angle would exceed maxArcAngle (default π/2 = 90°). This prevents circles and
large arcs from collapsing to 1-2 vertices with near-infinite bulge, which broke
toolpath rendering for holes. A full circle now comes back as 4 × 90° arcs.

## Lead collision detection fixes (toolpath-generator.js)
- tessellateOffsetPolylineForPiP: fixed for is_closed:true (no duplicate vertex)
  — now uses n=verts.length and (i+1)%n wrap-around, catching the missing
  last→first boundary segment that caused leads to escape without detection.
- leadCollidesWithPolyline: same wrap-around fix; removed stale "skip attachment
  segment" logic (segmentsIntersect margins handle the pierce-point touch).

## Lead point override system (main.js, part-manager.js, toolpath-generator.js)
Replaced the broken lead_point_index (vertex index, never consumed by generator)
with lead_point_xy: { x, y } (local-space float position):
  - findLeadPoint() accepts hintXY — finds nearest dense sample and rotates the
    search to start there, so the user's chosen point wins if valid or the search
    walks forward from it.
  - generate() passes contour.lead_point_xy as the hint.
  - _enterLeadPointPicker() now samples the stored offset_polyline via
    sampleOffsetPolylineForPicker() — the exact same 0.125" dense points the
    generator walks — so every clickable dot is a real candidate.
  - Stored position is local-space, survives polyline format changes and rotations.
  - part-manager.js: all contour types include lead_point_xy: null.
  - Reset Overrides clears lead_point_xy; propagate-to-children copies it.
Nesting engine:
- Fix overlap detection: use _anyVertexInside (all vertices) instead of centroid-only
  containment check, catching concave polygon interlocking that centroid missed
- Add per-vertex AABB pre-reject for fast collision filtering
- Widen segment intersection tolerance (SEG_TOL=0.001) so touching edges don't
  false-positive as collisions
- Increase time limits: medium 30s→120s, max 60s→300s
- Report elapsed time and timeout flag from both skyline and NFP workers
- Task indicator shows elapsed time on completion and "TIME LIMIT REACHED" when hit

Incremental nesting:
- Add _nested flag: set when auto-nester places a part, cleared on user move/rotate
- Nested parts treated as obstacles (like locked) in subsequent nest runs
- New "Rip Up Nest" option in auto-nest dropdown clears all _nested flags
- Nested parts render in distinct color (darker blue, between normal and locked)

Import behavior:
- Remove auto-nest on all import paths (DXF, DXF with qty, sketch, duplicate)
- New _gridLayoutParts() places imported parts in rows to the right of the sheet
- Preserves existing nest when adding new parts

Drag/rotate with locked parts:
- Multi-select drag/rotate now skips locked parts instead of blocking the operation
- Selection overlay bbox excludes locked parts so it tracks only movable parts
- Locked parts stay put while unlocked parts in the selection move freely

Other:
- Stop rendering sheet debug polygon after nesting
- Clear _colliding flag on all parts when marking toolpaths dirty
Import DXF split button with "Set Each QTY..." dialog:
- + Import DXF is now a split button matching Auto-Nest pattern
- Dropdown option "Set Each QTY..." opens file picker then shows
  a dialog listing each identified part with a QTY number input
- Live "Total parts to import" counter updates as QTY values change
- On Import: master part + duplicates created per QTY, all at (0,0)
- Duplicates linked to master via master_id field
- Part naming: filename without extension, numbered if QTY > 1
- Auto-nest runs after import if enabled

Browser panel part nesting:
- Parts grouped by master_id instead of name-based "Duplicate" check
- Masters are top-level, duplicates nest underneath with chevron toggle
- fa-clone icon for duplicates, fa-file-import for DXF, fa-drafting-compass for sketch
- Right-click "Duplicate Part" now sets master_id linking back to original
- Removed stale innerHTML-based click handler that was dead code

Auto-nester effort presets:
- EFFORT_PRESETS moved from worker to auto-nester.js (exported constant)
- Worker receives resolved preset object in message, no local lookup
- Presets tuned: quick=45°, medium=30°+holes, max=10°+holes+optimize

Auto-nester skyline fallback:
- When skyline candidate fails collision (e.g. locked part in the way),
  falls back to scanning Y upward at step increments, X left-to-right
  at each Y level — carriage return pattern
- Uses spatial hash for fast collision at each candidate position
- Ensures parts are placed if ANY valid position exists on the sheet

Locked parts in nester:
- Locked parts NOT registered in skyline (skyline can't represent
  arbitrary positions) — collision handled by spatial hash only
- Toolpath offsets generated for locked parts even on quick effort
  so collision geometry is accurate

Multi-select lock/unlock context menu:
- All locked: "Unlock (N)"
- All unlocked: "Lock (N)"
- Mixed: "Invert Locks (N)"
Browser panel:
- Generate + Post Process buttons moved to top of panel
- PARTS header uses panel-header style matching Drafting workbench
- Import DXF/From Sketch/Auto-Nest buttons below PARTS header with divider
- Part rows use Drafting tree-row pattern: tree-toggle, tree-icon, tree-label
- Icons: fa-file-import (blue) for DXF, fa-drafting-compass (pink) for sketch,
  fa-clone (gray) for duplicates
- Duplicates nest under parent part with chevron expand/collapse
- Selected parts highlighted with tree-row-selected (syncs with viewport)
- Right-click context menu on part rows with Delete option
- workspace-browser-container overflow fix

Part naming:
- DXF imports named after filename (no extension), counter if multiple parts
- Sketch imports named after sketch name
- Duplicates named "Duplicate 1", "Duplicate 2", etc.

DXF import fixes:
- Circle bulge: was 1 (180° semicircle), now tan(PI/8) ≈ 0.414 (90° quarter)
- Removed duplicate closing vertex on LWPOLYLINE and POLYLINE that was
  shifting bulge assignments and losing closing arc curvature
- Multi-file DXF import: dialog allows selecting multiple files at once

Auto-nester (new file):
- Skyline packing + spatial hash + 3-level collision testing
- Graham scan convex hull, SAT overlap, segment intersection
- Configurable rotation step, part spacing, sheet margin
- Machine profile autonest settings added
- Auto-Nest button in browser panel, _runAutoNest() in workspace

Interaction:
- Click-on-unselected-part-and-drag: selects + starts drag in one motion
- Delete/Backspace key deletes all selected parts
- onDeleteSelected callback wired to _deleteSelectedParts()
Machine coordinate system:
- mcs_origin field on machine profile (lower_left/lower_right/upper_left/upper_right)
- MCS Origin combo in machine edit dialog with torch offset X/Y fields
- Machine bed positioned relative to WCS using getMachineBedOffset()

Sheet/material origin:
- 2x2 corner dot selector in browser panel Sheet section
- getSheetOriginOffset() positions sheet so selected corner is at WCS (0,0)
- Offset X/Y inputs: always-positive distance from bed edge to sheet edge
  (sign auto-flipped per MCS home corner so positive = inward from boundary)
- Sheet settings persisted to localStorage across sessions

Camera fix:
- Camera positioned below XZ plane (y=-100) looking up
- Gives screen +X = scene +X, screen +Y = scene +Z — no X-mirroring
- Eliminates all the mirror compensation that was causing offset bugs
- All DoubleSide materials render correctly from below

Machine boundary:
- Red dashed border at travel limits
- _checkBoundaryViolations() warns when parts extend beyond bed
- Boundary tracks with sheet origin and work offset changes

Dialog improvements:
- Cancel button on machine edit and recipe edit dialogs
- +New doesn't create machine until OK is pressed
- Delete Machine button with confirm dialog (same pattern as Drafting)
- Import/Export show task indicator feedback

Browser panel fix:
- innerHTML += was destroying event listeners on machine buttons
- Replaced with createElement + appendChild for section headers

Removed auto-fit on parameter changes (only fires on workspace activate).
Toolpath generator:
- polylineToPolygon(): tessellates bulge-encoded polylines into dense
  polygon points using max-deviation tolerance (ARC_MAX_DEV=0.0001)
  matching the Drafting workbench convention
- tessellateArcByDeviation(): segment count derived from max radial
  error per chord, not fixed segment count
- samplePolygonEqually(): walks polygon edges accumulating arc-length,
  emits {x, y, nx, ny} samples at uniform spacing (0.5" default)
- Each toolpath now stores offset_polygon alongside offset_polyline
  for visualization; toolpath pipeline always preserves arcs

Scene visualization:
- Arrows are unit-size meshes (chevron head + thin shaft) built once
  at origin, positioned/rotated per sample point tangent direction
- updateToolpathArrowScale() called each frame from render loop
  scales all arrows for constant screen size (~10px)
- Max world-size clamp (0.15"): when zoomed out far enough that
  constant-screen-size would make arrows huge, they cap at 0.15"
  and shrink with the scene like normal geometry
Shaft 0.02" (was 0.08"), head 0.02" (was 0.04"), total ~0.04".
Shaft width 0.004", head half-width 0.012".
Lead perpendicular sign was inverted due to camera Y-flip:
- Camera up=(0,0,-1) means doc Y appears inverted on screen
- Doc LEFT appears as screen RIGHT and vice versa
- Flipped sign: outside cuts now use (-ny, nx) in doc space,
  inside cuts use (ny, -nx) — both point toward waste on screen

Arrow shape rewrite:
- Classic arrow: long thin shaft (0.08") + wide chevron head (0.04")
- Total length ~0.12" — visible at sheet-level zoom (was 0.05")
- Shaft width 0.008", head half-width 0.025"
- 3 triangles: head chevron + shaft quad
- Same green color, same look-ahead tangent for curve alignment
Lead-in/out now uses isOutside parameter to flip perpendicular:
- Outside cuts (CCW): waste = RIGHT of travel, perp = (ny, -nx)
- Inside cuts (CW): waste = LEFT of travel, perp = (-ny, nx)
- Arc bulge sign also flips for correct arc sweep direction
- Line lead-in/out use same perpX/perpY for consistent side

Arrow improvements:
- Use look-ahead sampling (3+ points ahead) for smoother tangent
  on curves instead of single next-point which was noisy
- Skip degenerate zero-length tangent vectors
- Slightly smaller arrows (headLen/tailLen 0.025, headW 0.015)
Lead-in/out perpendicular fix:
- All leads now use RIGHT of travel direction (ny, -nx) = waste side
- Arc lead-in and lead-out had perpendicular pointing LEFT (-ny, nx)
  which placed leads on the material side instead of the scrap side
- Lead-out line also had opposite perpendicular from lead-in line

Toolpath visualization:
- Lead-in color: blue (0x4488ff), lead-out color: red (0xff4444)
- Removed pierce point yellow dot mesh
- Direction arrows: half the size, twice as many (interval /12 vs /6)
- Arrow shape changed from equilateral triangle to arrow-with-tail
  (pointed head + narrow rectangular tail) so direction is unambiguous
  even at curves where triangles could be interpreted multiple ways

Browser panel:
- Visibility toggle now uses Font Awesome eye/eye-slash icons matching
  the Drafting workspace browser panel style
Selection system rewrite:
- Multi-select via selectedPartIds Set (was single selectedPartId)
- Shift+click toggles parts in/out of selection
- Drag box selection: left→right = window (fully contained),
  right→left = crossing (overlapping), matching Drafting workspace
- Escape key clears all selection
- Combined AABB overlay with rotation handles for multi-select
- Drag-move applies to all selected parts simultaneously

Rotation system (Manipulate tool pattern):
- During drag: bbox + handles rotate as rigid body via createRotatingOverlay()
  with captured start-bbox dimensions, handles always follow the mouse
- On release: _bakePartRotation() writes rotation into polyline vertices,
  resets part.rotation to 0, recomputes axis-aligned bounding box
- Multi-select rotation orbits all parts around combined AABB center,
  each part's position orbits AND local rotation increments, all baked on release
- Context menu 90° rotations also bake immediately

Fill mesh / contour line alignment fix:
- ShapeGeometry.rotateX(-PI/2) maps shape (x,y) → scene (x, 0, -y) but
  polylineToPoints3D maps doc (x,y) → scene (x, 0, y) — these were mirrored
- Fixed by negating Y in polylineToShape/polylineToPath so both produce (x, 0, y)
- Previously invisible on symmetric shapes, broke visibly when rotation was baked

Winding direction normalization at import:
- identifyParts() now forces outers to CCW, holes to CW via reverseVertices()
- reverseVertices() correctly reassigns bulge values for arc segments
- Winding field on contours is now deterministic: 'CCW' for outer, 'CW' for hole

Kerf offset simplification:
- Toolpath generator always uses same offset sign: -(kerfWidth/2)
- CCW outer + negative offset = expand outward + climb cutting
- CW hole + negative offset = shrink inward + climb cutting
- No runtime winding check needed — normalized at import time
- Future overrides: negate sign for conventional cut, reverse winding to flip side

Interaction refactor:
- Removed click event, unified into pointerdown/move/up with 5px drag threshold
- Pointer-down records target (handle/selectedFill/fill/empty), resolves into
  drag-move, rotation, or box-select after threshold exceeded
- Hover cursor adapts to multi-select state
- Add activeWorkspace field to config (defaults to Drafting)
- Load saved workspace on startup instead of hardcoded Drafting
- Save workspace choice to config when user switches via dropdown
- Update dropdown label on startup to reflect restored workspace
Complete 2D plasma cutting CAM workspace:
- Machine profile system (.jcm files) with Hypertherm 45XP defaults
- DXF import pipeline with part identification via containment depth
- Sketch import with picker dialog
- Toolpath generation: cut ordering, Calvc kerf offset, arc-preserving leads
- Generate button with integrated progress bar, stub post processor
- Browser panel with Machine/Sheet/Parts/Toolpaths tree UI
- Machine and recipe edit dialogs using existing dialog system
- Top-down orthographic scene with machine bed, sheet, part, and toolpath viz
- plasma_cam property on Document for project persistence
- IPC handlers for profile load/save/import/export
- Fix closed polyline arc rendering (last vertex bulge wrapping to first)
Implements a virtual GCode machine simulator for all CAM workbenches
(Plasma, Laser, Mill, Turn). This is a kinematic simulator that parses
gcode, plans motion with trapezoidal acceleration profiles, and tracks
XYZ position over simulated time — not a firmware port.

Architecture:
- Worker thread (src/main/gcodeSimulator.js) runs in a separate thread
  with three internal modules:
  - GCodeParser: line-by-line parsing with modal state (G0-G3, G4, G20/21,
    G90/91, G92, M0/M2/M3/M5/M30), comment stripping, unit scaling
  - MotionPlanner: trapezoidal velocity profiles with GRBL-style junction
    deviation look-ahead, arc linearization (G2/G3) at 0.002mm tolerance,
    configurable per-machine accel/feedrate/junction params
  - MotionExecutor: 10ms tick interval, interpolates position along
    trapezoidal profiles, supports speed multiplier (0=instant to Nx)

- SimulatorManager (src/main/simulatorManager.js) wraps the worker with
  a clean API. Lazy singleton — only created on first simulator IPC call.
  Machine configs for plasma (1000mm/s², 15k mm/min), laser (3000, 20k),
  mill (500, 5k), and lathe (300, 3k).

- SharedArrayBuffer (6x Float64) for zero-overhead position reads between
  worker and main thread: [x, y, z, feed, state_flags, isRapid]

- IPC bridge: 9 handlers in main process (simulator:*), matching preload
  contextBridge namespace (window.api.simulator.*)

- DRO sphere (src/renderer/src/workspaces/common/dro-sphere.js):
  - Green sphere indicator that maintains constant screen size regardless
    of camera zoom (scales based on ortho frustum or perspective FOV)
  - Toolpath trail: solid blue LineSegments for feed moves (G1/G2/G3),
    dashed grey LineSegments for rapid moves (G0)
  - Lazily initializes into the active workspace's scene when toggled
  - Polls position at 60fps via requestAnimationFrame + IPC

- Terminal commands (single `sim` command with subcommands):
  sim status  — state, position, feedrate, machine type
  sim load    — file picker for .nc/.gcode/.ngc/.tap files
  sim line    — send single gcode line (e.g. sim line G0 X10 Y20)
  sim run     — start/resume execution
  sim pause   — feedhold
  sim reset   — reset simulator and clear toolpath trail
  sim machine — switch config (plasma|laser|mill|lathe)
  sim dro     — toggle DRO sphere + trail visibility
  sim speed   — set multiplier (0=instant, 1=realtime, 10=10x)

- electron.vite.config.mjs: added gcodeSimulator as separate rollup
  entry so the worker thread builds as its own file (out/main/gcodeSimulator.js)
Split MillTurn into separate Mill and Turn workspaces, add Laser workspace.
Workspace dropdown now filters by subscription tier (drafting, drafting_cam_jet,
drafting_cam) via feature flags.
Two-phase splash: BMP shown during 7-zip extraction, Electron splash window
shown during app startup with progress updates. Version bump to 1.0.0.
1–30 of 57 commits
1 / 2