concinnity-dev 0.19.23

The Concinnity dev tooling library: world authoring, the in-engine editor, the debug server, docs and packaging
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
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
// src/editor/hook.rs
//
// The editor's per-frame drive. Implements the run loop's `DebugHook` seam: each
// frame it hit-tests the editor HUD's controls against the live input, mutates
// the working authored entry list, persists on SAVE, drives the world's cursor /
// freeze state, and re-anchors + recolours the HUD. This is the whole editor: it
// lives in the editor crate (never linked by the shipped runtime), so no editor
// code is compiled into a shipped game.
//
// The top bar (`hud.rs`) owns SAVE and the Templates dropdown. The Assets button
// opens the assets panel (`panel.rs`): a search field over every asset of the
// expanded world, grouped by origin into one collapsible tree (`asset_tree.rs`),
// and a "+" that opens a typed autocomplete of the addable types. Clicking a row
// (or picking a type from the "+" picker) opens the add / edit form in its own
// floating panel (`form_panel.rs`). A row the build generates has no world.jsonl
// line of its own: its form is seeded from what the expansion produced, and only
// confirming appends the line -- which then overrides the expansion. The search
// field and the form's name heading are real `TextInput` assets edited by the
// engine's text-input system; the hook reads them back. All three panels
// (Assets, edit form, Preview) are floating: holding their title bars drags them
// (the hook owns each origin, clamped so a panel can never leave the screen).
//
// Cursor control follows the simulation transport (`editor/sim.rs`): while
// Stopped or Paused the editor holds the cursor and the world sits frozen
// (`MenuOverride(Some(true))`); Play hands the cursor to the running world
// (`Some(false)`); Escape pauses and takes it back. Stop restores the authored
// state through the same preview rebuild every committed edit takes. F1 hides /
// shows the whole HUD.
//
// An edit shows in the preview the frame after it is committed, and SAVE only
// persists: `apply_world_swap` writes the change into the running world where
// the running world can express it (`editor/live/`), and otherwise recompiles
// and swaps a fresh world under the live render backend, which is transplanted
// across as a `PendingBackend` so the OS window is never recreated. SAVE
// re-serializes the entry list to world.jsonl and stops there: the compiled
// blobs are refreshed by an explicit build, not by editing.

use super::asset_tree::{self, TreeGroup, TreeRow};
use super::axes;
use super::behavior_panel::{self, BehaviorAction, BehaviorView, Status, ViewMode};
use super::character_shape_panel;
use super::console::{self, ConsoleSink};
use super::console_panel::{self, ConsoleAction, ConsoleView};
use super::content_panel;
use super::form::{self, FormField};
use super::form_panel::{self, FormAction, FormFocus, FormView};
use super::health::HealthState;
use super::health_panel;
use super::hud::{self, HudAction, HudState};
use super::import_panel::{self, ImportAction, ImportRow, ImportStatus, ImportView};
use super::lighting;
use super::lighting_panel::{self, LightingAction, LightingView};
use super::list_panel::Row;
use super::live;
use super::overrides;
use super::palette;
use super::palette_panel::{self, PaletteHit, PaletteView};
use super::panel::{self, PanelAction, PanelView};
use super::preview::{self, PreviewAction};
use super::registry::{self, PANEL_COUNT, PanelKey};
use super::snap;
use super::story;
use super::story_panel::{self, StoryAction, StoryView};
use super::template_panel::{self, TemplateAction, TemplateView};
use super::templates::{self, TemplatesAction};
use super::variables;
use super::variables_panel::{self, VariablesAction, VariablesView};
use super::view::{self, ViewAction};
use super::view_menu;
use super::widget::{self, point_in};
use super::world_files;
use super::worlds::{self, WorldRow, WorldTarget, WorldsAction, WorldsConfirm, WorldsView};
use worlds_start::Adopt;
// Re-exported for the hook's submodules (they reach these editor-level items as
// `super::asset_list` / `super::seeded_content`).
use super::asset_list;
use super::asset_list::ListRow;
use super::billboards;
use super::build_renderable;
use super::create_menu;
use super::cursor;
use super::framing;
use super::gizmo;
use super::group_transform;
use super::highlight;
use super::history::History;
use super::marquee;
use super::modal;
use super::notify;
use super::orbit;
use super::outlines;
use super::resize;
use super::selection::Selection;
use super::session_store;
use super::sim;
use super::toast_overlay;
use super::visibility;
use crate::app::state::App;
use crate::components::FrameInput;
use crate::debug_hook::DebugHook;
use crate::ecs::asset_id::AssetId;
use crate::ecs::{CursorShape, DesiredCursor, HudLayers, MenuOverride, PendingBackend, World};

// Draw layer for the top bar: far above the floating panels' layers (which are a
// small 1..=6 rank), so the bar always sits on top even under a dragged panel.
const TOP_BAR_LAYER: i32 = 1_000;

// An active title-bar drag: the grabbed panel and the cursor's offset from its
// origin at the press, so the panel follows without snapping to the cursor.
#[derive(Debug, Clone, Copy)]
struct Drag {
    key: PanelKey,
    grab: [f32; 2],
}

