BREP_render 0.4.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
//! [`EngineState`] — the windowing-agnostic viewer state machine the host UI
//! programs against (R3): scene + camera + controls + settings + emphasis, plus
//! the whole event/command/query surface (run-history feed, pointer/wheel
//! ingestion, camera commands, picking, world→screen, visibility). No GPU, no
//! canvas — the wasm `Engine` and the winit desktop shell both wrap this; it is
//! fully unit-testable on native.
//!
//! Everything crosses the R3 boundary as plain JSON/scalars: the host never holds a
//! renderer object, only names, ids, and JSON.

use crate::controls::ArcballControls;
use crate::history::History;
use crate::pick::{self, PickOptions};
use crate::scene::RenderScene;
use crate::style::{Emphasis, RenderSettings};
use crate::view::ViewCamera;
use crate::widgets::{gizmo_camera, WidgetOverlay, WidgetRegistry};
use brep_kernel::HistoryRequest;
use std::collections::HashMap;

/// One DIALOG row's hover state — which panel lit it, the row's own TEXT, and
/// what that text resolved to in the scene.
///
/// The row text is the memo key rather than the candidate's name because the two
/// differ whenever the derived-face fallback fires (a fillet's `Edges` row reads
/// `A|B[0]` while the face it lights is `F1:BLEND:A|B[0]` — see
/// [`EngineState::hover_entity_by_name`]). An unresolvable row memoizes as
/// `candidate: None`, so a held hover over a name the scene does not carry scans
/// the scene ONCE, not once per frame.
#[derive(Clone)]
pub(crate) struct DialogHover {
    /// Which panel set it (`"history"`, `"constraints"`, `"pmi"`, `"refsel"`) —
    /// [`EngineState::dialog_hover_end`] acts only for the owner that set it, so a
    /// pane that never hovered a row cannot end another pane's hover.
    pub(crate) owner: &'static str,
    /// The row's TEXT, exactly as the dialog listed it.
    pub(crate) row: String,
    /// What it resolved to, or `None` for a row the scene does not carry.
    pub(crate) candidate: Option<pick::PickCandidate>,
}

