babylon.quarks-editor 0.17.9 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/galleryPlayback.d.ts +6 -1
- package/dist/core/galleryPlayback.js +30 -1
- package/dist/core/galleryScene.d.ts +8 -0
- package/dist/core/galleryScene.js +24 -0
- package/dist/core/renderDebug.d.ts +8 -0
- package/dist/core/renderDebug.js +42 -0
- package/dist/core/scrub.d.ts +10 -1
- package/dist/core/scrub.js +41 -1
- package/dist/core/timeline.d.ts +2 -1
- package/dist/core/timeline.js +26 -2
- package/dist/react/EffectEditorHost.js +87 -11
- package/dist/react/GalleryPanel.d.ts +1 -0
- package/dist/react/GalleryPanel.js +45 -14
- package/dist/react/TimelinePanel.d.ts +2 -0
- package/dist/react/TimelinePanel.js +30 -13
- package/package.json +1 -1
- package/src/core/galleryPlayback.ts +36 -1
- package/src/core/galleryScene.ts +34 -0
- package/src/core/renderDebug.ts +46 -0
- package/src/core/scrub.ts +44 -1
- package/src/core/timeline.ts +30 -2
- package/src/react/EffectEditorHost.tsx +107 -9
- package/src/react/GalleryPanel.tsx +78 -14
- package/src/react/TimelinePanel.tsx +30 -13
|
@@ -25,21 +25,30 @@ import {GridMaterial} from '@babylonjs/materials/grid/gridMaterial';
|
|
|
25
25
|
import {
|
|
26
26
|
BatchedRenderer,
|
|
27
27
|
ParticleSystem,
|
|
28
|
+
QuarksUtil,
|
|
28
29
|
} from 'babylon.quarks';
|
|
29
30
|
import {EffectBinding} from '../core/binding';
|
|
30
31
|
import {EffectHistory} from '../core/history';
|
|
31
32
|
import {ensureGroundResolver} from '../core/collision';
|
|
32
33
|
import {createGalleryDefaultEntry} from '../core/defaultEffect';
|
|
33
34
|
import {loadEffectFromJson, disposeLoadedEffect, parseEffectFromJson} from '../core/loadEffect';
|
|
34
|
-
import {applyGalleryPlayback, countActivePreviews, countGalleryParticles, hasActivePreview} from '../core/galleryPlayback';
|
|
35
|
-
import {createPlaybackState, getFocusElapsed, isFocusFinished, setFocusElapsed, type PlaybackState} from '../core/playback';
|
|
35
|
+
import {applyGalleryPlayback, countActivePreviews, countGalleryParticles, hasActivePreview, pauseFinishedPreviews} from '../core/galleryPlayback';
|
|
36
|
+
import {createPlaybackState, clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, type PlaybackState} from '../core/playback';
|
|
37
|
+
import {pauseFocus, scrubFocusToSettled} from '../core/scrub';
|
|
38
|
+
import {logRenderMismatch} from '../core/renderDebug';
|
|
36
39
|
import {cancelGalleryCameraFocus, focusGalleryEntry} from '../core/galleryLayout';
|
|
37
40
|
import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
|
|
38
41
|
import {GalleryDropMarker} from '../core/galleryDropMarker';
|
|
39
42
|
import {GallerySelectionMarker} from '../core/gallerySelectionMarker';
|
|
40
43
|
import {EmitterShapeWireframes} from '../core/emitterShapeWireframe';
|
|
41
44
|
import {getGalleryDragSession, setGalleryDragSession} from '../core/galleryDragSession';
|
|
42
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
clearGalleryEntrySimulation,
|
|
47
|
+
pickGroundPosition,
|
|
48
|
+
placeGalleryEntriesAt,
|
|
49
|
+
placeGalleryEntryInScene,
|
|
50
|
+
removeGalleryEntryFromScene,
|
|
51
|
+
} from '../core/galleryScene';
|
|
43
52
|
import {describeGalleryDrag, GALLERY_DRAG_MIME, readGalleryDrag} from '../core/galleryTree';
|
|
44
53
|
import {loadEffectGallery, collectJsonFiles, bindingFromGalleryEntry, mergeGalleryEntries, type GalleryEntry, type GalleryLoadProgress} from '../core/loadEffectGallery';
|
|
45
54
|
import {pickGalleryJsonFiles} from '../core/pickGalleryFiles';
|
|
@@ -135,6 +144,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
135
144
|
const importGalleryRef = useRef<(files: FileList | File[]) => Promise<void>>(async () => {});
|
|
136
145
|
const selectGalleryEntryRef = useRef<(entry: GalleryEntry) => void>(() => {});
|
|
137
146
|
const toggleGalleryPreviewRef = useRef<(entry: GalleryEntry) => void>(() => {});
|
|
147
|
+
const removeGalleryFromSceneRef = useRef<(entry: GalleryEntry) => void>(() => {});
|
|
138
148
|
const placeGalleryDropRef = useRef<(clientX: number, clientY: number, dataTransfer?: DataTransfer) => void>(() => {});
|
|
139
149
|
const createGalleryDefaultRef = useRef<() => void>(() => {});
|
|
140
150
|
const dropMarkerRef = useRef<GalleryDropMarker | null>(null);
|
|
@@ -352,12 +362,14 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
352
362
|
return;
|
|
353
363
|
}
|
|
354
364
|
const id = entry.root.uniqueId;
|
|
365
|
+
const removing = (stateRef.current?.previewRootIds ?? []).includes(id);
|
|
355
366
|
setPreviewRootIds((prev) => {
|
|
356
367
|
const next = new Set(prev);
|
|
357
368
|
if (next.has(id)) {
|
|
358
369
|
next.delete(id);
|
|
359
370
|
} else {
|
|
360
371
|
next.add(id);
|
|
372
|
+
clearFocusFinished(playbackRef.current, id);
|
|
361
373
|
}
|
|
362
374
|
const ids = [...next];
|
|
363
375
|
if (stateRef.current) {
|
|
@@ -365,6 +377,47 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
365
377
|
}
|
|
366
378
|
return ids;
|
|
367
379
|
});
|
|
380
|
+
if (removing) {
|
|
381
|
+
clearGalleryEntrySimulation(entry, renderer);
|
|
382
|
+
clearFocusFinished(playbackRef.current, id);
|
|
383
|
+
}
|
|
384
|
+
renderer.refreshBatches();
|
|
385
|
+
scene.render();
|
|
386
|
+
force();
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const removeGalleryFromScene = (entry: GalleryEntry) => {
|
|
390
|
+
if (!entry.inScene) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const id = entry.root.uniqueId;
|
|
394
|
+
removeGalleryEntryFromScene(entry, renderer, galleryRoot);
|
|
395
|
+
setPreviewRootIds((prev) => {
|
|
396
|
+
const next = prev.filter((rootId) => rootId !== id);
|
|
397
|
+
if (stateRef.current) {
|
|
398
|
+
stateRef.current.previewRootIds = next;
|
|
399
|
+
}
|
|
400
|
+
return next;
|
|
401
|
+
});
|
|
402
|
+
clearFocusFinished(playbackRef.current, id);
|
|
403
|
+
playbackRef.current.focusElapsedByRoot.delete(id);
|
|
404
|
+
galleryEntries = [...galleryEntries];
|
|
405
|
+
if (stateRef.current) {
|
|
406
|
+
stateRef.current.galleryEntries = galleryEntries;
|
|
407
|
+
}
|
|
408
|
+
setGallery(galleryEntries);
|
|
409
|
+
if (stateRef.current?.binding?.root === entry.root) {
|
|
410
|
+
selectionMarker.hide();
|
|
411
|
+
}
|
|
412
|
+
applyGalleryPlayback(
|
|
413
|
+
galleryEntries,
|
|
414
|
+
new Set(stateRef.current?.previewRootIds ?? []),
|
|
415
|
+
playbackRef.current.stagePlaying,
|
|
416
|
+
playbackRef.current.focusFinishedByRoot
|
|
417
|
+
);
|
|
418
|
+
renderer.refreshBatches();
|
|
419
|
+
scene.render();
|
|
420
|
+
force();
|
|
368
421
|
};
|
|
369
422
|
|
|
370
423
|
const addGalleryPreview = (...entries: GalleryEntry[]) => {
|
|
@@ -503,6 +556,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
503
556
|
importGalleryRef.current = appendGalleryFromFiles;
|
|
504
557
|
selectGalleryEntryRef.current = focusCatalogEntry;
|
|
505
558
|
toggleGalleryPreviewRef.current = toggleGalleryPreview;
|
|
559
|
+
removeGalleryFromSceneRef.current = removeGalleryFromScene;
|
|
506
560
|
placeGalleryDropRef.current = placeGalleryDropAt;
|
|
507
561
|
createGalleryDefaultRef.current = appendGalleryDefault;
|
|
508
562
|
|
|
@@ -550,11 +604,41 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
550
604
|
hasActivePreview(snapshot.galleryEntries, previewIds) && (playback.stagePlaying || stepQueued);
|
|
551
605
|
if (simulate) {
|
|
552
606
|
renderer.update(delta);
|
|
553
|
-
|
|
554
|
-
if (
|
|
607
|
+
let needsRefresh = false;
|
|
608
|
+
if (pauseFinishedPreviews(snapshot.galleryEntries, previewIds, playback.focusFinishedByRoot)) {
|
|
609
|
+
needsRefresh = true;
|
|
610
|
+
}
|
|
611
|
+
const binding = snapshot.binding;
|
|
612
|
+
const focusId = playback.focusRootId ?? binding?.root.uniqueId ?? null;
|
|
613
|
+
if (
|
|
614
|
+
focusId != null &&
|
|
615
|
+
binding &&
|
|
616
|
+
playback.stagePlaying &&
|
|
617
|
+
!playback.focusFinishedByRoot.has(focusId) &&
|
|
618
|
+
!focusTimelineMetaRef.current.anyLooping &&
|
|
619
|
+
QuarksUtil.isEffectFinished(binding.root)
|
|
620
|
+
) {
|
|
621
|
+
const settleAt = getFocusElapsed(playback, focusId);
|
|
622
|
+
scrubFocusToSettled(binding, renderer, settleAt);
|
|
623
|
+
markFocusFinished(playback, focusId);
|
|
624
|
+
pauseFocus(binding);
|
|
625
|
+
needsRefresh = true;
|
|
626
|
+
} else if (
|
|
627
|
+
focusId != null &&
|
|
628
|
+
playback.stagePlaying &&
|
|
629
|
+
!playback.focusFinishedByRoot.has(focusId)
|
|
630
|
+
) {
|
|
555
631
|
const prev = getFocusElapsed(playback, focusId);
|
|
556
632
|
setFocusElapsed(playback, focusId, prev + delta);
|
|
557
633
|
}
|
|
634
|
+
if (needsRefresh) {
|
|
635
|
+
renderer.refreshBatches();
|
|
636
|
+
}
|
|
637
|
+
if (snapshot.galleryEntries.length > 0) {
|
|
638
|
+
logRenderMismatch(renderer, countGalleryParticles(snapshot.galleryEntries, previewIds));
|
|
639
|
+
}
|
|
640
|
+
} else if (snapshot && snapshot.galleryEntries.length > 0) {
|
|
641
|
+
renderer.refreshBatches();
|
|
558
642
|
}
|
|
559
643
|
}
|
|
560
644
|
}
|
|
@@ -606,7 +690,9 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
606
690
|
const stageLabel = playback.stagePlaying ? 'playing' : 'paused';
|
|
607
691
|
const timeLabel = meta.anyLooping
|
|
608
692
|
? `${focusElapsed.toFixed(1)}s ⟳`
|
|
609
|
-
:
|
|
693
|
+
: focusEnded
|
|
694
|
+
? `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s (end)`
|
|
695
|
+
: `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s`;
|
|
610
696
|
counterRef.current.textContent = `${playing} preview · ${stageLabel}${focusLabel} · ${timeLabel} · ${count} particles · ${inScene}/${snapshot.galleryEntries.length} in catalog`;
|
|
611
697
|
}
|
|
612
698
|
} else if (binding) {
|
|
@@ -620,7 +706,9 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
620
706
|
const stageLabel = playback.stagePlaying ? 'playing' : 'paused';
|
|
621
707
|
const timeLabel = meta.anyLooping
|
|
622
708
|
? `${focusElapsed.toFixed(1)}s ⟳`
|
|
623
|
-
:
|
|
709
|
+
: focusEnded
|
|
710
|
+
? `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s (end)`
|
|
711
|
+
: `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s`;
|
|
624
712
|
counterRef.current.textContent = `${stageLabel} · ${timeLabel} · ${count} particles`;
|
|
625
713
|
}
|
|
626
714
|
}
|
|
@@ -678,7 +766,15 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
678
766
|
if (!isCatalogEntry) {
|
|
679
767
|
return;
|
|
680
768
|
}
|
|
681
|
-
applyGalleryPlayback(
|
|
769
|
+
applyGalleryPlayback(
|
|
770
|
+
snap.galleryEntries,
|
|
771
|
+
new Set(previewRootIds),
|
|
772
|
+
stagePlaying,
|
|
773
|
+
playbackRef.current.focusFinishedByRoot
|
|
774
|
+
);
|
|
775
|
+
if (!stagePlaying && stateRef.current?.renderer) {
|
|
776
|
+
stateRef.current.renderer.refreshBatches();
|
|
777
|
+
}
|
|
682
778
|
}, [stagePlaying, previewRootIds, gallery]);
|
|
683
779
|
|
|
684
780
|
useEffect(() => {
|
|
@@ -875,7 +971,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
875
971
|
Gallery{gallery.length > 0 ? ` · ${gallery.length}` : ''}
|
|
876
972
|
</div>
|
|
877
973
|
<div style={{fontSize: 11, color: theme.textDim, marginTop: 4, lineHeight: 1.4}}>
|
|
878
|
-
Drag into
|
|
974
|
+
Drag into viewport to place · ✓ preview · × remove from stage
|
|
879
975
|
</div>
|
|
880
976
|
</div>
|
|
881
977
|
<div style={{flex: 1, minHeight: 0, overflow: 'hidden', padding: '8px 12px 14px', display: 'flex', flexDirection: 'column'}}>
|
|
@@ -887,6 +983,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
887
983
|
folderInputRef={galleryFolderInputRef}
|
|
888
984
|
onFocus={(entry) => selectGalleryEntryRef.current(entry)}
|
|
889
985
|
onTogglePreview={(entry) => toggleGalleryPreviewRef.current(entry)}
|
|
986
|
+
onRemoveFromScene={(entry) => removeGalleryFromSceneRef.current(entry)}
|
|
890
987
|
onImportFiles={(files) => void runGalleryImport(files)}
|
|
891
988
|
onImportFolder={() => void handleImportFolder()}
|
|
892
989
|
onCreateDefault={() => createGalleryDefaultRef.current()}
|
|
@@ -1134,6 +1231,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
|
|
|
1134
1231
|
selectedNode={selectedNode}
|
|
1135
1232
|
onSelectNode={setSelectedNode}
|
|
1136
1233
|
scene={state.scene}
|
|
1234
|
+
renderer={state.renderer}
|
|
1137
1235
|
playbackRef={playbackRef}
|
|
1138
1236
|
playheadRef={playheadRef}
|
|
1139
1237
|
counterRef={counterRef}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
FolderIcon,
|
|
8
8
|
FolderOpenIcon,
|
|
9
9
|
SparklesIcon,
|
|
10
|
+
XMarkIcon,
|
|
10
11
|
} from '@heroicons/react/24/solid';
|
|
11
12
|
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
12
13
|
import type {GalleryEntry, GalleryLoadProgress} from '../core/loadEffectGallery';
|
|
@@ -34,6 +35,7 @@ export interface GalleryPanelProps {
|
|
|
34
35
|
folderInputRef: React.RefObject<HTMLInputElement | null>;
|
|
35
36
|
onFocus: (entry: GalleryEntry) => void;
|
|
36
37
|
onTogglePreview: (entry: GalleryEntry) => void;
|
|
38
|
+
onRemoveFromScene: (entry: GalleryEntry) => void;
|
|
37
39
|
onImportFiles: (files: FileList | File[]) => void;
|
|
38
40
|
onImportFolder: () => void;
|
|
39
41
|
onCreateDefault: () => void;
|
|
@@ -55,9 +57,18 @@ function EffectRow(props: {
|
|
|
55
57
|
inPreview: boolean;
|
|
56
58
|
onFocus: (entry: GalleryEntry) => void;
|
|
57
59
|
onTogglePreview: (entry: GalleryEntry) => void;
|
|
60
|
+
onRemoveFromScene: (entry: GalleryEntry) => void;
|
|
58
61
|
}): React.ReactElement {
|
|
59
|
-
const {entry, depth, focused, inPreview, onFocus, onTogglePreview} = props;
|
|
62
|
+
const {entry, depth, focused, inPreview, onFocus, onTogglePreview, onRemoveFromScene} = props;
|
|
60
63
|
const isLooping = entry.systems.some((system) => system.looping);
|
|
64
|
+
const parkedOnStage = entry.inScene && !inPreview;
|
|
65
|
+
const rowBackground = focused
|
|
66
|
+
? 'rgba(120, 165, 255, 0.22)'
|
|
67
|
+
: inPreview
|
|
68
|
+
? 'rgba(120, 165, 255, 0.1)'
|
|
69
|
+
: parkedOnStage
|
|
70
|
+
? 'rgba(120, 165, 255, 0.04)'
|
|
71
|
+
: 'transparent';
|
|
61
72
|
return (
|
|
62
73
|
<div
|
|
63
74
|
style={{
|
|
@@ -66,7 +77,7 @@ function EffectRow(props: {
|
|
|
66
77
|
width: '100%',
|
|
67
78
|
height: GALLERY_ROW_HEIGHT,
|
|
68
79
|
borderBottom: `1px solid ${theme.border}`,
|
|
69
|
-
background:
|
|
80
|
+
background: rowBackground,
|
|
70
81
|
boxSizing: 'border-box',
|
|
71
82
|
}}
|
|
72
83
|
>
|
|
@@ -74,7 +85,11 @@ function EffectRow(props: {
|
|
|
74
85
|
<button
|
|
75
86
|
type="button"
|
|
76
87
|
className="qe-hover"
|
|
77
|
-
title={
|
|
88
|
+
title={
|
|
89
|
+
inPreview
|
|
90
|
+
? 'Remove from stage preview — stops group playback and clears particles; effect stays on stage'
|
|
91
|
+
: 'Add to stage preview — plays with Stage Play/Pause'
|
|
92
|
+
}
|
|
78
93
|
style={{
|
|
79
94
|
flexShrink: 0,
|
|
80
95
|
width: 28,
|
|
@@ -93,7 +108,11 @@ function EffectRow(props: {
|
|
|
93
108
|
onTogglePreview(entry);
|
|
94
109
|
}}
|
|
95
110
|
>
|
|
96
|
-
{inPreview ?
|
|
111
|
+
{inPreview ? (
|
|
112
|
+
<CheckCircleIcon style={iconStyle(14)} />
|
|
113
|
+
) : (
|
|
114
|
+
<span style={{width: 10, height: 10, border: `1px solid currentColor`, borderRadius: 2, boxSizing: 'border-box'}} />
|
|
115
|
+
)}
|
|
97
116
|
</button>
|
|
98
117
|
) : (
|
|
99
118
|
<span style={{width: 28, flexShrink: 0, borderRight: `1px solid ${theme.border}`}} />
|
|
@@ -104,8 +123,8 @@ function EffectRow(props: {
|
|
|
104
123
|
className="qe-hover-bg"
|
|
105
124
|
title={
|
|
106
125
|
entry.inScene
|
|
107
|
-
? `${entry.path} —
|
|
108
|
-
: `${entry.path} — drag into viewport`
|
|
126
|
+
? `${entry.path} — on stage${inPreview ? ' · previewing' : ' · parked'} · Ctrl+click toggles preview`
|
|
127
|
+
: `${entry.path} — catalog only · drag into viewport to place`
|
|
109
128
|
}
|
|
110
129
|
style={{
|
|
111
130
|
display: 'flex',
|
|
@@ -118,7 +137,7 @@ function EffectRow(props: {
|
|
|
118
137
|
border: 'none',
|
|
119
138
|
background: 'transparent',
|
|
120
139
|
color: focused ? theme.text : theme.textDim,
|
|
121
|
-
padding: `0
|
|
140
|
+
padding: `0 8px 0 ${8 + depth * 14}px`,
|
|
122
141
|
fontSize: 12.5,
|
|
123
142
|
cursor: 'grab',
|
|
124
143
|
fontFamily: theme.font,
|
|
@@ -133,16 +152,56 @@ function EffectRow(props: {
|
|
|
133
152
|
onDragStart={(e) => beginGalleryDrag(e, {kind: 'effect', id: entry.root.uniqueId})}
|
|
134
153
|
onDragEnd={endGalleryDrag}
|
|
135
154
|
>
|
|
136
|
-
<span
|
|
137
|
-
|
|
155
|
+
<span
|
|
156
|
+
style={{
|
|
157
|
+
width: 14,
|
|
158
|
+
flexShrink: 0,
|
|
159
|
+
opacity: entry.inScene ? 1 : 0.35,
|
|
160
|
+
color: inPreview ? theme.accent : entry.inScene ? theme.text : theme.textDim,
|
|
161
|
+
display: 'inline-flex',
|
|
162
|
+
}}
|
|
163
|
+
title={entry.inScene ? 'On stage' : 'Catalog only'}
|
|
164
|
+
>
|
|
165
|
+
<SparklesIcon style={iconStyle(14)} />
|
|
138
166
|
</span>
|
|
139
167
|
<span style={{overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{entry.name}</span>
|
|
168
|
+
{parkedOnStage && (
|
|
169
|
+
<span title="On stage but not in preview" style={{flexShrink: 0, fontSize: 10, color: theme.textDim, letterSpacing: 0.3}}>
|
|
170
|
+
parked
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
140
173
|
{isLooping && (
|
|
141
174
|
<span title="Looping effect" style={{flexShrink: 0, fontSize: 11, color: theme.textDim}}>
|
|
142
175
|
⟳
|
|
143
176
|
</span>
|
|
144
177
|
)}
|
|
145
178
|
</button>
|
|
179
|
+
{entry.inScene ? (
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
className="qe-hover"
|
|
183
|
+
title="Remove from scene — returns to catalog; drag into viewport to place again"
|
|
184
|
+
style={{
|
|
185
|
+
flexShrink: 0,
|
|
186
|
+
width: 28,
|
|
187
|
+
border: 'none',
|
|
188
|
+
borderLeft: `1px solid ${theme.border}`,
|
|
189
|
+
background: 'transparent',
|
|
190
|
+
color: theme.textDim,
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
display: 'inline-flex',
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
justifyContent: 'center',
|
|
195
|
+
padding: 0,
|
|
196
|
+
}}
|
|
197
|
+
onClick={(e) => {
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
onRemoveFromScene(entry);
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<XMarkIcon style={iconStyle(13)} />
|
|
203
|
+
</button>
|
|
204
|
+
) : null}
|
|
146
205
|
</div>
|
|
147
206
|
);
|
|
148
207
|
}
|
|
@@ -218,9 +277,10 @@ function VirtualGalleryTree(props: {
|
|
|
218
277
|
previewRootIds: ReadonlySet<number>;
|
|
219
278
|
onFocus: (entry: GalleryEntry) => void;
|
|
220
279
|
onTogglePreview: (entry: GalleryEntry) => void;
|
|
280
|
+
onRemoveFromScene: (entry: GalleryEntry) => void;
|
|
221
281
|
onToggleFolder: (path: string) => void;
|
|
222
282
|
}): React.ReactElement {
|
|
223
|
-
const {rows, closedFolders, focusedRoot, previewRootIds, onFocus, onTogglePreview, onToggleFolder} = props;
|
|
283
|
+
const {rows, closedFolders, focusedRoot, previewRootIds, onFocus, onTogglePreview, onRemoveFromScene, onToggleFolder} = props;
|
|
224
284
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
225
285
|
const virtualizer = useVirtualizer({
|
|
226
286
|
count: rows.length,
|
|
@@ -273,6 +333,7 @@ function VirtualGalleryTree(props: {
|
|
|
273
333
|
inPreview={previewRootIds.has(row.node.entry.root.uniqueId)}
|
|
274
334
|
onFocus={onFocus}
|
|
275
335
|
onTogglePreview={onTogglePreview}
|
|
336
|
+
onRemoveFromScene={onRemoveFromScene}
|
|
276
337
|
/>
|
|
277
338
|
)}
|
|
278
339
|
</div>
|
|
@@ -285,7 +346,7 @@ function VirtualGalleryTree(props: {
|
|
|
285
346
|
|
|
286
347
|
/** Folder-tree catalog; import effects here and drag them into the viewport. */
|
|
287
348
|
export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
|
|
288
|
-
const {entries, focusedRoot, previewRootIds, loading, folderInputRef, onFocus, onTogglePreview, onImportFiles, onImportFolder, onCreateDefault} = props;
|
|
349
|
+
const {entries, focusedRoot, previewRootIds, loading, folderInputRef, onFocus, onTogglePreview, onRemoveFromScene, onImportFiles, onImportFolder, onCreateDefault} = props;
|
|
289
350
|
const [query, setQuery] = useState('');
|
|
290
351
|
const [closedFolders, setClosedFolders] = useState<Set<string>>(() => new Set());
|
|
291
352
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -394,10 +455,12 @@ export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
|
|
|
394
455
|
<div style={{fontSize: 12, color: theme.textDim, lineHeight: 1.45}}>
|
|
395
456
|
{isLoading
|
|
396
457
|
? `Loading… ${loading.done} / ${loading.total || '…'} files`
|
|
397
|
-
: `${previewCount} previewing · ${inSceneCount}
|
|
458
|
+
: `${previewCount} previewing · ${inSceneCount} on stage · ${entries.length} in catalog`}
|
|
398
459
|
</div>
|
|
399
|
-
<div style={{fontSize: 11, color: theme.textDim, lineHeight: 1.
|
|
400
|
-
|
|
460
|
+
<div style={{fontSize: 11, color: theme.textDim, lineHeight: 1.45}}>
|
|
461
|
+
✓ stage preview · ✨ on stage · drag to place · × remove from stage
|
|
462
|
+
<br />
|
|
463
|
+
Click = edit · Ctrl+click = toggle preview
|
|
401
464
|
</div>
|
|
402
465
|
<input
|
|
403
466
|
className="qe-hover"
|
|
@@ -445,6 +508,7 @@ export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
|
|
|
445
508
|
previewRootIds={previewRootIds}
|
|
446
509
|
onFocus={onFocus}
|
|
447
510
|
onTogglePreview={onTogglePreview}
|
|
511
|
+
onRemoveFromScene={onRemoveFromScene}
|
|
448
512
|
onToggleFolder={toggleFolder}
|
|
449
513
|
/>
|
|
450
514
|
)}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
PlayIcon,
|
|
7
7
|
RectangleGroupIcon,
|
|
8
8
|
} from '@heroicons/react/24/solid';
|
|
9
|
-
import type {ParticleSystem} from 'babylon.quarks';
|
|
9
|
+
import type {ParticleSystem, BatchedRenderer} from 'babylon.quarks';
|
|
10
10
|
import type {Scene} from '@babylonjs/core/scene';
|
|
11
11
|
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
12
12
|
import type {EffectBinding} from '../core/binding';
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type PlaybackState,
|
|
20
20
|
} from '../core/playback';
|
|
21
21
|
import {groupNodes} from '../core/groups';
|
|
22
|
-
import {scrubFocusTo} from '../core/scrub';
|
|
22
|
+
import {pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled} from '../core/scrub';
|
|
23
23
|
import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
|
|
24
24
|
import {TimelineTrackRow} from './TimelineTrackRow';
|
|
25
25
|
import {iconStyle} from './icons';
|
|
@@ -32,6 +32,7 @@ export interface TimelinePanelProps {
|
|
|
32
32
|
selectedNode: TransformNode | null;
|
|
33
33
|
onSelectNode: (node: TransformNode | null) => void;
|
|
34
34
|
scene: Scene;
|
|
35
|
+
renderer: BatchedRenderer;
|
|
35
36
|
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
36
37
|
playheadRef: React.RefObject<HTMLDivElement>;
|
|
37
38
|
counterRef: React.RefObject<HTMLSpanElement>;
|
|
@@ -63,7 +64,7 @@ function pickTickInterval(pxPerSecond: number): number {
|
|
|
63
64
|
* apply only to the focused effect without stopping other previews.
|
|
64
65
|
*/
|
|
65
66
|
export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
66
|
-
const {binding, selectedNode, onSelectNode, scene, playbackRef, playheadRef, counterRef, stagePlaying, setStagePlaying, speed, setSpeed, playbackEnabled = true} = props;
|
|
67
|
+
const {binding, selectedNode, onSelectNode, scene, renderer, playbackRef, playheadRef, counterRef, stagePlaying, setStagePlaying, speed, setSpeed, playbackEnabled = true} = props;
|
|
67
68
|
|
|
68
69
|
const [bodyWidthPx, setBodyWidthPx] = useState(600);
|
|
69
70
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<TransformNode>>(() => new Set());
|
|
@@ -144,13 +145,19 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
144
145
|
let shown = elapsed;
|
|
145
146
|
if (anyLooping) {
|
|
146
147
|
shown = timelineSpan > 0 ? elapsed % timelineSpan : 0;
|
|
147
|
-
} else if (
|
|
148
|
+
} else if (isFocusFinished(state, focusRootId)) {
|
|
149
|
+
// Park at real simulation time — core may finish before the ruler's worst-case span.
|
|
150
|
+
shown = elapsed;
|
|
151
|
+
} else if (elapsed >= timelineSpan) {
|
|
148
152
|
shown = timelineSpan;
|
|
149
153
|
const justFinishedPlaying =
|
|
150
|
-
!state.scrubbing && state.stagePlaying && prevElapsedRef.current < timelineSpan
|
|
154
|
+
!state.scrubbing && state.stagePlaying && prevElapsedRef.current < timelineSpan;
|
|
151
155
|
if (justFinishedPlaying) {
|
|
156
|
+
scrubFocusToSettled(binding, renderer, timelineSpan);
|
|
152
157
|
markFocusFinished(state, focusRootId);
|
|
153
158
|
setFocusElapsed(state, focusRootId, timelineSpan);
|
|
159
|
+
pauseFocus(binding);
|
|
160
|
+
scene.render();
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
prevElapsedRef.current = elapsed;
|
|
@@ -161,7 +168,7 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
161
168
|
};
|
|
162
169
|
raf = requestAnimationFrame(tick);
|
|
163
170
|
return () => cancelAnimationFrame(raf);
|
|
164
|
-
}, [pxPerSecond, timelineSpan, anyLooping, playbackRef, playheadRef, focusRootId, playbackEnabled]);
|
|
171
|
+
}, [pxPerSecond, timelineSpan, anyLooping, playbackRef, playheadRef, focusRootId, playbackEnabled, binding, scene, renderer]);
|
|
165
172
|
|
|
166
173
|
const visibleRows = useMemo(() => {
|
|
167
174
|
let hideUntilDepth: number | null = null;
|
|
@@ -197,16 +204,23 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
197
204
|
};
|
|
198
205
|
|
|
199
206
|
const applyScrub = (time: number) => {
|
|
200
|
-
scrubFocusTo(binding, time);
|
|
201
207
|
const state = playbackRef.current;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
const atEnd = !anyLooping && time >= timelineSpan;
|
|
209
|
+
const shown = atEnd ? timelineSpan : time;
|
|
210
|
+
if (atEnd) {
|
|
211
|
+
scrubFocusToSettled(binding, renderer, timelineSpan);
|
|
206
212
|
markFocusFinished(state, focusRootId);
|
|
213
|
+
pauseFocus(binding);
|
|
214
|
+
} else {
|
|
215
|
+
scrubFocusTo(binding, renderer, time);
|
|
216
|
+
clearFocusFinished(state, focusRootId);
|
|
217
|
+
if (stagePlaying) {
|
|
218
|
+
playFocus(binding);
|
|
219
|
+
}
|
|
207
220
|
}
|
|
221
|
+
setFocusElapsed(state, focusRootId, shown);
|
|
208
222
|
if (playheadRef.current) {
|
|
209
|
-
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET +
|
|
223
|
+
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + shown * pxPerSecond}px`;
|
|
210
224
|
}
|
|
211
225
|
scene.render();
|
|
212
226
|
};
|
|
@@ -223,10 +237,13 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
|
|
|
223
237
|
};
|
|
224
238
|
|
|
225
239
|
const restartFocus = () => {
|
|
226
|
-
scrubFocusTo(binding, 0);
|
|
240
|
+
scrubFocusTo(binding, renderer, 0);
|
|
227
241
|
const state = playbackRef.current;
|
|
228
242
|
setFocusElapsed(state, focusRootId, 0);
|
|
229
243
|
clearFocusFinished(state, focusRootId);
|
|
244
|
+
if (stagePlaying) {
|
|
245
|
+
playFocus(binding);
|
|
246
|
+
}
|
|
230
247
|
if (playheadRef.current) {
|
|
231
248
|
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET}px`;
|
|
232
249
|
}
|