pub(crate) struct EditorHook {
    // Path to the world.jsonl the edits are written back to.
    world_path: String,
    // The authored entry list (names live here, unlike the compiled blob). Edits
    // mutate this; SAVE serializes it back to `world_path`.
    entries: Vec<serde_json::Value>,
    // Undo/redo stacks over `entries`. `baseline` mirrors `entries` as of the
    // last committed edit (or undo/redo), so when `mark_changed` runs after a
    // mutation it still holds the pre-edit list -- the undo snapshot. `saved`
    // mirrors the on-disk state, so a history jump can recompute `dirty`.
    history: History,
    baseline: Vec<serde_json::Value>,
    saved: Vec<serde_json::Value>,
    // Whether `entries` has changes not yet written to disk.
    dirty: bool,
    // The simulation transport (Play / Pause / Step / Stop). Starts Stopped:
    // the editor owns the cursor at launch so the HUD is immediately usable.
    sim: sim::SimControl,
    // Whether the whole HUD is shown (F1 toggle). Starts shown.
    hud_visible: bool,
    // Whether the world-origin axes draw in the viewport (Preview panel row).
    // Starts on: the axes are the viewport's orientation reference.
    axes_visible: bool,
    // Set whenever the authored entries change (add / edit / delete / template);
    // consumed by `apply_world_swap` to bring the live preview world back in
    // line with them. SAVE does not set this -- the preview is already current.
    rebuild_preview: bool,
    // Set alongside it when only a full rebuild will do: the running world
    // holds state no authored diff describes (a simulation that ran, a story
    // source re-read from disk), so the live-apply path must not claim it.
    rebuild_required: bool,
    // The entry list the live preview world was built from, and the template
    // baselines that build's expansion merged authored patches over. An edit is
    // applied to the running world by diffing against these; a rebuild re-seeds
    // both. The baselines are seeded on demand (the first edit that could be
    // applied live expands the list once to find them), and an edit cannot be
    // applied live until they are.
    world_entries: Vec<serde_json::Value>,
    world_shadows: Option<live::ShadowBaselines>,
    // Whether the Templates panel is shown (toggled from the View panel).
    templates_open: bool,
    // The template whose detail panel is open (index into the templates
    // registry); `None` means the detail panel is closed.
    open_template: Option<usize>,
    // First visible row of the Template detail panel's asset list.
    template_list_scroll: usize,
    // Whether the Lighting panel is shown (toggled from the View panel), which
    // text binding holds keyboard focus, and the message from the last rejected
    // Apply.
    lighting_open: bool,
    lighting_focus: Option<usize>,
    lighting_status: Option<String>,
    // The CharacterShape panel: shown state, the row window's scroll, the row
    // count sampled once a frame (the rows come from the live world, which
    // the panel sizing cannot reach), the last rejected commit, the seed
    // counter behind Randomize, and a slider drag in flight
    // (`hook/shape_drag.rs`).
    shape_open: bool,
    shape_scroll: usize,
    shape_rows: usize,
    shape_status: Option<String>,
    shape_seed: u64,
    shape_drag: Option<shape_drag::ShapeDrag>,
    // The Story panel: shown state, the loaded source's lines / edit line /
    // window scroll, whether the edit line holds keyboard focus, the source
    // path shown in the header, and the last parse / IO error. `story_blur`
    // suppresses the edit line's focus for one frame after a Backspace line
    // join, so the text system does not also apply that Backspace to the
    // freshly joined content.
    story_open: bool,
    story_lines: Vec<String>,
    story_line: usize,
    story_scroll: usize,
    story_focus: bool,
    story_path: String,
    story_status: Option<String>,
    story_blur: bool,
    // The Import panel: shown state, whether the path field holds keyboard
    // focus, the list window scroll, and the last Add's outcome.
    import_open: bool,
    import_focus: bool,
    import_scroll: usize,
    import_status: Option<ImportStatus>,
    // The Console panel: shown state, whether the command line holds keyboard
    // focus (suppressed for one frame after a backtick open so the text
    // system does not type the backtick into it), the log window's scroll
    // position with its pinned-to-bottom flag (pinned auto-scrolls on new
    // lines until the user scrolls up), the shared log sink, and whether a
    // worker is mid-build (the /cook guard).
    console_open: bool,
    console_focus: bool,
    console_blur: bool,
    console_scroll: usize,
    console_pinned: bool,
    console_sink: ConsoleSink,
    console_build_running: std::sync::Arc<std::sync::atomic::AtomicBool>,
    // The toast queue (`editor/notify.rs`): result sites push, the per-frame
    // drive (`hook/notify_drive.rs`) draws the stack. The latch skips the
    // overlay's hide pass while nothing is live, so an idle queue costs one
    // lock check a frame.
    notifier: notify::Notifier,
    toasts_hidden: bool,
    // The Behavior panel: shown state, which of the world's Behavior entries is
    // open (an ordinal into them, so an unrelated add / delete cannot retarget
    // it), the selected outline row, the outline and palette scrolls, whether
    // the palette is up and which of its options the keyboard is on, whether the
    // value field holds keyboard focus, and the world checker's verdict on the
    // open behavior. The name field carries its own focus, and
    // `behavior_remove_armed` is the removal chip waiting on the press that
    // carries it out.
    behavior_open: bool,
    behavior_index: usize,
    behavior_row: Option<usize>,
    behavior_scroll: usize,
    behavior_picking: bool,
    behavior_pick_scroll: usize,
    behavior_pick: usize,
    // The palette's filter text, mirrored off its field once a frame so the
    // presses and draws that follow all narrow by the same query.
    behavior_filter: String,
    behavior_focus: bool,
    behavior_name_focus: bool,
    behavior_remove_armed: bool,
    behavior_status: Option<Status>,
    behavior_mode: ViewMode,
    // The chart's scroll offset, and the anchor an in-flight canvas pan holds.
    behavior_pan: [f32; 2],
    behavior_pan_drag: Option<[f32; 2]>,
    // The Variables panel: shown state, the selected row of the table, the row
    // window's scroll, and which of its two fields holds the keyboard.
    variables_open: bool,
    variables_row: Option<usize>,
    variables_scroll: usize,
    variables_name_focus: bool,
    variables_value_focus: bool,
    // The list member held for a paste, with the kind of list it came out of so
    // it can only land in one of the same kind. Session state, not an edit, and
    // deliberately not cleared by opening another behavior: carrying a node
    // between two of them is most of the point.
    behavior_clip: Option<crate::editor::behavior::clip::Clip>,
    // The overview's selected card. The map's cards stand for whole behaviors
    // and the things they reach rather than for places inside one, so the
    // outline row the other two views share cannot address them.
    behavior_overview_card: Option<usize>,
    // Live-debug state fed by the runtime's execution trace while a play
    // session runs with the Behavior or Variables panel open
    // (`hook/trace_drive.rs`). Pulses cover the OPEN behavior only (paths are
    // per-body); breakpoints are held by behavior NAME + node path so they
    // survive preview rebuilds and body edits shifting node ids.
    behavior_pulses: Vec<crate::editor::behavior::pulse::NodePulse>,
    behavior_breakpoints: Vec<(String, crate::editor::behavior::path::Path)>,
    trace_seen: u64,
    live_vars: Vec<(String, String, String)>,
    live_locals: Vec<(String, String, String)>,
    // Ctrl state sampled from this frame's input, for the panel presses that
    // resolve without direct input access (a Ctrl+click on a chart card
    // toggles its breakpoint).
    ctrl_held: bool,
    // Editor-session hide / lock sets, by NAME (ids drift across preview
    // rebuilds). Hidden assets skip rendering (via the published
    // `HiddenAssets` resource); locked ones are skipped by viewport picking.
    // Neither touches the authored entries.
    hidden_assets: std::collections::BTreeSet<String>,
    locked_assets: std::collections::BTreeSet<String>,
    // An active isolate (`hook/hide.rs`): the names kept visible, hiding
    // everything else. Composes with (and never mutates) `hidden_assets`.
    isolate: Option<std::collections::BTreeSet<String>>,
    // Shift state sampled from this frame's input, for the panel presses that
    // resolve without direct input access (the Assets tree's additive select).
    shift_held: bool,
    // Viewport sampled from this frame's input, for the panel presses that
    // resolve without direct input access (framing a palette pick).
    viewport: [f32; 2],
    // Window chrome floating over the top of the frame, sampled from the same
    // input. The start screen's docked sidebar starts its content below it so
    // the OS window buttons never land on a control.
    top_inset: f32,
    // Whether the Assets panel is shown (toggled from the View panel).
    panel_open: bool,
    // The Assets panel's body: every asset of the expanded world as one tree
    // grouped by origin. `tree_groups` is the cooked model (it costs a world
    // expansion, so it is recomputed only when `tree_stale` and the panel is
    // up), `tree_unfolded` holds the groups the user unfolded, `row_menu` the
    // name whose Delete menu is open, and `tree_status` carries a cook failure
    // to the status line.
    tree_groups: Vec<TreeGroup>,
    tree_unfolded: Vec<usize>,
    tree_scroll: usize,
    tree_stale: bool,
    tree_status: Option<String>,
    search_focus: bool,
    row_menu: Option<String>,
    // The header "+" type picker: whether its option list is open and how far it
    // is scrolled. While open the search field narrows those options instead of
    // the tree.
    picker_open: bool,
    picker_scroll: usize,
    // The Content panel (the visual-asset thumbnail grid): shown state, the
    // grid's first visible row, the type-chip cycle position (0 = All, i =
    // VISUAL_TYPES[i-1]), whether its search field holds keyboard focus, and
    // an in-flight drag-out placement (`hook/content_drag.rs`), if any.
    content_open: bool,
    content_scroll: usize,
    content_type: usize,
    content_search_focus: bool,
    content_drag: Option<content_drag::ContentDrag>,
    // The right-click "Create here" menu (`hook/create_menu_drive.rs`), if open.
    create_menu: Option<create_menu_drive::CreateMenu>,
    // The confirmation dialog (`hook/modal_drive.rs`), if open. Screen-modal:
    // while open every press and wheel is swallowed before any other routing,
    // and only one of its buttons closes it.
    modal: Option<modal_drive::ModalState>,
    // The Worlds panel (`hook/worlds_edit.rs`): shown state, the project's
    // worlds as of the last refresh (the listing changes only when the panel
    // acts on it, so it is not re-read every frame), the row window's scroll,
    // the path of the row whose triple-dot menu is open, and why the last
    // preview failed.
    worlds_open: bool,
    worlds_rows: Vec<WorldRow>,
    worlds_scroll: usize,
    worlds_menu: Option<String>,
    worlds_status: Option<String>,
    // Whether the session's world has never been named: `+` starts one, the
    // whole editor comes up on it, and the first SAVE asks what to call it
    // before anything reaches disk.
    untitled: bool,
    // The start screen (`hook/worlds_start.rs`): a session that named no world
    // on the command line opens on the Worlds panel alone, which owns the
    // window and previews the picked world behind itself. Set at construction
    // and cleared for good the first time a world is opened.
    start_mode: bool,
    // The path of the start screen's selected row, and of the world its
    // background preview was compiled from. They part company when the
    // previewed world is deleted (the background drops back to the seeded
    // empty scene) or its compile fails. Both `None` outside the start screen,
    // which has no selection model.
    worlds_selected: Option<String>,
    worlds_preview: Option<String>,
    // The world the start screen picked before it had a window to show it in
    // (`hook/worlds_start.rs`): held until the screen itself is up and drawn,
    // then staged like any other preview. `start_drawn` counts the frames the
    // screen has been laid out for, which is what "up" means here.
    start_preview: Option<String>,
    start_drawn: u32,
    // The start screen's attract camera (`hook/cinematic_drive.rs`): the shot
    // cycle running over the previewed world, the clock it advances on, and the
    // pose that world's own camera held before the cycle took it -- put back
    // the moment the screen hands the session a world.
    cinematic: Option<worlds::cinematic::Cinematic>,
    cinematic_clock: Option<std::time::Instant>,
    cinematic_restore: Option<framing::CameraPose>,
    // The command palette (`hook/palette_edit.rs`): shown state, a one-frame
    // focus blur after the Ctrl+K open, the query mirrored off its field once
    // a frame, the item list built on open with the matches the query keeps,
    // the highlighted match with its window scroll, and the labels of recent
    // commits (session state, the empty query's launch list).
    palette_open: bool,
    palette_blur: bool,
    palette_query: String,
    palette_items: Vec<palette::PaletteItem>,
    palette_matches: Vec<usize>,
    palette_pick: usize,
    palette_scroll: usize,
    palette_recent: Vec<String>,
    // Whether the Preview panel is shown (starts shown; toggled from the View
    // panel).
    preview_open: bool,
    // Whether the Health panel is shown (starts hidden; toggled from the View
    // panel).
    health_open: bool,
    // The Health panel's sampler. Ticked every frame whether or not the panel is
    // shown, so its rates cover a continuous window.
    health: HealthState,
    // Whether the View panel itself is shown (the top-bar View button toggles it).
    view_open: bool,
    // The Display menu (`hook/view_menu_drive.rs`): open state, the viewport
    // view mode, the show flags, and the editor-side billboard-icons toggle.
    display_menu_open: bool,
    view_mode: view_menu::ViewMode,
    show_flags: view_menu::ShowFlags,
    show_billboards: bool,
    // The Display menu's always-on extent-outline categories (selection
    // outlines regardless; `hook/outline_drive.rs`).
    extent_show: outlines::CategorySet,
    // The type of the open add / edit form; `None` means the form panel is
    // closed.
    selected_type: Option<String>,
    // What confirming the open form commits to: a new asset, an existing line,
    // or the promotion of a generated asset.
    form_target: FormTarget,
    // The editable arg fields of the open form (derived from the type's default
    // args). Empty while the form is closed.
    form_fields: Vec<FormField>,
    // First visible field of the form's scroll window (its physical control pool is
    // fixed size, so a form wider than `form::FIELD_POOL` scrolls). Reset on open /
    // structural change.
    form_scroll: usize,
    // Which form input has keyboard focus.
    form_focus: FormFocus,
    // A validation message from the last rejected Add, shown under the form.
    form_error: Option<String>,
    // The form arg field whose value dropdown is open (a large enum / ref set),
    // and its scroll offset. `None` outside an open dropdown.
    field_dropdown: Option<usize>,
    field_dropdown_scroll: usize,
    // The open form's working args tree: the fields are derived from it, and it is
    // mutated by add / remove (structure) and, on capture, by the controls. Empty
    // outside AddForm.
    form_args: serde_json::Map<String, serde_json::Value>,
    // The template behind the open form when it edits a template-derived asset:
    // confirming writes the minimal patch against this baseline, and the rows
    // show per-field override state. `None` for plain authored / new assets.
    form_template: Option<FormTemplate>,
    // The field whose override menu (Revert / Apply-to-template) is open, and
    // whether the header's entity-level menu is open.
    override_menu: Option<usize>,
    entity_menu_open: bool,
    // Template baselines for every template-derived asset, derived from the
    // working entries. Invalidated by every edit, rebuilt on demand.
    template_index: Option<overrides::TemplateIndex>,
    // The paths of the form's non-colour vector fields currently disclosed into
    // per-element leaves. Cleared when the form opens / closes.
    vec_expanded: std::collections::HashSet<String>,
    // The viewport selection set (`editor/selection.rs`), held by NAME: every
    // live-preview rebuild resets the interner and re-interns names, so a
    // stored AssetId could silently drift to a different asset. Members are
    // re-resolved each frame (`hook/pick.rs`). `pick_last` is the transient
    // repeat-click cycle; `marquee` an in-flight box select.
    selection: Selection,
    pick_last: Option<pick::PickLast>,
    marquee: Option<marquee_drag::MarqueeDrag>,
    // An in-flight Alt+drag tumble around the selection
    // (`hook/orbit_drive.rs`), if any.
    orbit: Option<orbit_drive::OrbitDrag>,
    // The gizmo's edit mode (T/R/S keys) and an active drag
    // (`hook/gizmo_drag.rs`), if any.
    gizmo_mode: gizmo::GizmoMode,
    gizmo_drag: Option<gizmo_drag::GizmoDrag>,
    // Grid / angle snapping for gizmo drags (Preview panel rows + /snap).
    snap: snap::SnapSettings,
    // Whether a drag-out placement orients the drop to the struck surface's
    // face normal (Preview panel row). Session state, off by default.
    align_to_surface: bool,
    // The edit-mode fly camera (`hook/fly.rs`): on/off and the frame clock
    // its integration steps against.
    fly: bool,
    fly_clock: Option<std::time::Instant>,
    // An in-flight framing / bookmark camera glide (`hook/glide_drive.rs`).
    glide: Option<glide_drive::CameraGlide>,
    // Saved camera poses (`hook/bookmarks.rs`), loaded from the per-project
    // session store and persisted on save.
    bookmarks: [Option<framing::CameraPose>; session_store::BOOKMARK_SLOTS],
    // The floating panels' dragged origins, indexed by `PanelKey`; `None` means
    // the panel still sits at its default anchor. Always clamped fully on screen
    // before use.
    positions: [Option<[f32; 2]>; PANEL_COUNT],
    // The user's per-panel size overrides, indexed by `PanelKey`; `None` means the
    // panel is at its content-derived default. Only ever grows a panel past that
    // default (see `effective_size`), and only the resizable panels are set.
    sizes: [Option<[f32; 2]>; PANEL_COUNT],
    // The title-bar drag in progress, if any.
    drag: Option<Drag>,
    // The edge / corner resize in progress, if any.
    resize: Option<resize::Resize>,
    // The floating panels back-to-front: the last entry is the frontmost (drawn on
    // top + first to receive clicks). Dragging or clicking a panel moves it to the
    // end. Its position drives the per-frame `HudLayers` publish so overlapping
    // panels occlude cleanly instead of merging.
    panel_order: Vec<PanelKey>,
    // Unapplied-edit markers: typed-or-clicked control state a panel holds
    // that has not been committed by its Apply / Add. Event-driven (set on
    // edit input, cleared on open / apply), so no per-frame comparisons. The
    // panels suffix their heading with "*" while set. Behavior / Variables
    // commit per change and never hold unapplied state, so they carry none.
    form_touched: bool,
    lighting_touched: bool,
    story_touched: bool,
    // The preview rebuild blocks the frame loop, so when the last one measured
    // slow its card goes up ahead of the stall: `rebuild_op` holds the card,
    // `rebuild_countdown` the frames left before the rebuild runs (the card
    // must be drawn and presented first), and `last_rebuild_secs` the honest
    // measure that gates the whole affordance.
    rebuild_op: Option<notify::OpHandle>,
    rebuild_countdown: u8,
    last_rebuild_secs: f32,
}