pub struct EngineState {
    pub scene: RenderScene,
    pub camera: ViewCamera,
    pub controls: ArcballControls,
    pub settings: RenderSettings,
    pub emphasis: Emphasis,
    /// In-scene overlay widgets: datums/dimensions/curves, transform
    /// gizmo, ViewCube — fed as JSON, drawn by the render core's overlay pass.
    pub widgets: WidgetRegistry,
    /// The engine-owned editable model recipe (ordered features + rollback
    /// index) — the SINGLE source of truth for the model. The UI never keeps its
    /// own copy; it mutates/reads this through the `history_*` / feature methods.
    pub history: History,
    /// The build report (`{featureErrors, unresolved, displayErrors}`) of the
    /// last history run, so the UI can show it without re-running.
    history_report: String,
    /// The Properties-panel metadata store: user attributes keyed by OBJECT NAME
    /// (solid / face / edge kernel name), NOT feature id — so a record survives
    /// feature edits as long as the object's name persists. Persisted with the
    /// model (a top-level `metadata` field in the history document); see the
    /// [`crate::metadata`] module for the store + the object-info/measurement API.
    pub metadata: crate::metadata::MetadataStore,
    /// Bumped whenever settings change so the renderer re-derives per-solid
    /// base styles (a cheap key, not a per-frame diff).
    pub settings_generation: u64,
    /// The engine sets this whenever the camera/scene/emphasis changed; the
    /// presentation shell renders only when it is set (R22 on-demand render —
    /// the OrthoCameraIdle matrix-compare analogue, made explicit).
    pub dirty: bool,
    /// The modal reference-selection state (the ref-select widget). `Some` while
    /// the user is picking references for a feature-dialog field; `None`
    /// otherwise. The picked-name list here is the SINGLE source of truth while
    /// active (the UI reads it back; the viewport appends to it on a pick).
    pub ref_select: Option<RefSelectState>,
    /// Which entity KINDS a plain viewport click may select (the selection
    /// filter, mirroring the earlier `SelectionFilter.allowedSelectionTypes`). `select_top_at`
    /// consults it via `pick_filtered`; see the appended `SelectionFilter` impl
    /// block near the end of this file for the state + honoring logic.
    pub selection_filter: SelectionFilter,
    /// The transform-controls gizmo controller: which feature (if any) has the
    /// move/rotate gizmo armed (via the in-viewport center-sphere toggle), plus the
    /// in-flight handle drag. All the arm/drag/apply logic lives in the appended
    /// transform-gizmo impl block near the end of this file.
    pub transform_gizmo: TransformArm,
    /// The COMPONENT Move gizmo controller (assemblies §8.5): which ACOMP
    /// instance has the bbox-center move/rotate gizmo armed, the translate/
    /// rotate cycle mode, and the commit-on-release drag. Exclusive with
    /// `transform_gizmo` (shared widget slot). See `component_move.rs`.
    pub component_move: ComponentMoveArm,
    /// The active engine-native sketch edit (`Some` while in sketch mode). Holds
    /// the live [`crate::sketch::SketchSession`] plus the pre-entry camera + roll
    /// to restore on exit. All the enter/exit/new logic lives in the appended
    /// sketch-mode impl block at the END of this file.
    sketch_edit: Option<SketchEdit>,
    /// Sketch-mode camera lock. When true (the default on every sketch entry), the
    /// camera is held flat-on to the sketch plane and an empty drag only PANS —
    /// no orbit. Toggling it back on re-faces the camera to the plane. Meaningful
    /// only while `sketch_edit` is `Some`. See the sketch-mode impl block.
    sketch_camera_locked: bool,
    /// Set for ONE frame when the sketch entity-LIST panel hovers a row (it calls
    /// [`sketch_hover_entity`](Self::sketch_hover_entity)). The viewport, which
    /// draws AFTER the panel and would otherwise `sketch_clear_hover` because the
    /// pointer is off the viewport, consumes this flag via
    /// [`take_sketch_list_hover`](Self::take_sketch_list_hover) and keeps the
    /// panel-set hover so list→canvas highlight survives the frame.
    sketch_list_hover_active: bool,
    /// Transient user-facing notices (e.g. a sketch solve that failed after an
    /// edit). The shell drains them each frame via [`take_notices`](Self::take_notices)
    /// into a toast overlay; the engine only queues. Replaces the swallowed
    /// `eprintln!` on the interactive re-solve paths.
    notices: Vec<String>,
    /// Committed-sketch visibility: the feature ids whose persistent committed-sketch
    /// overlay is HIDDEN (its Scene-tree checkbox off). Absent = visible. See the
    /// committed-sketch impl block appended at the END of this file.
    hidden_sketches: std::collections::HashSet<String>,
    /// The committed-sketch feature ids whose overlay groups were fed on the LAST
    /// [`refresh_committed_sketches`](Self::refresh_committed_sketches), so an id that
    /// is no longer shown (rolled back, deleted, hidden, or became the active edit)
    /// can have its now-stale groups cleared.
    shown_sketch_ids: Vec<String>,
    /// The named plane FRAMES the last history run resolved `(frame name, frame)`.
    /// Stored so [`refresh_construction_datums`](Self::refresh_construction_datums)
    /// (and datum selection) can display/pick the construction datum/plane frames
    /// without re-running. Filled from the run's
    /// [`crate::pipeline::SceneBuildReport::frames`]; the D/P filter is applied at
    /// display time (a frame name maps to its producing feature TYPE via the
    /// history). See the appended construction-datum impl block at the END.
    construction_frames: Vec<(String, brep_kernel::Frame)>,
    /// The solved sketch PROFILES the last history run produced `(sketch id,
    /// profile)`. Stored so [`refresh_committed_sketches`](Self::refresh_committed_sketches)
    /// can synthesize each committed sketch's SHEET SOLID (planar face + named
    /// boundary edges + corner vertices) without re-running. Filled from the run's
    /// [`crate::pipeline::SceneBuildReport::profiles`]; rollback / active-edit /
    /// visibility gating is applied at display time.
    sketch_profiles: Vec<(String, brep_kernel::SketchProfile)>,
    /// The named PATH chains the last history run produced `(path name, curves)`.
    /// Stored alongside [`sketch_profiles`](Self::sketch_profiles) so
    /// [`refresh_committed_sketches`](Self::refresh_committed_sketches) can draw the
    /// sketch geometry no closed profile covers: a sketch's OPEN chain publishes no
    /// profile, so the sheet builder had nothing to draw it from and an open sketch
    /// was invisible in 3D. Filled from the run's
    /// [`crate::pipeline::SceneBuildReport::paths`]; the per-segment `{id}:G{gid}`
    /// entries are the display input (the whole-chain `{id}` entry duplicates them
    /// and `{id}:REF:{source}` is projected reference geometry).
    sketch_paths: Vec<(String, Vec<brep_kernel::NurbsCurve>)>,
    /// The named world POINTS the last history run produced `(point name, point)`.
    /// Stored alongside [`sketch_paths`](Self::sketch_paths) so
    /// [`refresh_committed_sketches`](Self::refresh_committed_sketches) can draw the
    /// sketch points no segment covers: a points-only sketch (a hole-placement
    /// sketch) publishes no profile and no path, so it had nothing to draw from
    /// and was invisible in 3D. Filled from the run's
    /// [`crate::pipeline::SceneBuildReport::points`]; the per-point `{id}:P{pid}`
    /// entries are the display input (construction points are skipped at draw
    /// time, like construction geometry).
    sketch_points: Vec<(String, brep_kernel::ScenePoint)>,
    /// The named axis LINES the last history run produced `(axis name, line)`.
    /// Stored so the feature-dimension angle gizmo can resolve a revolve `axis`
    /// reference to a world line without re-running. Filled from the run's
    /// [`crate::pipeline::SceneBuildReport::axes`].
    sketch_axes: Vec<(String, brep_kernel::Axis)>,
    /// The wire-harness routing report of the last APPLIED run (`None` before
    /// the first run). Filled from [`crate::pipeline::SceneBuildReport::wire_harness`];
    /// the harness panel reads it, the committed-curve display reads the port
    /// kinds off it. See the appended wire-harness impl block.
    wire_harness_report: Option<brep_kernel::WireHarnessReport>,
    /// The PMI tail's report of the last APPLIED run (`pmi_ops.rs`).
    pub(crate) pmi_report: Option<brep_kernel::PmiReport>,
    /// The ACTIVE PMI view (engine memory, never persisted): its camera,
    /// display state and explode poses are applied; its annotations drawn.
    pub(crate) pmi_active_view: Option<String>,
    /// The annotation whose form the PMI panel shows (engine memory).
    pub(crate) pmi_open_annotation: Option<String>,
    /// The modeling camera / visibility / wireframe remembered on entering
    /// the PMI workbench, restored when a view deactivates or the workbench
    /// is left.
    pub(crate) pmi_modeling: Option<pmi_ops::PmiModelingSnapshot>,
    /// The un-exploded displays of the solids the active view's explode
    /// annotations posed, for an exact restore.
    pub(crate) pmi_explode_originals: std::collections::HashMap<String, crate::scene::SolidDisplay>,
    /// `(world_per_pixel, view direction)` the PMI overlay was last baked at.
    pub(crate) pmi_overlay_key: Option<(f64, [f64; 3])>,
    pub(crate) pmi_hovered: Option<String>,
    pub(crate) pmi_label_hover_active: bool,
    /// What the spline-anchor cage overlay was last fed for — `(spline id,
    /// selected anchor, applied run generation)` — so the per-frame refresh
    /// the history panel makes while an SP form is open is a no-op until one
    /// of those moves. See `spline_edit.rs`.
    spline_overlay_key: Option<(String, Option<usize>, u64)>,
    /// Whether the ports placed components carry (`ACOMP1:PORT1`) draw their
    /// sheets. The app sets it from the active workbench — one that shows the
    /// Wire Harness panel; a document's own PORT features always draw.
    component_ports_visible: bool,
    /// The spline whose anchor editor is open (fed by the history panel with
    /// the cage overlay), so a viewport click on one of its anchor dots selects
    /// that anchor; the index of the last such pick, until the panel takes it.
    spline_edit_feature: Option<String>,
    spline_anchor_picked: Option<usize>,
    /// Construction datum/plane visibility: the frame NAMES whose datum plane is
    /// HIDDEN (its Scene-tree checkbox off). Absent = visible. Mirrors
    /// [`hidden_sketches`].
    hidden_datums: std::collections::HashSet<String>,
    /// The datum frame NAMES fed to the widget on the LAST
    /// [`refresh_construction_datums`](Self::refresh_construction_datums). The datum
    /// feed REPLACES its set wholesale each call, so this is a bookkeeping mirror of
    /// what is currently shown (parallels [`shown_sketch_ids`]).
    shown_datum_names: Vec<String>,
    /// The history-run seam (M2a of the off-thread runner). Owns the scene-free
    /// runner + its delta baseline (`name → last-emitted handle`) ACROSS reruns:
    /// [`rerun_history`](Self::rerun_history) SUBMITS a run tagged with
    /// [`run_generation`](Self::run_generation), and [`pump`](Self::pump) drains
    /// the completed reply and [`apply_run_output`](Self::apply_run_output)s its
    /// delta to [`Self::scene`]. The [`InlineRunner`](crate::runner::InlineRunner)
    /// default runs synchronously (submit → immediate poll, byte-identical to the
    /// old in-place reconcile); a background thread/worker impl slots in behind the
    /// same trait in M2b/M3. Reset on a document switch
    /// ([`set_history_json`](Self::set_history_json)) so a new model rebuilds fully.
    pub(crate) runner: Box<dyn crate::runner::HistoryRunner>,
    /// Monotonic run counter: bumped each time [`rerun_history`](Self::rerun_history)
    /// SUBMITS a run, and stamped onto the reply so a stale reply (a newer run that
    /// finished first) can be dropped. `run_generation != applied_generation` means
    /// a run is in flight (always equal for the synchronous Inline runner).
    run_generation: u64,
    /// The generation of the last reply [`pump`](Self::pump) APPLIED — the high-water
    /// mark that gates stale replies.
    applied_generation: u64,
    /// The latest [`crate::runner::RunProgress`] of the run in flight (a
    /// background runner posts one before each feature it executes), or
    /// `None` when nothing is running. Names what the spinner is waiting on.
    run_progress: Option<crate::runner::RunProgress>,
    /// The feature the last CANCELLED run was executing (`Some("")` when the
    /// run was cancelled before any progress arrived), cleared by the next
    /// submit. The history header shows it until the model is rebuilt.
    cancelled_run: Option<String>,
    /// A pending one-shot "frame the scene once the in-flight run lands" request.
    /// Import / Open SUBMIT an async run (native [`ThreadRunner`], wasm worker) and
    /// want to `zoom_to_fit` the RESULT — but the scene is still empty when they
    /// return, so an immediate fit frames nothing (bbox empty → no-op). Instead they
    /// set this flag and [`pump`](Self::pump) performs the fit on the first apply that
    /// leaves the run no longer pending. Under the synchronous Inline runner the run
    /// applies inside the submitting call's own `pump`, so the fit is still immediate.
    pending_fit: bool,
    /// EAGER provenance `name → creating-feature id` for the current resident
    /// solids, shipped with each run ([`crate::pipeline::RunOutput::provenance`]) and
    /// replaced wholesale in [`apply_run_output`](Self::apply_run_output). Answers
    /// `creating_feature` + the Info tab's `creatingFeature` WITHOUT a cold
    /// `execute_history` — the freeze side-door once the run lives off-thread.
    pub(crate) provenance: std::collections::HashMap<String, String>,
    /// EAGER ENTITY ORIGIN `face/edge NAME → ORIGINATING feature id` (FIRST writer in
    /// timeline order), shipped with each run
    /// ([`crate::pipeline::RunOutput::entity_origin`]) and replaced wholesale in
    /// [`apply_run_output`](Self::apply_run_output). Unlike `provenance` (the SOLID's
    /// LAST writer) this is the feature that gave the face/edge its NAME — the answer
    /// `creating_feature` returns for a face/edge (the "Edit owning feature" action +
    /// the Info tab's `creatingFeature`), so it rolls to the entity's origin, not the
    /// owning solid's last producer.
    pub(crate) entity_origin: std::collections::HashMap<String, String>,
    /// Object-info MEASUREMENT cache `name → merged object-info JSON`, filled by
    /// [`pump_queries`](crate::metadata) from the runner's replies and served every
    /// frame a selection persists (so `object_info_json` fires ONE query per
    /// selection, not one per frame). Invalidated on any geometry change
    /// (`apply_run_output`) or metadata edit (`set_metadata_attribute`).
    pub(crate) info_cache: std::collections::HashMap<String, String>,
    /// In-flight measurement queries `id → object NAME` — the key needed to MERGE a
    /// reply back into the info cache (inject `name` + `creatingFeature`, the latter
    /// now resolved by the entity name itself). Cleared alongside `info_cache` on a
    /// geometry change so a stale reply is dropped rather than caching a superseded
    /// measurement.
    pub(crate) pending_query: std::collections::HashMap<u64, String>,
    /// Monotonic measurement-query id (pairs a [`crate::runner::MeasureReply`] back
    /// with its `pending_query` entry).
    pub(crate) next_query_id: u64,
    /// Off-thread mesh reconstructions awaiting a runner reply.
    pub(crate) pending_mesh_imports: std::collections::HashMap<u64, MeshImportDestination>,
    pub(crate) mesh_preview_results: std::collections::VecDeque<crate::runner::MeshImportReply>,
    /// Monotonic id for pairing mesh import replies with submissions.
    pub(crate) next_mesh_import_id: u64,
    /// The scene's assembly COMPONENT records (deterministic id order), a
    /// PROJECTION captured by the main-side assembly sync (`assembly_ops`) —
    /// the Assembly Structure tree's source. Empty for componentless documents.
    pub(crate) assembly_components: Vec<brep_kernel::ComponentRecord>,
    /// The [`Self::applied_generation`] the last assembly sync ran against
    /// (`None` = never synced). `ensure_assembly_synced` re-syncs when a newer
    /// display run has been applied. See `assembly_ops`.
    pub(crate) assembly_synced_generation: Option<u64>,
    /// Re-entrancy guard for the parts-library resync in [`Self::pump`] (a
    /// refused run re-runs, and `rerun_history` pumps).
    pub(crate) library_resync: bool,
    /// The kernel parts-library revision the document's `partsLibrary` block
    /// was last refreshed from (`sync_assembly`). Serializing the store is
    /// proportional to the embedded part payload, so it is re-read only when
    /// the library actually changed, not once per edit.
    pub(crate) parts_library_block_revision: Option<u64>,
    /// The assembly-constraint viewport overlays (build-spec §8.4) — the cached
    /// [`crate::constraint_overlays::ConstraintOverlay`] records the engine last
    /// built from the kernel session (`assembly_overlay_json` + state), the source
    /// of the drawn leader/arrow group, the label feed, and the grabbable-handle
    /// hit regions. Refreshed after every history apply and every constraint
    /// mutation; see the appended assembly-overlay impl block.
    pub(crate) constraint_overlays: Vec<crate::constraint_overlays::ConstraintOverlay>,
    /// A live GRAB on a constraint's distance-arrow / angle-arc handle (`Some`
    /// between `constraint_drag_begin` and `constraint_drag_release`): the drag
    /// previews the value locally and COMMITS on release via
    /// `assembly_update_constraint_json` (which auto-solves).
    pub(crate) constraint_drag: Option<ConstraintDrag>,
    /// The `world_per_pixel` the constraint overlay buffers were last baked at
    /// (screen-constant arc/rod sizing): `ensure_constraint_overlay_current`
    /// re-bakes when the camera zoom moves it materially. `0.0` = never baked.
    constraint_overlay_wpp: f64,
    /// The `world_per_pixel` the FEATURE-DIMENSION gizmo group
    /// (`feature-dim-leaders`) was last baked at. The leaders' rod/cone/origin
    /// sphere — and the angular arc's whole world RADIUS — are
    /// `pixels × world_per_pixel`, so a zoom makes the drawn gizmo stale;
    /// `ensure_feature_dimension_overlay_current` re-bakes on a material move.
    /// `0.0` = nothing baked (gizmo disarmed / not in dimension mode).
    /// See `feature_dims`.
    pub(crate) feature_dim_overlay_wpp: f64,
    /// The `world_per_pixel` the live SKETCH overlay groups (geometry, points,
    /// preview, `sketch-dim-leaders`, `sketch-constraint-glyphs`) were last baked
    /// at — construction dashes, dimension arrowheads and constraint glyphs are
    /// all screen-constant. `ensure_sketch_overlay_current` re-bakes on a material
    /// zoom. `0.0` = nothing baked (not in sketch mode). See `sketch_input`.
    pub(crate) sketch_overlay_wpp: f64,
    /// The constraint id whose referenced ELEMENTS the panel/label hover is
    /// currently highlighting (dedupe key so a held hover doesn't re-bump the
    /// emphasis generation every frame).
    constraint_hovered: Option<String>,
    /// Set for ONE frame when a constraint LABEL hover applied the element
    /// highlight; the viewport's modeling hover branch consumes it (mirrors
    /// [`Self::take_sketch_list_hover`]) so the scene hover doesn't clobber it.
    constraint_label_hover_active: bool,
    /// The SCENE-TREE row the pointer is over, as the pick candidate its hover
    /// lit (the row→viewport highlight). Kept so
    /// [`scene_tree_hover_end`](Self::scene_tree_hover_end) clears ONLY a hover
    /// the tree itself set — never one the viewport lit in the meantime — and as
    /// the dedupe key that keeps a held row hover from re-bumping the emphasis
    /// generation every frame. See `scene_query.rs`.
    scene_tree_hovered: Option<pick::PickCandidate>,
    /// Set for ONE frame while a Scene-tree row is hovered; the viewport's
    /// modeling hover branch consumes it via
    /// [`take_scene_tree_hover`](Self::take_scene_tree_hover) and skips the
    /// pointer-off-viewport `clear_hover` (mirrors
    /// [`Self::take_sketch_list_hover`]) so the row-set highlight survives.
    scene_tree_hover_active: bool,
    /// The DIALOG row the pointer is over — a feature form's reference line, its
    /// read-only `Outputs` line, or the reference picker card's picked-name line
    /// (the row→viewport highlight, the twin of the Scene tree's above). Its OWN
    /// slot, not the tree's: History, Constraints, PMI and the Scene tree can all
    /// be on screen at once in a split dock, and a shared slot would let the pane
    /// that draws second end the hover the pane that drew first had just set. See
    /// [`DialogHover`] for what it memoizes and `scene_query.rs` for the API.
    dialog_hovered: Option<DialogHover>,
    /// Set for ONE frame while a dialog row is hovered; the viewport's modeling
    /// hover branch consumes it via [`take_dialog_hover`](Self::take_dialog_hover)
    /// and skips the pointer-off-viewport `clear_hover` (mirrors
    /// [`Self::take_scene_tree_hover`]) so the row-set highlight survives.
    dialog_hover_active: bool,
    /// The constraint SELECTED via its viewport label chip (`Some` = selected):
    /// drives the chip's selected accent, the context bar's Delete-constraint
    /// action, and clears with the selection (Esc / Clear / delete). Read
    /// through [`Self::selected_constraint`], which prunes a stale id.
    selected_constraint: Option<String>,
    /// The STEP product structure a [`Self::probe_step_assembly`] parsed, held
    /// until the import dialog's button decides its fate: consumed by
    /// [`Self::import_probed_step_assembly`], dropped by
    /// [`Self::discard_probed_step_assembly`] (Cancel), by the next probe, or by
    /// a document switch. The ONE parse of a structured STEP import lives here —
    /// the dialog needs the counts BEFORE the user chooses, and re-parsing
    /// multi-MB Part-21 text on the way back would double the most expensive step
    /// of the import. See `model_io`'s import block.
    pub(crate) pending_step_assembly: Option<brep_kernel::StepAssembly>,
    /// STEP probes submitted to the runner and not yet answered (their ids).
    pub(crate) pending_step_probes: std::collections::HashSet<u64>,
    /// Answered probes awaiting [`Self::take_step_probe`], oldest first.
    pub(crate) step_probe_results: std::collections::VecDeque<(u64, StepProbeOutcome)>,
    /// Monotonic id pairing a probe reply with its submission.
    pub(crate) next_step_probe_id: u64,
}

