forked from markaren/threepp
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathEditorApp.hpp
More file actions
1495 lines (1417 loc) · 81.4 KB
/
Copy pathEditorApp.hpp
File metadata and controls
1495 lines (1417 loc) · 81.4 KB
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
// The editor application: one window, one document, one selection.
//
// EditorApp owns the platform-facing pieces (canvas, renderer, editor camera,
// gizmo, ImGui context) and the reusable editor core from
// threepp/extras/editor (document, selection, command stack, play controller).
// The panels are member functions split across apps/editor/panels/*.cpp — they
// are all views onto this one object, so keeping them members avoids a web of
// back-pointers.
//
// Editor-only scene content (grid, axes, selection outline, transform gizmo)
// hangs off a single overlay Group that is registered with the SceneDocument
// as editor-only, so it renders but is detached from the scene for the duration
// of every save and every play snapshot.
#ifndef THREEPP_EDITOR_EDITORAPP_HPP
#define THREEPP_EDITOR_EDITORAPP_HPP
#include "FileBrowser.hpp"
#include "VulkanViewPane.hpp"
#ifdef THREEPP_EDITOR_WITH_PYTHON
#include "Scripting.hpp"
#endif
#include "threepp/extras/editor/Command.hpp"
#include "threepp/extras/editor/EditorCommands.hpp"
#include "threepp/extras/editor/EditorSettings.hpp"
#include "threepp/extras/editor/ObjectFactory.hpp"
#include "threepp/extras/editor/PlaySession.hpp"
#include "threepp/extras/editor/RenderConfig.hpp"
#include "threepp/extras/editor/SceneDocument.hpp"
#include "threepp/extras/editor/Selection.hpp"
#include "threepp/animation/AnimationMixer.hpp"
#include "threepp/cameras/OrthographicCamera.hpp"
#include "threepp/cameras/PerspectiveCamera.hpp"
#include "threepp/canvas/Canvas.hpp"
#include "threepp/constants.hpp"
#include "threepp/controls/OrbitControls.hpp"
#include "threepp/controls/TransformControls.hpp"
#include "threepp/core/Raycaster.hpp"
#include "threepp/helpers/Box3Helper.hpp"
#include "threepp/helpers/BoxHelper.hpp"
#include "threepp/input/IOCapture.hpp"
#include "threepp/loaders/Xacro.hpp"
#include "threepp/math/Vector2.hpp"
#include "threepp/objects/Group.hpp"
#include "threepp/objects/InstancedMesh.hpp"
#include "threepp/renderers/Renderer.hpp"
#include <deque>
#include <filesystem>
#include <future>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
class ImguiContext;
namespace threepp {
class Audio;
class AudioListener;
class CameraHelper;
class Line;
class LineBasicMaterial;
class LineSegments;
class Material;
class MeshBasicMaterial;
class ObjectWithMorphTargetInfluences;
class Points;
class Robot;
class Texture;
}// namespace threepp
namespace threepp::editor {
// Held by the app so the collider overlay can reach its PhysX world.
// Forward-declared: PhysicsPlaySession pulls in the whole PhysX SDK, and
// every panel includes this header.
class PhysicsPlaySession;
// Forward-declared for the same reason (it includes PhysicsPlaySession).
class ConveyorPlaySession;
// PhysX-free (the PhysX half is PhysxSensorPlaySession, constructed in
// EditorApp.cpp), but still heavy — it pulls in the depth/lidar sensors and
// the renderer, which the panels have no business recompiling against.
class SensorPlaySession;
// Sounds during Play. Only exists in a THREEPP_WITH_AUDIO build — that
// macro is PUBLIC on the threepp target, so every TU that includes this
// header agrees on whether the member below is there.
class AudioPlaySession;
class EditorApp {
public:
struct Options {
bool vulkan = false;
std::filesystem::path openOnStart;
// Run this many frames and exit (0 = until the window closes).
// Makes the editor scriptable enough for a smoke test.
int maxFrames = 0;
// Drive select/delete/undo through the UI code paths and exit
// non-zero on failure. Diagnostic, not part of the test suite.
bool selfTest = false;
// Press Play as soon as the scene is open. With --frames this
// makes a play session scriptable end to end: open, play, exit.
bool play = false;
// Optional robot for the selftest's URDF pass.
std::filesystem::path urdf;
// Light the scene from this .hdr / .exr on start, as File > Set Environment does.
// A document cannot carry a float environment of its own (its images go through the
// 8-bit ImageLoader), so without this a --screenshot run could never be lit the way
// the app lights it. Set as the background too, matching the menu's default.
std::filesystem::path environment;
// Render the road-acceptance scene to this PNG and exit. Exists so
// a person — or a tool — can LOOK at what the geometry does before
// anyone claims it works.
std::filesystem::path screenshot;
// Open a shipped example on start, by slug (see ExampleScenes.hpp).
// Composes with --screenshot: with a document of its own to shoot,
// --screenshot skips its built-in spline scenario and photographs
// what is loaded, which is what makes an arbitrary scene reviewable
// without a new code path per scene.
std::string example;
// Camera placements for that pass, as position/target pairs. Empty
// means one automatically framed three-quarter view. On the command
// line: --shot=px,py,pz@tx,ty,tz, repeatable.
struct Shot {
Vector3 position;
Vector3 target;
// Suffix on the file name; the first shot writes the bare path.
std::string label;
};
std::vector<Shot> shots;
// Seconds of play before the first shot of that pass.
float settle = 3.f;
// Keys held for that settle, by name ("W", "SPACE", ...), then
// released before the shots. A scene whose whole point is that you
// drive it cannot be reviewed standing still, and pressing a key by
// hand is not something a capture script can do.
std::vector<std::string> keys;
// Keep holding them THROUGH the shots instead of letting go and
// waiting for the scene to settle. What a manoeuvre looks like
// halfway through it — a drone mid-turn, and the chase camera coming
// round with it — is a picture the settled pose cannot show.
bool holdKeys = false;
// Timed pass: warm up, then measure this many frames and print
// median/p95 frame time (plus the Vulkan per-pass GPU breakdown)
// instead of running interactively. Implies vsync OFF — a
// present-capped frame time measures the display, not the renderer.
int bench = 0;
};
// The standard editor viewpoints. `User` is any freely orbited angle;
// the other six are the axis-aligned views every 3D editor puts on the
// numpad. Public because it names the argument of setViewPreset().
enum class ViewPreset {
User,
Front,
Back,
Left,
Right,
Top,
Bottom
};
// "px,py,pz@tx,ty,tz" -> two points. The format a document's
// userData["editorView"] and the command line's --shot both speak,
// parsed in one place so the two cannot drift. False on anything it
// cannot read, leaving both outputs untouched.
[[nodiscard]] static bool parseViewSpec(const std::string& text,
Vector3& position, Vector3& target);
explicit EditorApp(const Options& options);
~EditorApp();
EditorApp(const EditorApp&) = delete;
EditorApp& operator=(const EditorApp&) = delete;
// Runs until the window closes. Returns a process exit code.
int run();
private:
// --- frame ---------------------------------------------------------
void frame(float dt);
void drawUi();
void updateWindowTitle();
// --- panels (apps/editor/panels/*.cpp) ------------------------------
void drawMenuBar();
void drawHierarchy();
void drawInspector();
void drawBottomPanel();
void drawStatusBar();
// Hierarchy helpers
void drawHierarchyNode(Object3D& object);
void drawAddMenu(Object3D& parent);
// Inspector sections
void drawObjectSection(Object3D& object);
void drawTransformSection(Object3D& object);
void drawMaterialSection(Object3D& object);
void drawGeometrySection(Object3D& object);
void drawGeneratorSection(Object3D& object);
// Read-only: what the cloud IS (splats, SH degree), where it came from,
// and the fact that none of it is saved yet. No authoring verbs — a
// scan is imported, looked at and placed, and the placement is the
// transform section's job like everything else's.
void drawSplatSection(Object3D& object);
void drawInstancingSection(Object3D& object);
void drawLightSection(Object3D& object);
void drawLightShadowSection(Object3D& object);
void drawCameraSection(Object3D& object);
void drawAnimationSection(Object3D& object);
void drawJointsSection(Object3D& object);
// Whether Play simulates this robot as a PhysX articulation, written into
// userData["articulation"]. Drawn inside the Robot section, PhysX-free.
void drawArticulationBlock(Object3D& object, Robot& robot);
void drawScriptSection(Object3D& object);
void drawPhysicsSection(Object3D& object);
// Joint authoring: shown for a node carrying a JointConfig (its own
// scene node — the transform is the joint frame, the parent chain is
// body A, the other body is picked here by name). NOT the Robot
// section's joint sliders — that is drawJointsSection above.
void drawJointAuthoringSection(Object3D& object);
// Vehicle authoring: shown for a node carrying a VehicleConfig, and —
// as an invitation — for any node with enough descendant meshes to
// pick four wheels from. Point at the wheels, press Play, drive; the
// geometry is derived from the picks unless overridden. See
// VehicleConfig.
void drawVehicleSection(Object3D& object);
// Sensor authoring: type, rate, seed and the per-type noise model, all
// written into userData["sensor"]. Fields for the types you are not on
// are hidden, never dropped — see SensorConfig. The host gates the type
// list: a Camera hosts the pinhole sensors (its frustum is theirs),
// everything else hosts the rest.
void drawSensorSection(Object3D& object);
// The migration behind the legacy hint in that section: a pinhole
// sensor authored on a plain object moves onto a new camera child whose
// frustum is stamped from the config, in one undo step. The child sits
// at the host's origin with identity rotation, so the aim the host's
// transform encoded is exactly the aim the camera wakes up with.
void moveSensorToCameraChild(Object3D& host);
// Shown for a spline and, in its point form, for one of its control
// points — both are ordinary scene nodes, so the section is what tells
// them apart.
void drawSplineSection(Object3D& object);
// Shown for a node carrying a SoundConfig: the file, the playback
// parameters and — in edit mode only — the audition button.
void drawSoundSection(Object3D& object);
// --- sound audition (edit mode only) --------------------------------
// Hears the authored file without pressing Play. Its own listener and
// its own Audio, rebuilt from the config on every start, and FLAT: it
// answers "is this the right file at the right volume", not "how does
// it sound from over there". The uuid is what identifies the target,
// since a play/stop or a scene load replaces the whole graph.
void startAudition(const Object3D& object);
void stopAudition();
[[nodiscard]] bool isAuditioning(const Object3D& object) const;
// Shown for a text mesh (TextConfig): the content and the type
// parameters, each edit rebuilding the geometry through the same
// undoable property write every other config section uses.
void drawTextSection(Object3D& object);
// Shown for a procedural tree (TreeConfig): the species presets, the
// seed and the generator's parameters. Edits write the CONFIG only —
// the trunk and foliage meshes are derived state that syncTreeOverlays
// regrows to follow it, which is also what makes undo cheap.
void drawTreeSection(Object3D& object);
// The conveyor twin: shown for a conveyor group and, in its waypoint
// form, for one of its waypoints (arc centre / segment surface).
void drawConveyorSection(Object3D& object);
// `owner` is the object the material hangs off; the slot is identified
// by (owner uuid, label) whenever it has to outlive the frame.
void drawTextureSlot(const Object3D& owner, Material& material, const char* label,
const std::shared_ptr<Texture>& current,
const std::function<void(const std::shared_ptr<Texture>&)>& setter,
bool srgb);
// Tiling / offset / rotation, drawn once for the whole material rather
// than once per slot. GL feeds a SINGLE uvTransform uniform for every
// uv1 map (GLMaterials picks the first assigned map by priority and
// copies ITS matrix), so a per-slot transform would silently not render
// there. `textures` is every distinct map on the material and every one
// of them is written.
void drawUvTransformBlock(Material& material,
const std::vector<std::shared_ptr<Texture>>& textures);
// The slot row's "..." button: wrap, filtering, anisotropy and colour
// space for that one texture. Textures are shared instances, so this
// deliberately changes every material using it - there is no
// clone-on-edit.
void drawTextureSettingsPopup(Material& material, const std::shared_ptr<Texture>& texture);
// What the UV transform block writes. The command stores one of these
// PER texture, so undo restores each map's own prior transform - a
// loaded document may well arrive with unequal ones.
struct UvTransform {
Vector2 repeat{1, 1};
Vector2 offset{0, 0};
Vector2 center{0, 0};
float rotation = 0;// radians, as Texture stores it
};
// What the per-slot popup writes, as one value: undo puts the whole
// sampling state back, and each widget names the entry it pushed.
struct TextureSampling {
TextureWrapping wrapS{TextureWrapping::ClampToEdge};
TextureWrapping wrapT{TextureWrapping::ClampToEdge};
Filter minFilter{Filter::LinearMipmapLinear};
Filter magFilter{Filter::Linear};
int anisotropy = 1;
ColorSpace colorSpace{ColorSpace::NoColorSpace};
};
[[nodiscard]] static UvTransform uvTransformOf(const Texture& texture);
[[nodiscard]] static TextureSampling samplingOf(const Texture& texture);
// The undoable writes behind those two, factored out of the widgets so
// the self-test drives exactly what a click drives. `label` names the
// undo entry.
void applyUvTransform(Material& material,
const std::vector<std::shared_ptr<Texture>>& textures,
const UvTransform& after, const char* label);
void applyTextureSampling(Material& material, const std::shared_ptr<Texture>& texture,
const TextureSampling& after, const char* label);
// Thumbnails are cached per texture; drop the lot. Called when the scene
// is replaced, alongside every other cache that points into the old one.
void clearThumbnailCache();
// Assets / console / sensors tabs
void drawAssetsTab();
void drawConsoleTab();
// Live sensor readout (apps/editor/panels/SensorsPanel.cpp): what is
// measuring, what it last read, plots of the scalar channels, and the
// Record toggle. A sensor is invisible without this.
void drawSensorsTab();
// Script Editor (apps/editor/panels/ScriptEditorPanel.cpp): a Scripts
// tab in the bottom panel, holding one inner tab per open script.
//
// Several at once, because editing one object's script while reading
// another's is the normal way to write two things that talk to each
// other. One per object — the object still carries a single script.
struct ScriptEditorState;
// The unsaved marker, the raise-on-selection rule and the
// close-when-the-script-is-gone rule are per-frame state, so they run
// here rather than in a tab body — a collapsed panel, or a tab that is
// not the visible one, must not freeze them. Called before
// drawBottomPanel(), which draws the tabs.
void updateScriptEditors();
// The Scripts tab body: the inner tab bar, and the visible script.
void drawScriptsTab();
void drawScriptTab(ScriptEditorState& state);
// Labels: the outer tab, and one script's inner tab.
[[nodiscard]] std::string scriptsTabLabel() const;
[[nodiscard]] static std::string scriptTabLabel(const ScriptEditorState& state);
// The editor open on `uuid`, or nullptr. Invalidated by any open, so do
// not hold it across one.
[[nodiscard]] ScriptEditorState* scriptEditorFor(const std::string& uuid);
// The script the user is looking at: what Apply, Revert and Ctrl+Enter
// act on. Null with no scripts open.
[[nodiscard]] ScriptEditorState* activeScriptEditor();
// Opens a tab on `object`, or reuses the one already on it. `reveal`
// brings that tab forward (and the bottom panel with it); an external
// session syncing in the background passes false, since it must not
// pull the panel out from under whatever the user was looking at.
//
// Reopening an object that already has a tab keeps whatever was being
// typed — closing a tab is not a decision to throw text away.
void openScriptEditor(const Object3D& object, bool reveal = true);
// Normalizes, syntax-checks and commits the buffer as one undo step.
void applyScriptEditor(ScriptEditorState& state);
// The same, on whichever script is visible.
void applyScriptEditor();
// --- external editing (apps/editor/ExternalScriptEdit.cpp) ----------
// Tier 1: hand a .py to VS Code. No watcher — every Play recompiles the
// file, so a file script is already hot.
void openScriptFileExternally(const std::filesystem::path& file);
// Which inline source an external session is editing. A behaviour script
// and a generator live on the same object under different keys, so the
// session has to know which one it exported — and they commit through
// different paths (the Script Editor tab vs the Generator's own
// property write).
enum class ExternalEditKind {
Script,
Generator
};
// Tier 2: export the inline source to a scratch .py, open it, and poll
// it back in through applyScriptEditor() on every save.
void startExternalEdit(Object3D& object, ExternalEditKind kind = ExternalEditKind::Script);
// The undoable write behind a generator sync. Returns the normalized text
// as committed, which is what the poll compares against next time.
std::string applyGeneratorSource(Object3D& target, const std::string& text);
void stopExternalEdit(const std::string& why = {});
void pollExternalEdit(float dt);
[[nodiscard]] bool externalEditActive() const { return externalEdit_.active; }
[[nodiscard]] bool externalEditActive(const Object3D& object) const;
// Writes `<dir>/.vscode/settings.json` when it is absent, so Pylance
// completes `import threepp` in whatever folder the script lives in.
void ensureScriptWorkspace(const std::filesystem::path& dir);
// Where the threepp type stubs are: $THREEPP_PYTHON_STUBS, else the
// source tree this binary was built from.
[[nodiscard]] static std::filesystem::path pythonStubDir();
// Detached, non-blocking and without a console flash. Suppressed in the
// self-test, which drives everything else about a session.
void launchExternalEditor(const std::filesystem::path& dir,
const std::filesystem::path& file);
// A material slot resolved down to the pointers needed to write it.
// Valid for the frame that built it and no longer — see
// PendingTextureSlot for the form that survives a file dialog.
struct TextureSlotTarget {
Material* material = nullptr;
std::function<void(const std::shared_ptr<Texture>&)> setter;
std::shared_ptr<Texture> current;
std::string slot;
bool srgb = true;
};
// --- editing operations --------------------------------------------
void newScene();
void buildTemplateScene();
void openScene(const std::filesystem::path& path);
// Open one of the scenes compiled into the binary (ExampleScenes.hpp).
// Everything openScene does, minus the two things that are about a file:
// the document keeps no path (so Save prompts Save As) and the slug is
// not a recent file. Framing is added instead, because an example is
// something you asked to LOOK at.
void openExample(const std::string& slug);
// Put the viewport where the whole document is visible, from wherever
// the camera currently stands. focusSelected() with the scene as the
// subject; separate because it must work with nothing selected.
void frameDocument();
// How a document asks to be SEEN when it is opened, off the scene root:
//
// userData["editorView"] "px,py,pz@tx,ty,tz" - camera and orbit target
// userData["editorFollow"] "<object name>" - select it, chase it
//
// Called from the OPEN paths only (openScene, openExample, the file on
// the command line) and never from the scene-replaced listener: Stop
// restores a snapshot, and a Stop that teleports the camera away from
// wherever the user drove it is worse than no framing at all.
//
// Returns whether a view was applied, which is what tells openExample
// whether it still has to frame the document itself. A malformed value
// is a console line and nothing else.
bool applyDocumentView();
// How a document asks to be RENDERED, off the same scene root:
//
// userData["render"] "key=value;key=value" - see RenderConfig
//
// Called from the OPEN paths beside applyDocumentView(), and from New,
// which is a document too. A document that says nothing gets
// renderDefaults_ — NOT whatever the last document left on the renderer,
// which is the difference between opening a scene and inheriting one.
void applyDocumentRender();
void saveScene();
void saveSceneAs(const std::filesystem::path& path);
void importModel(const std::filesystem::path& path);
// Save-time storage choices. Applied to the document and remembered in
// the settings file, so the preference outlives the session.
void setImageStorage(ImageStorage storage);
void setModelStorage(ModelStorage storage);
// Turns the selected linked subtree into ordinary scene content, so a
// save writes it in full. Undoable — it is a userData edit.
void unlinkSelectedAsset();
void setEnvironment(const std::filesystem::path& path, bool alsoBackground);
void clearEnvironment();
// Where a dropped image goes when nothing pointed at a specific slot:
// inferred from the file name, else the material's base colour map.
void assignTextureToSelection(const std::filesystem::path& path);
void assignTextureToSlot(const std::filesystem::path& path);
// Loads `path` in the slot's colour space and assigns it as one
// undoable step. `note` is appended to the console line.
void applyTextureToSlot(const std::filesystem::path& path,
const TextureSlotTarget& target,
const std::string& note);
// Consumes the frame's dropped images against the texture slot rows the
// inspector just drew, hit-testing them at (mouseX, mouseY). Called at
// the end of drawUi() with the cursor position; the position is a
// parameter so the self-test can aim a drop without an OS drag.
void resolveTextureDrops(float mouseX, float mouseY);
// Attaches (or clears, with an empty path) a .py on `object`, as one
// undoable step. Field values already stored for the same file are kept.
void assignScript(Object3D& object, const std::filesystem::path& path);
// The same for an audio file (userData["soundFile"]). Authors a default
// SoundConfig alongside it if the object had none, so a file dropped on
// an ordinary mesh makes it a sound source in one step.
void assignSound(Object3D& object, const std::filesystem::path& path);
// Stores inline source on `object` as one undoable step, clearing any
// file reference — an object carries one script, in one form. Parameter
// values survive an edit to the same inline script and are dropped when
// the form changes, since they belong to the class that exposed them.
void setInlineScript(Object3D& object, const std::string& source, const std::string& label);
// Starting point for "New Inline Script": one class, one exposed field,
// and a header saying what the shape is.
[[nodiscard]] static std::string inlineScriptTemplate();
void addObject(const std::shared_ptr<Object3D>& object, Object3D& parent, const std::string& label);
// Run the generator script `carrier` holds and replace its output with
// what the script built. One undoable step; nothing is committed if the
// script raises, because it fills a detached node that is only attached
// on success. Refused while playing, like every other document edit.
// False (with a console line) when there is no generator, the build has
// no Python, or the script failed.
bool regenerate(Object3D& carrier);
// Remove a generator AND the output it produced, as one undoable step.
void clearGenerator(Object3D& carrier);
// Starting source for a scene generator, offered by the Generator section
// when nothing is authored yet. Deliberately NOT inlineScriptTemplate():
// a behaviour script is a class with update(dt) on one object, an
// authoring script is a module body that builds content.
[[nodiscard]] static std::string generatorTemplate();
// A new control point for `spline`, at `index` among its siblings
// (AddObjectCommand::atEnd appends). Placed midway to the neighbour it
// is inserted next to, or past the end along the last segment, so the
// curve visibly changes. Undoable, and the point becomes the selection
// so the gizmo is already on it.
void addSplinePoint(Object3D& spline, std::size_t index, const std::string& label);
// Same contract for a conveyor's waypoints.
void addConveyorPoint(Object3D& conveyor, std::size_t index, const std::string& label);
// A wall (diverter / side guide) attached to `conveyor`, and a point
// inserted into an existing wall at `index` (AddObjectCommand::atEnd
// appends, extending the last span — the grow-it-point-by-point verb).
// Both undoable, both select the result.
void addConveyorWall(Object3D& conveyor, const std::string& label);
void addConveyorWallPoint(Object3D& wall, std::size_t index, const std::string& label);
void deleteSelected();
void duplicateSelected();
void focusSelected();
void reparent(Object3D& object, Object3D& newParent);
// Undo/redo as editor operations rather than raw stack calls: they are
// document mutations and go through the same Play gate as the rest.
// The self-test drives commands_ directly where it wants the stack.
void undo();
void redo();
// The gate every document-mutating operation passes through. Play runs
// on a snapshot and Stop rebuilds the authored scene from it, so an edit
// made while playing is thrown away on Stop while leaving an undo entry
// rebound against the restored graph. Worse, PlaySession's contract is
// that "the editor does not touch the graph while playing" — a delete
// pulls a node out from under a live PhysX actor. Refusing centrally is
// what makes that a contract rather than a hope; the disabled menu items
// are only the visible half. `what` names the action in the console line.
[[nodiscard]] bool rejectWhilePlaying(const char* what);
// --- selection / picking -------------------------------------------
// `instance` is the InstancedMesh sub-instance the pick landed on, when
// the selection IS an InstancedMesh. It only changes which instance the
// outline boxes — the selection, the gizmo and every edit still address
// the whole object, because one instance is not an Object3D and has
// nothing to carry a transform edit on.
void selectObject(Object3D* object, std::optional<int> instance = std::nullopt);
void refreshSelectionHelpers();
// World-space bounds of `instance` of `mesh`: the geometry's own box
// through matrixWorld * instanceMatrix[instance]. Empty when the index
// is out of range or the geometry has no bounds to take.
[[nodiscard]] static Box3 instanceWorldBox(const InstancedMesh& mesh, int instance);
// --- viewport markers ----------------------------------------------
// Billboarded SVG icons standing in for objects that draw nothing
// (cameras, lights), plus the frustum helper for a selected camera.
// Rebuilds live Robots over the frozen placeholders a loaded document
// leaves behind. See RobotConfig.
void rearticulateRobots(Scene& scene);
// Drives one joint and records the new pose in userData, so the scene
// carries the pose it is showing.
void setJointValue(Robot& robot, std::size_t index, float radians);
void syncViewportMarkers();
void syncCameraHelper();
// Min/max distance circles for the SELECTED positional sound, in the
// same file and for the same reason as the camera frustum: an authored
// falloff is otherwise a pair of numbers with no picture.
void syncSoundRings();
void clearSoundRings();
// Anchor cross + hinge/slide axis for the SELECTED joint node, same
// file and same selected-only rule as the sound rings: the node's
// transform IS the joint frame, and an axis you cannot see is an axis
// authored by trial and error.
void syncJointHelper();
void clearJointHelper();
// Wheel rings for the SELECTED vehicle: a circle of the derived radius
// at each picked wheel, same selected-only rule as the joint helper —
// the picture that says which meshes the config resolved and what
// radius it read off them.
void syncVehicleHelper();
void clearVehicleHelper();
void clearViewportMarkers();
// --- spline overlay (apps/editor/SplineOverlay.cpp) -----------------
// One Line per spline, sampled from the CatmullRomCurve3 its control
// points describe. Editor furniture: it lives under the overlay, so it
// is never saved and never picked.
void syncSplineOverlays();
void clearSplineOverlays();
// --- conveyor overlay (apps/editor/ConveyorOverlay.cpp) -------------
// One Line per conveyor path, plus the derived-group regeneration —
// the conveyor twin of the spline overlay pass.
void syncConveyorOverlays();
void clearConveyorOverlays();
// --- procedural trees (apps/editor/TreeOverlay.cpp) -----------------
// Regrows the trunk and foliage meshes an authored TreeConfig
// describes. No editor furniture of its own: unlike the two passes
// above, everything a tree has to show IS the generated geometry.
void syncTreeOverlays();
void clearTreeOverlays();
// The corner-radius handle: a draggable ball on the selected corner's
// arc midpoint. Interaction runs in the ImGui frame (it reads the same
// mouse state picking does); placement rides syncConveyorOverlays.
// Returns true while a drag owns the mouse, so picking stands down.
bool updateConveyorRadiusDrag();
// The drag core, separated so the selftest can drive it without a
// mouse: maps a world-space ray to a radius via the corner's bisector.
void applyConveyorRadiusDrag(const Vector3& rayOrigin, const Vector3& rayDirection);
void beginConveyorRadiusDrag(const Vector3& rayOrigin, const Vector3& rayDirection);
void endConveyorRadiusDrag();
// --- physics collider overlay (apps/editor/PhysicsDebugOverlay.cpp) --
// PhysX's own debug lines for every collider in the playing world,
// drawn as one LineSegments under the overlay. The answer to "where is
// my collider" being unanswerable without leaving the editor.
void syncPhysicsDebug();
void clearPhysicsDebug();
// --- script debug draw (apps/editor/DebugDrawOverlay.cpp) -----------
// threepp.editor.draw_line and friends: the segments a playing script
// asked to see this frame, drained from scripting::debugDraw() into one
// LineSegments under the overlay. Immediate mode - drained is gone, a
// paused frame keeps the last picture.
void syncDebugDraw();
void clearDebugDraw();
// --- sensor point cloud (apps/editor/SensorOverlay.cpp) --------------
// Every playing depth camera's and LIDAR's returns, as one Points under
// the overlay, coloured by range. Same in-place attribute contract as
// the collider lines above and for the same reason.
void syncSensorOverlay();
void clearSensorOverlay();
// The object a marker stands for, or nullptr when `hit` is not part of
// one. Lets a click on an icon select its owner.
[[nodiscard]] Object3D* markerOwnerOf(Object3D* hit) const;
// The dock the docked camera renders into: the band beside the bottom
// panel, under the inspector. False when there is no room for it (the
// bottom panel is collapsed, or the window is tiny).
[[nodiscard]] bool cameraDockRect(float& x, float& y, float& w, float& h) const;
// The selected object as a Camera, perspective or orthographic. Aims
// the dock when it changes; it is NOT what the dock renders — see
// dockCamera().
[[nodiscard]] Camera* selectedCamera() const;
// The camera the dock renders. Resolved from dockCamera_ every frame
// rather than cached, which is what carries it across a scene replace
// (play/stop rebuilds every camera behind the same uuid) and lets a
// deleted camera fall back to another without any bookkeeping.
[[nodiscard]] Camera* dockCamera() const;
// Every camera in the scene, in hierarchy order: what the dock's picker
// offers, and where dockCamera() finds its default.
[[nodiscard]] std::vector<Camera*> sceneCameras() const;
// Aims the dock. nullptr is the explicit "None" — the dock stays empty
// instead of falling back to the first camera in the scene.
void setDockCamera(Camera* camera);
// The picker in the dock's corner. Drawn as a real ImGui window (the
// rest of the dock is background-drawlist), so it also stops a click on
// the dock from picking through into the scene behind it.
void drawCameraDockPicker();
// Points the dock's Vulkan secondary view at the dock camera and at
// this frame's dock rect. Must run BEFORE Renderer::render(), which is
// where the view is actually recorded and composited. A no-op on OpenGL,
// whose dock is a scissored second render in renderCameraPreview().
void syncCameraDockPane();
// Renders the docked scene camera into that dock; drawUi frames it and
// draws the picker via preview_. On Vulkan the pixels are already there
// (see syncCameraDockPane) and this only fills preview_ in.
void renderCameraPreview();
void pickAt(float mouseX, float mouseY);
[[nodiscard]] Object3D* resolveSelectable(Object3D* hit) const;
// --- viewport camera -------------------------------------------------
// The camera the viewport is currently seen through: the perspective
// one, or the orthographic one while an ortho view is on. Everything
// that projects or unprojects (render, pick, gizmo, markers) goes
// through this rather than naming camera_ directly.
[[nodiscard]] Camera& viewCamera();
// Swaps the projection, preserving what is framed: the ortho frustum is
// sized to what the perspective camera sees at the orbit distance, and
// the reverse on the way back.
void setOrthographic(bool ortho);
[[nodiscard]] bool orthographic() const { return orthographic_; }
// Points the view down a world axis without changing what it looks at.
// `User` only clears the label — an axis view is not a mode, it is a
// place the camera happens to be standing.
void setViewPreset(ViewPreset preset);
[[nodiscard]] ViewPreset viewPreset() const { return viewPreset_; }
[[nodiscard]] static const char* viewPresetLabel(ViewPreset preset);
// Unit vector from the orbit target towards where the camera stands in
// that view. The pole views carry a hair of tilt so `lookAt` and the
// orbit spherical never hit their degenerate case.
[[nodiscard]] static Vector3 viewPresetDirection(ViewPreset preset);
// Drops the label back to `User` once the view has been orbited off its
// axis, and keeps the grid facing the viewer in the axis views.
void updateViewPreset();
void updateGridPlacement();
// Rebuilds the orbit and transform controls against whichever camera is
// active. Both hold a Camera& for life, so switching projection means
// building new ones.
void bindViewportControls();
// --- view gizmo (apps/editor/ViewGizmo.cpp) --------------------------
// The camera-orientation gizmo every 3D editor keeps in a viewport
// corner, after three.js editor's: a ball at each end of the three
// world axes, drawn where the axes actually point. Clicking a ball
// swings the view onto that axis; clicking the axis the camera already
// stands on heads for the far end, the same idiom as Ctrl on the
// numpad keys. Drawn with ImGui's background draw list - no second
// scene, no second render pass, the same picture on either backend.
void drawViewGizmo();
// Starts the swing. The projection is left alone here - the gizmo's
// click handler forces orthographic first, the numpad's policy - so
// the tween itself is usable from either projection.
void startViewTween(ViewPreset preset);
// One frame of it: slerp the orbit direction, keep the distance and
// target. Runs before orbit_->update(), which re-derives its spherical
// from wherever the camera stands - the same contract setViewPreset
// leans on, just spread over a third of a second.
void updateViewTween(float dt);
// --- viewport chrome (apps/editor/ToolPalette.cpp) -------------------
// The controls that used to be a toolbar, as viewport furniture drawn
// with the view gizmo's background-draw-list brush and interaction
// rules. The palette: Select/Move/Rotate/Scale, the space toggle and
// Snap, stacked top-left. The transport: Play/Pause/Stop as a pill
// top-centre. The viewpoint picker: a small real-ImGui window under
// the view gizmo (a combo is not worth reinventing in a draw list).
void drawToolPalette();
void drawTransportBar();
void drawViewpointPicker();
// --- follow selection -----------------------------------------------
// Chase camera. While it is on and something is selected, every frame
// walks the orbit target towards the selection's world position and
// carries the camera with it, keeping the offset the user orbited to —
// in the SUBJECT'S HEADING FRAME, so a body that turns is chased round
// the corner rather than watched flying sideways out of frame. Works in
// both projections (a parallel projection translates, see updateFollow)
// and, above all, while playing — chasing a body the physics is moving
// is the point.
void setFollowSelection(bool follow);
[[nodiscard]] bool followSelection() const { return followSelection_; }
// One frame of that chase. Deselecting pauses it (there is nothing to
// chase) and reselecting resumes; the approach is exponential rather
// than a hard lock, because a hard lock on a physics body reads as
// jitter.
void updateFollow(float dt);
// Sets the ortho frustum to `height` world units tall at the current
// aspect, with zoom reset.
void setOrthoHeight(float height);
// How far back from `from` an ortho camera has to stand to keep the
// whole document in front of its near plane. Ortho framing does not
// depend on the distance, only the clipping does.
[[nodiscard]] float sceneClearDistance(const Vector3& from) const;
// World units per screen pixel at `world`, for whichever projection is
// active. Constant-screen-size overlays size themselves with this.
[[nodiscard]] float viewportWorldPerPixel(const Vector3& world) const;
// --- play ----------------------------------------------------------
void startPlay();
void togglePause();
void stopPlay();
[[nodiscard]] bool isPlaying() const;
// --- vehicle teleop -------------------------------------------------
// While a played scene has vehicles, W/S/A/D and SPACE drive them:
// polled every frame before the sessions step, pushed through
// PhysicsPlaySession::driveVehicles. Only writes controls while a key
// is actually held (plus one release), so a script driving the same
// vehicle through its handle is not overwritten by silence. The
// transmission stays automatic — teleop only ever selects
// forward/reverse.
void updateVehicleTeleop(float dt);
// --- animation preview ---------------------------------------------
// Edit-mode preview of one clip on one subtree. Every touched value
// is recorded up front and put back on stop; no undo entries appear.
void startAnimationPreview(Object3D& root, const std::string& clipName,
bool loop, float speed);
// restore = false discards without writing (the nodes are gone, e.g.
// after a scene replace).
void stopAnimationPreview(bool restore = true);
[[nodiscard]] bool isPreviewing(const Object3D& root) const;
// --- async import --------------------------------------------------
// importModel only enqueues; pollImports runs one background load at
// a time (the loaders share global image-decoder state) and finalizes
// finished ones on the main thread.
void pollImports(float dt);
void drawImportToast();
// The modal that asks for a xacro's declared arguments. Drawn with the
// other dialogs; it has to answer before pollImports can launch the
// worker, which is why the queue entry waits in argPrompt_ meanwhile.
void drawArgPrompt();
void flashStatus(std::string message);
// --- misc ----------------------------------------------------------
int runSelfTest();
int runScreenshot();
// --screenshot over the document that is already open (a scene.json on
// the command line, or --example). Honours --play / --seconds / --shot.
int runSceneScreenshot();
// --bench: a timed pass over whatever is open. Honours --play/--keys,
// warms up for --seconds, then measures --bench frames and prints
// median/p95 CPU frame time and (on Vulkan) the per-pass GPU medians.
int runBench();
// One PNG of whatever the renderer last produced, right way up for the
// backend in use.
bool shootTo(const std::filesystem::path& path);
void handleShortcuts();
void handleFileDrop(const std::vector<std::string>& paths);
void log(const std::string& message);
void logWarnings();
// Says, in the console and the status bar, that any splat cloud in the
// scene is about to be dropped by `action` ("Play", "Save"). Splat
// clouds are not serialized yet, and both operations go through
// ObjectExporter — Play because Stop restores from a snapshot in the
// same format the save file uses. Losing the object is accepted for
// now; losing it silently is not. Returns how many were found.
std::size_t warnAboutSplatClouds(const char* action);
void applyGizmoMode();
// Whether the transform handles belong on screen at all: something is
// selected, the toolbar is not in Select mode, and play is stopped.
// Play runs on a snapshot the simulation owns, so a gizmo inviting a
// transform edit on a moving body is an invitation to nothing — the
// edit would be refused, and dragging handles across a falling crate
// is not a thing anyone means to do. One predicate, because
// applyGizmoMode() and refreshSelectionHelpers() both decide it and a
// gizmo that comes back for one frame is a gizmo that came back.
[[nodiscard]] bool gizmoActive() const;
// Whether the AUTHORING LAYER belongs on screen at all: the selection
// outline, the outlined instance, the marker icons and the selected
// camera's frustum. They are editor concepts — they say what you are
// editing — so Play takes them away with the gizmo and the viewport
// shows the scene, which is what the button is for. Deliberately NOT
// everything the overlay holds: the sensor point cloud is play DATA, the
// collider lines are a debug view that only means anything while
// playing, and the grid and the origin axes are View-menu preferences a
// user set on purpose.
[[nodiscard]] bool authoringVisible() const;
// Applies it to every node that carries it. Called from the frame loop
// and from selectObject rather than toggled once at Play, because
// picking stays live while playing: a selection made mid-play builds
// NEW outline nodes, and they have to arrive hidden.
void applyAuthoringVisibility();
void loadSettings();
void persistSettings();
[[nodiscard]] float scale() const { return contentScale_; }
// Panel sizes in device pixels. The unscaled values are a user
// preference (draggable, persisted); everything that lays out against
// a panel goes through these.
[[nodiscard]] float hierarchyPx() const;
[[nodiscard]] float inspectorPx() const;
// Height of the open bottom panel, clamped to what the window can
// actually spare (bottomHeightLimit()) so shrinking the window cannot
// leave the editor all panel and no viewport.
[[nodiscard]] float bottomPanelPx() const;
// The tab strip that is left when the panel is collapsed.
[[nodiscard]] float collapsedBottomPx() const;
// What the side panels have to keep clear above the status bar: the
// panel plus its splitter when open, the collapsed strip when not.
[[nodiscard]] float bottomBandPx() const;
// Largest bottom panel height, unscaled, for the current window.
[[nodiscard]] float bottomHeightLimit() const;
// The grab strip both splitters below are made of. `sign` is +1 when
// dragging along the axis grows the value, -1 when it shrinks it.
void drawSplitterStrip(const char* id, float x, float top, float width, float height,
bool horizontal, float& value, float sign, float lo, float hi);
// Vertical drag handle beside a side panel. `x` is the strip's left
// edge and `sign` is +1 when dragging right widens the panel (left-hand
// panels), -1 when it narrows it (right-hand panels).
void drawSplitter(const char* id, float x, float top, float height,
float& width, float sign);
// The horizontal twin, along the top edge of the bottom panel: dragging
// up makes it taller.
void drawHeightSplitter(const char* id, float x, float top, float width,
float& height);
// Undo-friendly ImGui helpers: begin a transaction when a widget is
// activated and close it when the edit finishes, so a drag collapses to
// one undo step.
void beginEditIfActivated();
void endEditIfDeactivated();
// --- state ---------------------------------------------------------
Options options_;
Canvas canvas_;
std::unique_ptr<Renderer> renderer_;
// Two cameras, one viewport: only one is ever rendered with, and every
// projection-aware path asks viewCamera() which. They look at the same
// orbit target and hand their framing over on each switch. The ortho
// frustum is sized in world units rather than by a negative near plane,
// so the camera is pushed clear of the scene when an axis view is
// entered — in a parallel projection the distance only sets clipping.
PerspectiveCamera camera_;
OrthographicCamera ortho_;
std::unique_ptr<OrbitControls> orbit_;
bool orthographic_ = false;
ViewPreset viewPreset_ = ViewPreset::User;
// The view gizmo's animated snap. `from` is the unit orbit direction
// at the click; the flight slerps it onto the preset's axis while the
// distance and target stay the orbit's own. Retired by any explicit
// view set (setViewPreset) or by a viewport drag mid-flight.
struct ViewTween {
bool active = false;
float t = 0.f;
Vector3 from;
ViewPreset preset = ViewPreset::Front;
};
ViewTween viewTween_;
// Whether the pointer is on the gizmo this frame. The pick gate and
// the orbit's mouse capture both stand down while it is - the gizmo
// is background furniture ImGui's own capture knows nothing about.
bool viewGizmoHovered_ = false;
// Its twin for the tool palette in the opposite corner, consumed by
// the same two gates (ioCapture_ and the pick), for the same reason.
bool toolPaletteHovered_ = false;
// Follow Selection (View menu, Shift+F). Session state on purpose: it
// belongs to what is open and what is selected, not to the editor's
// saved preferences.
bool followSelection_ = false;
// The heading the chase last placed the camera with: the followed
// object's yaw about world up, exponentially smoothed. Kept as state
// because it is BOTH ends of a frame — the offset is read back out of
// the camera through this angle and written back through the new one,
// which is what lets the user orbit while it follows.
//
// followHeadingFor_ is the subject it belongs to, by uuid: a different
// subject SNAPS (reading the offset through the new heading leaves the
// camera exactly where it stands, so selecting something that happens to
// face east does not fling the view). By uuid rather than by pointer
// because Stop rebuilds the graph, and the same subject across that swap
// is the same chase.
float followHeading_ = 0.f;
std::string followHeadingFor_;
// Whether the document that is open placed the camera itself, through
// userData["editorView"]. Only the --screenshot pass asks: a considered
// vantage is not something to overwrite with an automatic framing.
bool documentView_ = false;
// The renderer as the editor set it up, captured once in the
// constructor. Two jobs, both about keeping a saved document honest: it
// is what a document that carries no render block opens with, and it is
// the baseline a save is written as a difference FROM — so a scene that
// never touched the Renderer Settings panel saves no render block at
// all, and one that dialled in fog saves the fog and nothing else.
RenderConfig renderDefaults_;
SceneDocument document_;
Selection selection_;
CommandStack commands_;
PlayController play_;
EditorSettings settings_;
std::filesystem::path settingsPath_;
// Editor-only scene content.
std::shared_ptr<Group> overlay_;
std::shared_ptr<Object3D> grid_;
std::shared_ptr<Object3D> axes_;
std::shared_ptr<BoxHelper> selectionBox_;
// A selected InstancedMesh outlines the ONE instance that was picked,
// not the whole cloud — a box around 500 scattered rocks says nothing.
// Box3Helper keeps a reference to the box, so instanceBox_ must outlive
// the helper (both are members; the helper is also dropped whenever the
// selection changes) and is refreshed each frame in
// refreshSelectionHelpers so the outline tracks a moving instance.
// Set by the Generator section's button, consumed once at the top of the
// next frame. Regenerate replaces the node the panel is drawing from, and
// the selection re-resolve that follows must not run inside the ImGui tree
// that is reading it. By uuid, because a play/stop in between replaces the
// graph — the same reason scriptTargetUuid_ is one.
std::string pendingRegenerate_;
// Same deferral, for Clear — it removes the output node outright.
std::string pendingGeneratorClear_;
std::optional<int> selectedInstance_;
Box3 instanceBox_;
std::shared_ptr<Box3Helper> instanceOutline_;
std::unique_ptr<TransformControls> gizmo_;
// Marker icons. One node per owner, all parented to markers_, which is
// itself part of the editor-only overlay.
struct ViewportMarker {
Object3D* owner = nullptr;
std::shared_ptr<Object3D> node;
std::vector<std::shared_ptr<MeshBasicMaterial>> materials;
// Which glyph this was built from (ViewportMarkers.cpp's file-local
// Icon, as an int so this header stays free of it). What an object IS
// can change under a live marker — authoring a sensor on a camera
// changes its icon — so the marker is rebuilt when it no longer
// matches rather than showing yesterday's kind.
int icon = -1;
};
std::shared_ptr<Group> markers_;
std::vector<ViewportMarker> viewportMarkers_;
// Frustum of the selected camera. Holds a reference to that camera, so