// The template a form-edited asset derives from.
#[derive(Debug, Clone)]
pub(crate) struct FormTemplate {
    pub name: String,
    // Effective template args: the type's defaults with the generated args
    // merged over them, the baseline a field is inherited from.
    pub baseline: serde_json::Map<String, serde_json::Value>,
    // The authored asset or injection pass that produced the asset.
    pub generated_by: String,
}

// Owned per-tick data backing a `PanelView` (computed from the cooked tree + the
// live search field, then borrowed for both hit-testing and layout).
struct PanelData {
    rows: Vec<TreeRow>,
    picker_options: Option<Vec<String>>,
    form_title: String,
    form_overrides: Option<FormOverridesData>,
}

// Owned per-tick override state backing a `form_panel::OverridesView`.
struct FormOverridesData {
    marks: Vec<overrides::FieldOrigin>,
    count: usize,
    field_menu: Option<(usize, Vec<String>)>,
    entity_menu: Option<Vec<String>>,
}

// What confirming the open add / edit form commits to.
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) enum FormTarget {
    // A new asset: confirming appends it under a unique name.
    #[default]
    New,
    // Working-entry `idx`: confirming updates that line in place.
    Entry(usize),
    // An asset the build generates, which has no world.jsonl line of its own.
    // The form is seeded from the entry the expansion produced, and confirming
    // appends that line -- which then overrides the expansion, since the cook
    // drops a generated asset in favour of an authored one of the same name and
    // type. Renaming it in the form instead leaves the generated asset in place
    // and adds a separate one, which is the honest reading of a rename.
    Promote(serde_json::Value),
}