/// What a STEP probe found, as the file panel consumes it (see
/// [`EngineState::submit_step_probe`]).
#[derive(Debug, Clone, PartialEq)]
pub enum StepProbeOutcome {
    /// Product structure: the parsed assembly is stashed for
    /// [`EngineState::import_probed_step_assembly`]; these are its counts.
    Structure(model_io::StepAssemblyProbe),
    /// No structure worth keeping — take the flat lane.
    Flat,
    /// The text did not parse as Part 21; the flat lane refuses it with its
    /// own wording.
    Failed(String),
}

/// A live constraint-handle drag: which constraint + which `inputParams` field
/// (`distance` / `angle`), the params snapshot the commit mutates, and the live
/// preview value the overlay/label show while dragging.
#[derive(Debug, Clone)]
pub struct ConstraintDrag {
    pub id: String,
    pub field: &'static str,
    pub params: serde_json::Value,
    pub preview: f64,
}

/// WHO a finished reference-selection commits to: a history FEATURE's params
/// (the original widget) or an ASSEMBLY CONSTRAINT's `inputParams` (same modal,
/// different commit lane — see `assembly_ops::begin_ref_select_for_constraint`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RefSelectTarget {
    #[default]
    Feature,
    AssemblyConstraint,
    /// ONE SPLINE ANCHOR's port attachment: `path` is unused, the picked name
    /// must be a PORT feature id (a click on a port's drawn sheet), and Finish
    /// writes `persistentData.spline.points[index].attachment` (see
    /// `spline_edit.rs`).
    SplineAnchor { index: usize },
    /// A PMI annotation's reference field: `feature_id` is the annotation id;
    /// Finish writes the picked names into its params through the PMI block
    /// (`pmi_ops.rs`). Vertex picks are `{solid}@x,y,z` in WORLD coordinates.
    Pmi,
}

/// The modal state of the reference-selection widget while it is ACTIVE: which
/// feature-dialog field is being filled, what it accepts, and the running list
/// of picked kernel names (the source of truth — added by picking in the view,
/// removed via the per-line X). See the ref-select methods below.
#[derive(Debug, Clone, Default)]
pub struct RefSelectState {
    /// The id of the feature whose param is being edited.
    pub feature_id: String,
    /// The JSON path into that feature's `inputParams` the names write to
    /// (`["targetSolid"]`, `["boolean","targets"]`, `["faceRef"]`, …).
    pub path: Vec<String>,
    /// A human label for the modal heading (the field's label).
    pub label: String,
    /// The allowed pick kinds (`["SOLID"]`, `["FACE"]`, …) — the type constraint.
    pub filter: Vec<String>,
    /// Whether the field takes a LIST of references (else a single one).
    pub multiple: bool,
    /// The picked kernel names — the running selection (source of truth).
    pub names: Vec<String>,
    /// The rollback step to restore on Finish/Cancel (the edited feature's own
    /// step): entering the mode rolls to the pre-feature "before" state, so we
    /// remember where to return.
    pub restore_index: usize,
    /// Which surface Finish commits the names to (feature params vs an
    /// assembly constraint's `inputParams`). Defaults to [`RefSelectTarget::Feature`].
    pub target: RefSelectTarget,
}