impl FormTarget {
    // The working-entry index the form updates in place, if any.
    fn entry(&self) -> Option<usize> {
        match self {
            FormTarget::Entry(i) => Some(*i),
            _ => None,
        }
    }

    // Whether the form is editing an asset that already exists (in the world or
    // in the build), rather than adding a brand-new one.
    fn is_edit(&self) -> bool {
        !matches!(self, FormTarget::New)
    }
}

// Owned per-tick data backing a `TemplateView` (the open template's title,
// description, and grouped asset rows).
#[derive(Default)]
struct TemplateDetailData {
    title: String,
    description: String,
    rows: Vec<ListRow>,
}

// Owned per-tick data backing a `LightingView` (the row list and the
// per-binding fields derived from the current entries).
struct LightingData {
    rows: Vec<lighting::Row>,
    fields: Vec<Option<FormField>>,
}

// Move a scroll offset one row toward the wheel direction, clamped to `max`.
fn scroll_step(cur: usize, delta: f32, max: usize) -> usize {
    if delta > 0.0 {
        (cur + 1).min(max)
    } else {
        cur.saturating_sub(1)
    }
}

// The physical control slot showing logical form field `j` under scroll offset
// `scroll`, or `None` when the field is outside the visible window of `window`
// rows. The panel's control pool is slot-indexed, so seeding / reading a field
// goes through its slot.
fn visible_slot(j: usize, scroll: usize, window: usize) -> Option<usize> {
    (j >= scroll && j < scroll + window).then(|| j - scroll)
}

// The first line of a validation error, clipped to fit the panel's status line.
fn short_status(e: &str) -> String {
    let line = e.lines().next().unwrap_or(e);
    let clipped: String = line.chars().take(44).collect();
    if clipped.len() < line.len() {
        format!("{clipped}...")
    } else {
        clipped
    }
}

// The `name` string of an entry, if present.
fn entry_name(e: &serde_json::Value) -> Option<&str> {
    e.get("name").and_then(|v| v.as_str())
}
fn entry_type(e: &serde_json::Value) -> Option<&str> {
    e.get("type").and_then(|v| v.as_str())
}

// The names of the working entries whose type is `ty` (the reference options a
// field targeting that type can pick from).
fn names_of_type(entries: &[serde_json::Value], ty: &str) -> Vec<String> {
    entries
        .iter()
        .filter(|e| entry_type(e) == Some(ty))
        .filter_map(|e| entry_name(e).map(String::from))
        .collect()
}

// Named to avoid colliding with the `use super::asset_tree` module import.
mod asset_tree_edit;
mod axes_drive;
mod behavior_asset;
// Named to avoid colliding with the `use super::behavior_panel` import.
mod behavior_edit;
mod behavior_keys;
mod billboard_drive;
mod bookmarks;
mod browse;
mod camera_pose;
// Named to avoid colliding with the `use super::worlds::cinematic` import.
mod cinematic_drive;
// Named to avoid colliding with the `use super::console` module import.
mod console_edit;
mod content_drag;
mod content_edit;
mod cook_worker;
// Named to avoid colliding with the `use super::create_menu` module import.
mod create_menu_drive;
mod drop_floor;
mod duplicate;
mod editing;
mod edits;
mod export_edit;
mod fly;
mod gizmo_drag;
mod glide_drive;
mod hide;
mod import_edit;
mod layout;
mod marquee_drag;
// Named to avoid colliding with the `use super::modal` module import.
mod modal_drive;
#[cfg(test)]
mod modal_tests;
mod orbit_drive;
mod outline_drive;
// Named to avoid colliding with the `use super::overrides` module import.
mod override_edit;
// Named to avoid colliding with the `use super::palette` module import.
mod palette_edit;
mod pick;
// Named to avoid colliding with the `use super::lighting` module import.
mod lighting_edit;
// Named to avoid colliding with the `use super::character_shape_panel` import.
mod character_shape_edit;
mod shape_drag;
// The per-panel `Panel` impls, reachable by the registry (`editor/registry.rs`).
mod notify_drive;
pub(super) mod panels;
mod routing;
mod select_edit;
mod sim_control;
mod trace_drive;
mod view_menu_drive;
// Named to avoid colliding with the `use super::worlds` module import.
mod worlds_edit;
mod worlds_start;
#[cfg(test)]
mod worlds_start_tests;
#[cfg(test)]
mod worlds_tests;
// Named to avoid colliding with the `use super::story` module import.
mod story_edit;
// Named to avoid colliding with the `use super::variables_panel` import.
#[cfg(test)]
mod camera_tests;
#[cfg(test)]
mod cinematic_tests;
#[cfg(test)]
mod console_tests;
#[cfg(test)]
mod override_tests;
#[cfg(test)]
mod palette_tests;
#[cfg(test)]
mod panel_tests;
#[cfg(test)]
mod select_tests;
#[cfg(test)]
mod tests;
mod variables_edit;

impl EditorHook {
    pub(crate) fn new(world_path: String, entries: Vec<serde_json::Value>) -> Self {
        let bookmarks = session_store::default_path()
            .and_then(|path| {
                session_store::load(&path)
                    .worlds
                    .get(&session_store::world_key(&world_path))
                    .map(|w| w.bookmarks)
            })
            .unwrap_or_default();
        Self {
            world_path,
            history: History::default(),
            baseline: entries.clone(),
            saved: entries.clone(),
            world_entries: entries.clone(),
            entries,
            dirty: false,
            sim: sim::SimControl::default(),
            hud_visible: true,
            axes_visible: true,
            rebuild_preview: false,
            rebuild_required: false,
            world_shadows: None,
            templates_open: false,
            open_template: None,
            template_list_scroll: 0,
            lighting_open: false,
            lighting_focus: None,
            lighting_status: None,
            shape_open: false,
            shape_scroll: 0,
            shape_rows: 0,
            shape_status: None,
            shape_seed: 0,
            shape_drag: None,
            story_open: false,
            story_lines: vec![String::new()],
            story_line: 0,
            story_scroll: 0,
            story_focus: false,
            story_path: String::new(),
            story_status: None,
            story_blur: false,
            import_open: false,
            import_focus: false,
            import_scroll: 0,
            import_status: None,
            console_open: false,
            console_focus: false,
            console_blur: false,
            console_scroll: 0,
            console_pinned: true,
            console_sink: ConsoleSink::default(),
            console_build_running: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
            notifier: notify::Notifier::default(),
            toasts_hidden: true,
            behavior_open: false,
            behavior_index: 0,
            behavior_row: None,
            behavior_scroll: 0,
            behavior_picking: false,
            behavior_pick_scroll: 0,
            behavior_pick: 0,
            behavior_filter: String::new(),
            behavior_focus: false,
            behavior_name_focus: false,
            behavior_remove_armed: false,
            behavior_status: None,
            behavior_mode: ViewMode::default(),
            behavior_pan: [0.0, 0.0],
            behavior_pan_drag: None,
            variables_open: false,
            variables_row: None,
            variables_scroll: 0,
            variables_name_focus: false,
            variables_value_focus: false,
            behavior_clip: None,
            behavior_overview_card: None,
            behavior_pulses: Vec::new(),
            behavior_breakpoints: Vec::new(),
            trace_seen: 0,
            live_vars: Vec::new(),
            live_locals: Vec::new(),
            ctrl_held: false,
            hidden_assets: std::collections::BTreeSet::new(),
            locked_assets: std::collections::BTreeSet::new(),
            isolate: None,
            shift_held: false,
            viewport: [0.0, 0.0],
            top_inset: 0.0,
            panel_open: false,
            tree_groups: Vec::new(),
            tree_unfolded: Vec::new(),
            tree_scroll: 0,
            tree_stale: true,
            tree_status: None,
            search_focus: false,
            row_menu: None,
            picker_open: false,
            picker_scroll: 0,
            content_open: false,
            content_scroll: 0,
            content_type: 0,
            content_search_focus: false,
            content_drag: None,
            create_menu: None,
            modal: None,
            worlds_open: false,
            worlds_rows: Vec::new(),
            worlds_scroll: 0,
            worlds_menu: None,
            worlds_status: None,
            untitled: false,
            start_mode: false,
            worlds_selected: None,
            worlds_preview: None,
            start_preview: None,
            start_drawn: 0,
            cinematic: None,
            cinematic_clock: None,
            cinematic_restore: None,
            palette_open: false,
            palette_blur: false,
            palette_query: String::new(),
            palette_items: Vec::new(),
            palette_matches: Vec::new(),
            palette_pick: 0,
            palette_scroll: 0,
            palette_recent: Vec::new(),
            preview_open: true,
            health_open: false,
            health: HealthState::new(),
            view_open: false,
            display_menu_open: false,
            view_mode: view_menu::ViewMode::default(),
            show_flags: view_menu::ShowFlags::default(),
            show_billboards: true,
            extent_show: outlines::CategorySet::default(),
            selected_type: None,
            form_target: FormTarget::New,
            form_fields: Vec::new(),
            form_scroll: 0,
            form_focus: FormFocus::Name,
            form_error: None,
            field_dropdown: None,
            field_dropdown_scroll: 0,
            form_args: serde_json::Map::new(),
            form_template: None,
            override_menu: None,
            entity_menu_open: false,
            template_index: None,
            vec_expanded: std::collections::HashSet::new(),
            selection: Selection::default(),
            pick_last: None,
            marquee: None,
            orbit: None,
            gizmo_mode: gizmo::GizmoMode::default(),
            gizmo_drag: None,
            snap: snap::SnapSettings::default(),
            align_to_surface: false,
            fly: false,
            fly_clock: None,
            glide: None,
            bookmarks,
            positions: [None; PANEL_COUNT],
            sizes: [None; PANEL_COUNT],
            drag: None,
            resize: None,
            // Back-to-front, matching the injected draw order (registry order:
            // the Template detail panel frontmost, over the Templates list it
            // spawns from).
            panel_order: PanelKey::ALL.to_vec(),
            form_touched: false,
            lighting_touched: false,
            story_touched: false,
            rebuild_op: None,
            rebuild_countdown: 0,
            last_rebuild_secs: 0.0,
        }
    }
}