impl Default for EngineState {
    fn default() -> Self {
        Self {
            scene: RenderScene::new(),
            camera: ViewCamera::default(),
            controls: ArcballControls::new(),
            settings: RenderSettings::default(),
            emphasis: Emphasis::default(),
            widgets: WidgetRegistry::new(),
            history: History::default(),
            history_report: String::new(),
            metadata: crate::metadata::MetadataStore::new(),
            settings_generation: 1,
            dirty: true,
            ref_select: None,
            selection_filter: SelectionFilter::default(),
            transform_gizmo: TransformArm::default(),
            component_move: ComponentMoveArm::default(),
            sketch_edit: None,
            sketch_camera_locked: true,
            sketch_list_hover_active: false,
            notices: Vec::new(),
            hidden_sketches: std::collections::HashSet::new(),
            shown_sketch_ids: Vec::new(),
            construction_frames: Vec::new(),
            sketch_profiles: Vec::new(),
            sketch_paths: Vec::new(),
            sketch_points: Vec::new(),
            sketch_axes: Vec::new(),
            wire_harness_report: None,
            pmi_report: None,
            pmi_active_view: None,
            pmi_open_annotation: None,
            pmi_modeling: None,
            pmi_explode_originals: std::collections::HashMap::new(),
            pmi_overlay_key: None,
            pmi_hovered: None,
            pmi_label_hover_active: false,
            spline_overlay_key: None,
            component_ports_visible: false,
            spline_edit_feature: None,
            spline_anchor_picked: None,
            hidden_datums: std::collections::HashSet::new(),
            shown_datum_names: Vec::new(),
            runner: Box::new(crate::runner::InlineRunner::new()),
            run_generation: 0,
            applied_generation: 0,
            run_progress: None,
            cancelled_run: None,
            pending_fit: false,
            provenance: std::collections::HashMap::new(),
            entity_origin: std::collections::HashMap::new(),
            info_cache: std::collections::HashMap::new(),
            pending_query: std::collections::HashMap::new(),
            next_query_id: 0,
            pending_mesh_imports: std::collections::HashMap::new(),
            mesh_preview_results: std::collections::VecDeque::new(),
            next_mesh_import_id: 0,
            assembly_components: Vec::new(),
            assembly_synced_generation: None,
            library_resync: false,
            parts_library_block_revision: None,
            constraint_overlays: Vec::new(),
            constraint_drag: None,
            constraint_overlay_wpp: 0.0,
            feature_dim_overlay_wpp: 0.0,
            sketch_overlay_wpp: 0.0,
            constraint_hovered: None,
            constraint_label_hover_active: false,
            scene_tree_hovered: None,
            scene_tree_hover_active: false,
            dialog_hovered: None,
            dialog_hover_active: false,
            selected_constraint: None,
            pending_step_assembly: None,
            pending_step_probes: std::collections::HashSet::new(),
            step_probe_results: std::collections::VecDeque::new(),
            next_step_probe_id: 1,
        }
    }
}