impl EditorHook {
    // Replace the standalone sink defaulted by `new` with the shared one the
    // tracing mirror was installed on (see `run_editor`).
    pub(crate) fn with_console_sink(mut self, sink: ConsoleSink) -> Self {
        self.console_sink = sink;
        self
    }

    // Open on the start screen: a session started without a world named on the
    // command line picks one there before it edits anything (see `run_editor`).
    // `picked` is the project's most recent world, preselected so the user
    // lands on their last project rather than on a void -- but it is not
    // compiled yet. The window comes up on the listing alone and the pick is
    // previewed from there, behind the loading cover, so a world that takes
    // seconds to compile does not hold the window closed for them.
    pub(crate) fn with_start_screen(mut self, picked: Option<String>) -> Self {
        self.start_mode = true;
        self.worlds_open = true;
        self.worlds_selected = picked.clone();
        self.start_preview = picked;
        self.refresh_worlds();
        self
    }

    // A clone of the toast queue handle, for the sibling hooks (the debug
    // server's hot-reload passes report through it).
    pub(crate) fn notifier(&self) -> notify::Notifier {
        self.notifier.clone()
    }

    // Whether any editor text control holds keyboard focus this frame: the
    // Assets panel's search field (or the type picker typing into it), an open
    // edit form (its name / arg inputs always own focus while it shows), or a
    // focused Lighting / Story / Import / Console field. Undo/redo shortcuts
    // stand down while typing.
    // Read the palette's filter off its field. Mirrored onto the hook because
    // the data a press and a draw resolve against is built without world access.
    fn sample_behavior_filter(&mut self, world: &World) {
        let typed = match self.behavior_picking {
            true => widget::field_text(world, behavior_panel::FILTER_INPUT),
            false => String::new(),
        };
        if typed != self.behavior_filter {
            // A narrowed palette is a different list, so the highlight starts
            // again at its best answer rather than keeping a place that may no
            // longer be in it.
            self.behavior_pick = 0;
            self.behavior_pick_scroll = 0;
            self.behavior_filter = typed;
        }
    }

    // The frontmost open floating panel, if any: the keyboard target for
    // per-frame editing keys and the arbiter for shortcuts a panel claims.
    fn frontmost_open_panel(&self) -> Option<PanelKey> {
        self.panel_order
            .iter()
            .rev()
            .copied()
            .find(|&k| self.panel_shown(k))
    }

    fn text_focus_active(&self) -> bool {
        self.non_console_text_focus() || self.console_focus
    }

    // The same, excluding the console's own command line: the backtick toggle
    // must keep working while the console is being typed into, but stand down
    // while any other field is (a backtick there is just a character).
    fn non_console_text_focus(&self) -> bool {
        self.search_focus
            || self.content_search_focus
            || self.picker_open
            || self.selected_type.is_some()
            || self.lighting_focus.is_some()
            || self.story_focus
            || self.import_focus
            || self.behavior_focus
            || self.behavior_name_focus
            || self.behavior_picking
            || self.variables_name_focus
            || self.variables_value_focus
            || self.naming_world()
            || self.palette_open
    }
}

impl DebugHook for EditorHook {
    fn tick(&mut self, world: &mut World) {
        // Bring the Assets tree up to date before anything reads it, so this
        // frame's hit test and draw agree on the rows.
        self.refresh_tree_if_needed();
        // Accumulate the Health panel's per-frame counters and, on its throttled
        // boundary, resample. Unconditional: the rates measure a continuous
        // window, so gating this on the panel being open would make the first
        // window after opening report a partial one.
        self.health.sample(world);
        // Seed Transforms onto the billboard-backed entities before any of
        // this frame's picking or gizmo work resolves them.
        self.seed_billboard_transforms(world);
        // Sampled before anything routes, so this frame's hit tests and draw
        // narrow the palette by the same query.
        self.sample_behavior_filter(world);
        self.sample_palette_query(world);
        self.sample_shape_rows(world);
        let input = world.query::<FrameInput>().last().cloned();
        if let Some(input) = &input {
            // Sampled for the panel presses that resolve without direct input
            // access (the Assets tree's additive select, the chart's
            // Ctrl+click breakpoint toggle).
            self.shift_held = input.shift;
            self.ctrl_held = input.ctrl;
            self.viewport = input.viewport;
            self.top_inset = input.top_inset;
            // Typing into a batching panel's focused field marks its
            // unapplied-edit state (the heading's "*"). Frontmost-gated,
            // because only the frontmost panel's field owns the keyboard.
            if input.typed_char.is_some() {
                match self.frontmost_open_panel() {
                    Some(PanelKey::Edit) => self.form_touched = true,
                    Some(PanelKey::Lighting) if self.lighting_focus.is_some() => {
                        self.lighting_touched = true;
                    }
                    Some(PanelKey::Story) if self.story_focus => self.story_touched = true,
                    _ => {}
                }
            }
            // Escape hands the cursor back to the editor: a running world
            // pauses mid-state, the fly camera exits, and the create menu
            // dismisses.
            if input.escape {
                self.sim.pause();
                self.fly = false;
                self.fly_clock = None;
                self.glide = None;
                self.orbit = None;
                self.create_menu = None;
                self.display_menu_open = false;
                self.close_palette();
            }
            // The fly camera integrates before any routing: while it is on
            // the cursor is captured, so no click or HUD press can arrive.
            // An in-flight glide owns the camera instead; deliberate fly
            // input cancels it (see `drive_glide`).
            if self.glide.is_some() {
                self.drive_glide(input, world);
            } else {
                self.drive_fly(input, world);
            }
            // F1 (an edge pulse) toggles the whole HUD. The start screen
            // stands outside it: hiding the only panel on screen would leave
            // the session with nothing to click.
            if input.hud_toggle && !self.start_mode {
                self.hud_visible = !self.hud_visible;
            }
            let vp = input.viewport;
            if self.hud_visible && vp[0] > 0.0 {
                // The start screen owns the whole window while it is up: its
                // panel is the only thing to route to, and the editor's
                // shortcuts, menus, and overlays stand down until a world is
                // open (`hook/worlds_start.rs`).
                if self.start_mode {
                    self.drive_start_input(input, vp, world);
                } else {
                    self.drive_session_input(input, vp, world);
                }
            }
        }

        // The pick the screen opened on, once the screen itself has been drawn:
        // the compile it costs is spent behind a window that is already up.
        self.drive_start_preview();
        // The start screen's attract camera, after the frame's routing: a click
        // that picked another world has already restarted the cycle, so the
        // pose written here frames the world the sidebar is now previewing.
        self.drive_cinematic(world);

        // Drive the world's cursor / freeze state from the transport: Stopped /
        // Paused (`Some(true)`) free the cursor and freeze the world; Playing
        // (`Some(false)`) runs it -- as does the one frame a queued Step takes.
        // The fly flag layers on top: navigation input + cursor capture stay
        // live while the frozen world is flown through.
        self.drive_sim(world);
        // Exchange execution-trace state with the behavior system: publish the
        // request while a live-debug panel is open, ingest what last frame's
        // simulated tick reported (pulses, live values, breakpoint hits).
        self.drive_trace(world);
        world.insert_resource(crate::ecs::FlyCam(self.fly && !self.sim.playing()));
        // Publish the editor-session hidden set (manual hides composed with an
        // active isolate, resolved to this world's ids) so the renderer
        // collapses those objects this frame.
        world.insert_resource(crate::ecs::HiddenAssets(self.effective_hidden_ids()));
        // Publish the viewport view mode + show flags for this frame's draw.
        world.insert_resource(crate::ecs::ViewOverrides {
            mode: self.view_mode,
            show: self.show_flags,
        });

        // Re-anchor + recolour the top bar, then lay out (or hide) the panels.
        hud::apply_layout(world, self.hud_state());
        let vp = input.as_ref().map(|i| i.viewport).unwrap_or([0.0, 0.0]);
        let mouse = input
            .as_ref()
            .map(|i| [i.mouse_x, i.mouse_y])
            .unwrap_or([0.0, 0.0]);
        // The panels lay out only once the HUD is shown and a real viewport exists
        // (frame 0 keeps the injected-hidden placeholders).
        let shown = self.hud_visible && vp[0] > 0.0;
        // Publish the world-space editor lines (origin axes + extent outlines)
        // for the renderer's line pass, refilling last frame's buffer.
        // Republished every frame (the renderer expands whatever it finds), and
        // empty while everything is toggled off, the HUD is hidden, or the
        // Lines show flag is cleared (which would mask the pass anyway; the
        // empty buffer also skips the CPU generation and ribbon expansion).
        let mut lines = world
            .remove_resource::<crate::ecs::WorldLines>()
            .map(|l| l.0)
            .unwrap_or_default();
        lines.clear();
        if self.show_flags.contains(view_menu::ShowFlags::LINES) && !self.start_mode {
            self.push_axis_lines(world, &mut lines);
            self.push_extent_lines(world, vp, &mut lines);
        }
        world.insert_resource(crate::ecs::WorldLines(lines));
        // Drive the editor's in-engine cursor. It shows while the editor owns the
        // pointer (edit mode); a captured camera (play / fly) owns the pointer
        // instead, so the sprite hides and no stray arrow lingers over the frozen
        // pointer. Its shape becomes a resize cursor over a resizable panel edge.
        let owns_cursor = vp[0] > 0.0 && !self.sim.playing() && !self.fly;
        cursor::set_visible(world, owns_cursor);
        let shape = if !owns_cursor || !shown {
            CursorShape::Default
        } else if let Some(r) = &self.resize {
            // Keep the resize cursor for the whole drag, even if the pointer
            // drifts off the edge as the panel grows.
            resize::cursor_shape(r.edges)
        } else {
            self.hover_cursor(vp, mouse)
        };
        world.insert_resource(DesiredCursor(shape));
        // Publish the panels' draw layers (focus stack) so the renderer occludes
        // overlaps by focus this frame. Empty while the HUD is hidden, so the
        // renderer skips the overlay sort entirely.
        if shown {
            self.publish_layers(world);
        } else {
            world.insert_resource(HudLayers::default());
        }
        // Lay out every open panel (hiding keeps its state, so toggling back
        // restores the same view). Compound gates live on each panel's
        // `is_open` -- e.g. the edit form shows only while the assets UI is on.
        for p in registry::all() {
            if shown && self.panel_shown(p.key()) {
                let o = self.origin(p.key(), vp);
                p.draw(self, world, o, mouse);
            } else {
                p.hide(world);
            }
        }
        // The billboards sit at the bottom of the editor overlays; the
        // selection rings ride the picked assets' projected bounds above
        // them, under the panels (their ids take the default draw layer);
        // the gizmo and the marquee rect draw over both.
        // The viewport overlays belong to a session editing a world: the start
        // screen shows the previewed world plainly, with nothing over it but
        // the panel, its dialog, and its toasts.
        let overlays = shown && !self.start_mode;
        // The start screen's shot fade sits under all of them, covering the
        // previewed world alone; the loading cover stands over the same area
        // instead while the world behind it is being compiled.
        self.drive_cinematic_draw(world, vp, shown);
        self.drive_loading_draw(world, shown);
        if shown && self.start_mode {
            self.start_drawn = self.start_drawn.saturating_add(1);
        }
        self.drive_billboards(world, vp, overlays && self.show_billboards);
        self.drive_highlight(world, vp, overlays);
        self.drive_gizmo_draw(world, vp, overlays);
        self.drive_marquee_draw(world, overlays);
        self.drive_create_menu_draw(world, overlays, mouse);
        self.drive_display_menu_draw(world, vp, overlays, mouse);
        self.drive_toasts(world, vp, shown, mouse);
        self.drive_modal_draw(world, vp, shown, mouse);
    }