impl EngineState {
    pub fn new() -> Self {
        Self::default()
    }

    fn pick_options(&self) -> PickOptions {
        PickOptions {
            double_sided: self.settings.pick_double_sided,
            ..PickOptions::default()
        }
    }
}

/// Has the camera's `world_per_pixel` moved MATERIALLY (>0.5%) away from the
/// value an overlay group's screen-constant sizing was baked at?
///
/// The ONE judgement every camera-keyed overlay re-bake shares
/// (`ensure_overlays_current` and the per-group ensures it drives), so
/// "what counts as a zoom" cannot drift between them. `baked == 0.0` means
/// "never baked" — a state each caller handles itself, so it reads as NOT
/// stale here. The band is what keeps a quiet frame quiet: float jitter in the
/// camera never trips it, so there is no per-frame re-bake loop.
pub(crate) fn overlay_wpp_stale(baked: f64, now: f64) -> bool {
    baked > 0.0 && (now - baked).abs() > baked * 0.005
}

// ============================================================================
// MODULE MAP — engine_state is split into topic children. THIS file is the
// module root: it keeps the EngineState/RefSelectState structs, Default, the
// constructor + shared pick_options, the child `mod` declarations, and the
// re-exports that preserve the original `engine_state::*` public paths.
// Append new work to the matching child (or add a new child + re-export here).
// ============================================================================