    // Bring the live preview world back in line with the in-memory entries, so
    // any authored change (add / edit / delete / template apply) shows
    // immediately without touching disk (SAVE owns persistence). Run once per
    // frame by the run loop right after `tick`, whenever an edit flagged
    // `rebuild_preview`.
    //
    // An edit that only changed component data is written straight into the
    // running world (`try_live_apply`), which is what most editing is: the
    // renderer draws that data every frame, so it is already the shortest path
    // from the edit to the picture. Everything else recompiles and swaps a
    // fresh world under the running backend, below.
    //
    // The recompiled world is built FIRST, in a throwaway App; only once that
    // succeeds is the backend transplanted out of the live world. So a rebuild
    // failure leaves the live world -- and its window -- fully intact; the next
    // edit retries. The backend is never dropped on an error path.
    fn apply_world_swap(&mut self, app: &mut App) {
        if !self.refresh_preview(app.world_mut()) {
            return;
        }
        // Whatever the user waits behind needs a drawn-and-presented frame
        // before the stall, so the rebuild waits out a countdown (tick draws on
        // the frames between). A session announces itself with a card once a
        // rebuild has measured slow; the start screen sets its own countdown
        // when it stages a preview, since the loading cover is already up and
        // no card belongs over it. Fast session rebuilds (the common case) show
        // nothing and run immediately.
        if !self.start_mode
            && self.last_rebuild_secs > SLOW_REBUILD_SECS
            && self.rebuild_op.is_none()
        {
            self.rebuild_op = Some(self.notifier.begin_op("Rebuilding preview"));
            self.rebuild_countdown = 2;
        }
        if self.rebuild_countdown > 0 {
            self.rebuild_countdown -= 1;
            return;
        }
        self.rebuild_preview = false;
        self.rebuild_required = false;
        let started = std::time::Instant::now();
        self.swap_preview_world(app);
        self.last_rebuild_secs = started.elapsed().as_secs_f32();
        if let Some(op) = self.rebuild_op.take() {
            op.finish();
        }
    }
}

// A preview rebuild slower than this earns the announce card above.
const SLOW_REBUILD_SECS: f32 = 0.15;

impl EditorHook {
    // Bring the live preview back in line with the entries, writing the edit
    // into the running world where that expresses it. `true` when a rebuild is
    // still owed, which is what `apply_world_swap` goes on to do.
    fn refresh_preview(&mut self, world: &mut World) -> bool {
        if !self.rebuild_preview {
            return false;
        }
        if self.try_live_apply(world) {
            self.rebuild_preview = false;
            return false;
        }
        // The rebuild discards a running simulation's state, so the transport
        // honestly drops to Stopped. An edit written into the running world
        // leaves the simulation alone, which is why this belongs here rather
        // than with the edit that recorded it.
        self.sim.on_edit();
        true
    }

    // Write an edit that only changed component data into the running world,
    // instead of rebuilding it. `true` when the whole edit landed; `false`
    // leaves the world untouched for the rebuild to handle.
    //
    // The diff is taken against the entries the running world was built from,
    // not the pre-edit list, so a burst of edits that each declined still
    // applies as one once one of them can.
    fn try_live_apply(&mut self, world: &mut World) -> bool {
        if self.rebuild_required {
            return false;
        }
        if !live::same_assets(&self.world_entries, &self.entries) || !self.seed_world_shadows() {
            return false;
        }
        let shadows = self.world_shadows.as_ref().expect("seeded above");
        let Some(changes) = live::args_changes(&self.world_entries, &self.entries, shadows) else {
            return false;
        };
        let Some(plan) = live::plan(world, &self.entries, &changes) else {
            return false;
        };
        tracing::debug!("editor: {} edit(s) applied live, no rebuild", plan.len());
        live::commit(world, plan);
        self.world_entries = self.entries.clone();
        true
    }

    // The template baselines for the running world, expanding its entry list
    // once to find them if the session has not needed them yet. Every edit that
    // could move a baseline rebuilds, and a rebuild re-seeds these, so the one
    // expansion holds for the rest of the session. `false` when the list does
    // not expand, which sends the edit to the rebuild that will report why.
    fn seed_world_shadows(&mut self) -> bool {
        if self.world_shadows.is_some() {
            return true;
        }
        let Ok(loaded) = Self::cook_entries(&self.world_entries) else {
            return false;
        };
        self.world_shadows = Some(
            loaded
                .shadowed
                .into_iter()
                .map(|s| (s.name, s.args))
                .collect(),
        );
        true
    }

    // The rebuild + backend transplant itself (see `apply_world_swap` for the
    // timing shell around it).
    fn swap_preview_world(&mut self, app: &mut App) {
        let (world, shadows) = match self.build_preview_world() {
            Ok(built) => built,
            Err(e) => {
                tracing::error!("editor: live preview rebuild failed, keeping current world: {e}");
                match self.start_mode {
                    true => self.preview_failed(&e.to_string()),
                    false => self.notifier.error_with(
                        &format!("Preview rebuild failed: {e}"),
                        notify::Action::OpenConsole,
                    ),
                }
                return;
            }
        };
        self.world_entries = self.entries.clone();
        self.world_shadows = Some(shadows);
        let mut staged = App::new();
        staged.load_world(world);
        // Carry the editor's typed text (an open form's name + fields, the combo
        // filter) across the fresh HUD injection so it is not blanked.
        let fields = Self::field_snapshot(app.world());
        super::inject::editor_hud(staged.world_mut());
        Self::restore_fields(staged.world_mut(), &fields);
        staged
            .world_mut()
            .insert_resource(MenuOverride(Some(!self.sim.playing())));

        let Some(backend) = concinnity_engine::ecs::take_render_backend(app.world_mut()) else {
            return;
        };
        staged.world_mut().insert_resource(PendingBackend(backend));
        let new_world = std::mem::replace(staged.world_mut(), World::new());
        app.load_world(new_world);
        if let Err(e) = app.start() {
            tracing::error!("editor: live preview start failed: {e:?}");
            self.notifier.error_with(
                &format!("Preview start failed: {e:?}"),
                notify::Action::OpenConsole,
            );
        }
    }
}