/// The assembly surface (Wave-3): main-side session sync + component
/// projection, constraint CRUD with the document fold (pose-authority
/// contract), the insert-component flow, component fix/select actions, the
/// constraint flavor of the reference picker, and `document_signature` — the
/// ONE parts-library `sourceSignature` hash every writer shares.
mod assembly_ops;
/// Assembly-constraint viewport overlays (build-spec §8.4): overlay refresh from
/// the kernel session, the grabbable distance/angle handle pick + drag preview +
/// `assembly_update_constraint_json` commit, movedSolids re-tessellation, the
/// drag-path document fold, label feed + element hover, and tests.
mod assembly_overlay;
/// BOM export (assemblies build-spec §9): the `{partName, sourceKey,
/// quantity}` parts list off the main-side parts library + live component
/// projection, serialized as CSV / JSON for the file dialog's Export modal.
mod bom;
/// Camera & view commands (zoom-to-fit, resize, pointer/wheel ingestion,
/// projection, standard views, camera state/matrices, world→screen) plus the
/// widget-overlay feeds (datums/overlay/dimensions/transform JSON), the
/// ViewCube (incl. `apply_look_direction`), `datum_pick`, the widget
/// transform-handle hover/pick/drag path, and `dimension_anchors_json`.
mod camera_widgets;
/// Committed-sketch persistent overlays: sheet-solid synthesis from stored
/// profiles (`refresh_committed_sketches`), per-sketch visibility, the
/// Scene-tree rows + `sketch_entities_json`, and the committed-sketch tests.
mod committed_sketches;
/// The COMPONENT Move gizmo (assemblies §8.5): translate→rotate→off cycle at
/// the member-bbox center, fixed-refusal, free-move drag with the pose commit
/// (and re-solve) on release. See `ComponentMoveArm`.
mod component_move;
/// Assembly COMPONENT read surface: the app-side component projection derived
/// from the history's ACOMP features + the scene's namespaced solid names
/// (`component_of_solid` / `component_info` / `component_bbox_center`), and its
/// tests + shared assembly fixtures.
mod components;
/// Construction datum/plane display: frame→feature mapping,
/// `refresh_construction_datums`, datum visibility + selection + entity rows,
/// and the construction-datum tests.
mod construction_datums;
/// Expressions & configurator surface (`expressions_json`, `set_expressions`,
/// `configurator_json`, `expression_variables_json`) + parsing helpers + tests.
mod expressions;
/// The feature-dimension gizmo (dimension arrows / angle arc / center-sphere
/// toggle): arm state, `__brep`-style annotation JSON, overlay publishing,
/// drag + set-value writeback, `FEATURE_DIM_OVERLAY`, fd_* math helpers, tests.
mod feature_dims;
/// The assembly interference check (build-spec §9): the bbox-prefiltered
/// pairwise non-destructive INTERSECT sweep over component instances
/// (`interference_check`), its report types, and the pure pair planner.
mod interference;
/// History runs & the feature CRUD surface: `run_history_json`, the
/// runner/pump/apply seam, history JSON accessors, roll/update/add/delete/
/// reorder, engine undo/redo, `load_model_and_fit`,
/// and the history-cache rollback tests.
mod history_ops;
/// Model I/O: `import_step_feature`, `export_step_text`, `export_stl_text`,
/// `triangle_normal`, and the io tests.
mod model_io;
/// Construction PLANES as ordinary pick candidates: the combined
/// scene+plane-card candidate list (`pick_candidates_at`), the planes-aware
/// single-hit pick (`pick_top_at`), the shared candidate ordering, and the
/// plane-pick tests. Read its header for how a plane competes for a pick.
mod plane_pick;
/// Scene & object queries: `pick_json`/`hover_json`, settings + emphasis +
/// visibility + color overrides, `scene_listing_json`, `depth_range_bbox`,
/// `scene_entities_json`/select-by-name, mass properties + object info, tests.
mod scene_query;
/// The selection-filter state + kind gating (`SelectionFilter`,
/// `select_filtered_at`, hide-selected) and its tests.
mod selection_filter;
/// Selection & hover UX: clear/select-top/selection JSON, the modal
/// ref-select widget (incl. `set_json_at`), hover/candidate cycling, and the
/// live sketch overlay feed (`set_sketch_overlay`/`clear_sketch_overlay`) +
/// selection UX tests.
mod selection_ux;
/// Sketch editing ops appended after dimensions: dimension labels/value/drag,
/// sketch undo/redo + diagnostics dump, trim, external edge refs
/// (pick/link/reproject), and hand-draw strokes.
mod sketch_edit_ops;
/// Sketch-mode viewport input: overlay refresh, uv picking, hover/click/drag,
/// the draw tools + pending-geometry helpers, and delete-selection.
mod sketch_input;
/// Sketch mode session: `SketchDrag`/`SketchEdit` state, plane-frame helpers,
/// enter/exit/new sketch, and the camera lock.
mod sketch_mode;
/// Sketch entity-list panel rows + notices + solver settings
/// (`SketchEntityRow`), and the constraint palette/actions
/// (`SketchConstraintAction`, add-constraint builders, ground/construction
/// toggles, cleanup).
mod sketch_panel;
/// The transform-controls gizmo: `GizmoMode`/`TransformArm`, arm/drag/apply,
/// pose ⇄ params JSON, quaternion helpers (`rotate_euler_xyz_f64`), tests.
mod transform_gizmo;
/// The wire-harness surface: the document's `wireHarness` block (connection
/// add / edit / remove / bundles toggle — checkpointed document edits that
/// re-run the history, whose tail routes them), the applied run's routing
/// report, the endpoint list, and the panel's hover highlight.
mod wire_harness_ops;
mod pmi_ops;
pub use pmi_ops::{world_vertex_ref, PmiModelingSnapshot, PmiViewPatch};
mod pmi_overlay;
/// The 3D spline-anchor editor: the resolved anchor list, the value edits
/// (position / distances / flip / side / add / remove / reorder / detach),
/// the anchor flavour of the transform gizmo, the port-attach flavour of the
/// reference picker, and the direction-cage overlay.
mod spline_edit;

// Re-exports preserving the original `engine_state::*` public surface.
pub use assembly_ops::{document_signature, ComponentInsert};
pub use bom::PART_ATTRIBUTES;
pub use component_move::ComponentMoveArm;
pub use components::ComponentInfo;
pub use interference::{InterferencePair, InterferenceReport};
pub use model_io::{
    EmbeddedOnly, PartSink, StepAssemblyImport, StepAssemblyProbe, StepAssemblyReport,
};
// BREP private tests: 46ca25670c1b5601
pub use selection_filter::SelectionFilter;
pub use sketch_mode::{SketchDrag, SketchEdit};
pub use sketch_panel::{SketchConstraintAction, SketchEntityRow};
pub use wire_harness_ops::ConnectionPatch;
pub use spline_edit::SplineAnchorRow;
pub use transform_gizmo::{GizmoMode, TransformArm};
pub(crate) use transform_gizmo::rotate_euler_xyz_f64;

// BREP private tests: 49a4d8d471e9cf5a

// BREP private tests: 34eb9d031fb01de9

/// Reconstruction results either append to a document or remain uncommitted.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum MeshImportDestination {
    Document,
    Preview,
}