babylon.quarks-editor 0.17.8 → 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/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/dist/core/binding.js +2 -5
- package/dist/core/defaultEffect.d.ts +12 -0
- package/dist/core/defaultEffect.js +43 -0
- package/dist/core/editorScene.d.ts +7 -0
- package/dist/core/editorScene.js +11 -0
- package/dist/core/effectTree.js +2 -1
- package/dist/core/emitterShapeWireframe.d.ts +20 -0
- package/dist/core/emitterShapeWireframe.js +329 -0
- package/dist/core/galleryDragSession.d.ts +4 -0
- package/dist/core/galleryDragSession.js +8 -0
- package/dist/core/galleryDropMarker.d.ts +13 -0
- package/dist/core/galleryDropMarker.js +42 -0
- package/dist/core/galleryLayout.d.ts +20 -0
- package/dist/core/galleryLayout.js +68 -0
- package/dist/core/galleryPlayback.d.ts +14 -0
- package/dist/core/galleryPlayback.js +72 -0
- package/dist/core/galleryScene.d.ts +20 -0
- package/dist/core/galleryScene.js +65 -0
- package/dist/core/gallerySelectionMarker.d.ts +25 -0
- package/dist/core/gallerySelectionMarker.js +69 -0
- package/dist/core/galleryTree.d.ts +49 -0
- package/dist/core/galleryTree.js +138 -0
- package/dist/core/loadEffect.d.ts +13 -0
- package/dist/core/loadEffect.js +31 -17
- package/dist/core/loadEffectGallery.d.ts +36 -0
- package/dist/core/loadEffectGallery.js +97 -0
- package/dist/core/material.d.ts +13 -0
- package/dist/core/material.js +68 -0
- package/dist/core/pickGalleryFiles.d.ts +5 -0
- package/dist/core/pickGalleryFiles.js +42 -0
- package/dist/core/playback.d.ts +23 -0
- package/dist/core/playback.js +32 -0
- package/dist/core/renderDebug.d.ts +8 -0
- package/dist/core/renderDebug.js +42 -0
- package/dist/core/scrub.d.ts +14 -0
- package/dist/core/scrub.js +70 -0
- package/dist/core/timeline.d.ts +4 -1
- package/dist/core/timeline.js +30 -3
- package/dist/core/transformStore.d.ts +3 -0
- package/dist/core/transformStore.js +16 -0
- package/dist/core/viewportPicker.d.ts +25 -0
- package/dist/core/viewportPicker.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/react/EffectEditor.js +1 -4
- package/dist/react/EffectEditorHost.d.ts +7 -0
- package/dist/react/EffectEditorHost.js +745 -115
- package/dist/react/GalleryPanel.d.ts +20 -0
- package/dist/react/GalleryPanel.js +246 -0
- package/dist/react/InspectorInfoIcon.d.ts +7 -0
- package/dist/react/InspectorInfoIcon.js +22 -0
- package/dist/react/InspectorLabel.d.ts +7 -0
- package/dist/react/InspectorLabel.js +12 -0
- package/dist/react/MessageDialog.d.ts +9 -0
- package/dist/react/MessageDialog.js +24 -0
- package/dist/react/ModuleSection.d.ts +2 -0
- package/dist/react/ModuleSection.js +8 -1
- package/dist/react/ObjectModule.js +14 -2
- package/dist/react/TimelinePanel.d.ts +8 -11
- package/dist/react/TimelinePanel.js +129 -98
- package/dist/react/TimelineTrackRow.js +29 -15
- package/dist/react/Tooltip.d.ts +6 -0
- package/dist/react/Tooltip.js +65 -0
- package/dist/react/ValueField.d.ts +2 -0
- package/dist/react/ValueField.js +1 -1
- package/dist/react/behaviorModules.js +8 -8
- package/dist/react/fields.d.ts +2 -0
- package/dist/react/fields.js +14 -5
- package/dist/react/icons.d.ts +7 -0
- package/dist/react/icons.js +10 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +2 -0
- package/dist/react/inspectorHints.d.ts +2 -0
- package/dist/react/inspectorHints.js +138 -0
- package/dist/react/modules.js +109 -81
- package/dist/react/theme.d.ts +2 -0
- package/dist/react/theme.js +53 -1
- package/package.json +8 -4
- package/src/core/binding.ts +2 -5
- package/src/core/defaultEffect.ts +74 -0
- package/src/core/editorScene.ts +14 -0
- package/src/core/effectTree.ts +2 -1
- package/src/core/emitterShapeWireframe.ts +373 -0
- package/src/core/galleryDragSession.ts +12 -0
- package/src/core/galleryDropMarker.ts +57 -0
- package/src/core/galleryLayout.ts +90 -0
- package/src/core/galleryPlayback.ts +90 -0
- package/src/core/galleryScene.ts +92 -0
- package/src/core/gallerySelectionMarker.ts +84 -0
- package/src/core/galleryTree.ts +187 -0
- package/src/core/loadEffect.ts +55 -18
- package/src/core/loadEffectGallery.ts +146 -0
- package/src/core/material.ts +84 -0
- package/src/core/pickGalleryFiles.ts +58 -0
- package/src/core/playback.ts +49 -0
- package/src/core/renderDebug.ts +46 -0
- package/src/core/scrub.ts +76 -1
- package/src/core/timeline.ts +34 -3
- package/src/core/transformStore.ts +19 -0
- package/src/core/viewportPicker.ts +114 -0
- package/src/index.ts +2 -0
- package/src/react/EffectEditor.tsx +5 -7
- package/src/react/EffectEditorHost.tsx +936 -142
- package/src/react/GalleryPanel.tsx +517 -0
- package/src/react/InspectorInfoIcon.tsx +36 -0
- package/src/react/InspectorLabel.tsx +18 -0
- package/src/react/MessageDialog.tsx +57 -0
- package/src/react/ModuleSection.tsx +17 -2
- package/src/react/ObjectModule.tsx +17 -6
- package/src/react/TimelinePanel.tsx +227 -174
- package/src/react/TimelineTrackRow.tsx +58 -23
- package/src/react/Tooltip.tsx +93 -0
- package/src/react/ValueField.tsx +3 -1
- package/src/react/behaviorModules.tsx +13 -8
- package/src/react/fields.tsx +15 -6
- package/src/react/icons.ts +19 -0
- package/src/react/index.ts +4 -0
- package/src/react/inspectorHints.ts +150 -0
- package/src/react/modules.tsx +198 -183
- package/src/react/theme.ts +54 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useReducer, useRef, useState, useCallback } from 'react';
|
|
3
3
|
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { ArrowLeftIcon, ArrowPathIcon, ArrowUturnLeftIcon, ArrowUturnRightIcon, ArrowsPointingOutIcon, CubeTransparentIcon, CursorArrowRaysIcon, Square2StackIcon, } from '@heroicons/react/24/solid';
|
|
5
|
+
import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents';
|
|
4
6
|
import { Engine } from '@babylonjs/core/Engines/engine';
|
|
5
7
|
import { Scene } from '@babylonjs/core/scene';
|
|
6
8
|
import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
|
|
@@ -12,55 +14,45 @@ import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
|
12
14
|
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
|
|
13
15
|
import { UtilityLayerRenderer } from '@babylonjs/core/Rendering/utilityLayerRenderer';
|
|
14
16
|
import { GridMaterial } from '@babylonjs/materials/grid/gridMaterial';
|
|
15
|
-
import { BatchedRenderer,
|
|
17
|
+
import { BatchedRenderer, QuarksUtil, } from 'babylon.quarks';
|
|
16
18
|
import { EffectBinding } from '../core/binding';
|
|
17
19
|
import { EffectHistory } from '../core/history';
|
|
18
20
|
import { ensureGroundResolver } from '../core/collision';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
+
import { createGalleryDefaultEntry } from '../core/defaultEffect';
|
|
22
|
+
import { loadEffectFromJson, disposeLoadedEffect, parseEffectFromJson } from '../core/loadEffect';
|
|
23
|
+
import { applyGalleryPlayback, countActivePreviews, countGalleryParticles, hasActivePreview, pauseFinishedPreviews } from '../core/galleryPlayback';
|
|
24
|
+
import { createPlaybackState, clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed } from '../core/playback';
|
|
25
|
+
import { pauseFocus, scrubFocusToSettled } from '../core/scrub';
|
|
26
|
+
import { logRenderMismatch } from '../core/renderDebug';
|
|
27
|
+
import { cancelGalleryCameraFocus, focusGalleryEntry } from '../core/galleryLayout';
|
|
28
|
+
import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
|
|
29
|
+
import { GalleryDropMarker } from '../core/galleryDropMarker';
|
|
30
|
+
import { GallerySelectionMarker } from '../core/gallerySelectionMarker';
|
|
31
|
+
import { EmitterShapeWireframes } from '../core/emitterShapeWireframe';
|
|
32
|
+
import { getGalleryDragSession, setGalleryDragSession } from '../core/galleryDragSession';
|
|
33
|
+
import { clearGalleryEntrySimulation, pickGroundPosition, placeGalleryEntriesAt, placeGalleryEntryInScene, removeGalleryEntryFromScene, } from '../core/galleryScene';
|
|
34
|
+
import { describeGalleryDrag, GALLERY_DRAG_MIME, readGalleryDrag } from '../core/galleryTree';
|
|
35
|
+
import { loadEffectGallery, collectJsonFiles, bindingFromGalleryEntry, mergeGalleryEntries } from '../core/loadEffectGallery';
|
|
36
|
+
import { pickGalleryJsonFiles } from '../core/pickGalleryFiles';
|
|
37
|
+
import { GalleryViewportPicker } from '../core/viewportPicker';
|
|
38
|
+
import { notifyTransformChanged } from '../core/transformStore';
|
|
21
39
|
import { EffectEditor } from './EffectEditor';
|
|
22
40
|
import { PromptDialog } from './PromptDialog';
|
|
41
|
+
import { MessageDialog } from './MessageDialog';
|
|
42
|
+
import { GalleryPanel } from './GalleryPanel';
|
|
23
43
|
import { TimelinePanel } from './TimelinePanel';
|
|
24
44
|
import { buttonStyle, globalEditorStyles, theme } from './theme';
|
|
25
|
-
|
|
26
|
-
const system = new ParticleSystem({
|
|
27
|
-
scene,
|
|
28
|
-
duration: 4,
|
|
29
|
-
looping: true,
|
|
30
|
-
startLife: new IntervalValue(1.2, 1.8),
|
|
31
|
-
startSpeed: new IntervalValue(3, 5),
|
|
32
|
-
startSize: new IntervalValue(0.25, 0.5),
|
|
33
|
-
startColor: new ConstantColor(new Vector4(1, 1, 1, 1)),
|
|
34
|
-
emissionOverTime: new ConstantValue(80),
|
|
35
|
-
// quarks' angle is a half-angle; Babylon's native cone uses a full angle.
|
|
36
|
-
shape: new ConeEmitter({ radius: 0.3, angle: 0.25 }),
|
|
37
|
-
renderMode: RenderMode.BillBoard,
|
|
38
|
-
texture: (textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined),
|
|
39
|
-
transparent: true,
|
|
40
|
-
blendMode: 1,
|
|
41
|
-
});
|
|
42
|
-
system.addBehavior(new ColorOverLife(buildGradient(DEFAULT_GRADIENT_STOPS)));
|
|
43
|
-
// quarks emits along local +Z; Babylon's native cone emits along +Y.
|
|
44
|
-
system.emitter.rotation.x = -Math.PI / 2;
|
|
45
|
-
return system;
|
|
46
|
-
}
|
|
45
|
+
import { iconStyle } from './icons';
|
|
47
46
|
/** Disposes an effect's systems and its remaining group tree (otherwise the groups leak). */
|
|
48
|
-
function disposeEffect(root, systems, renderer) {
|
|
49
|
-
|
|
50
|
-
renderer.deleteSystem(system);
|
|
51
|
-
system.dispose();
|
|
52
|
-
}
|
|
53
|
-
// Disposing the systems already disposed their emitter nodes; dispose the leftover group
|
|
54
|
-
// tree too. Skip when the root IS a disposed emitter (default single-system effect).
|
|
55
|
-
if (!systems.some((system) => system.emitter === root)) {
|
|
56
|
-
root.dispose(false, true);
|
|
57
|
-
}
|
|
47
|
+
function disposeEffect(root, systems, renderer, inRenderer = true) {
|
|
48
|
+
disposeLoadedEffect(root, systems, renderer, inRenderer);
|
|
58
49
|
}
|
|
50
|
+
const GALLERY_PANEL_WIDTH = 280;
|
|
59
51
|
const GIZMO_MODE_BUTTONS = [
|
|
60
|
-
{ value: 'none',
|
|
61
|
-
{ value: 'position',
|
|
62
|
-
{ value: 'rotation',
|
|
63
|
-
{ value: 'scale',
|
|
52
|
+
{ value: 'none', Icon: CursorArrowRaysIcon, title: 'Select (hide stage gizmo)' },
|
|
53
|
+
{ value: 'position', Icon: ArrowsPointingOutIcon, title: 'Move the stage node (position)' },
|
|
54
|
+
{ value: 'rotation', Icon: ArrowPathIcon, title: 'Rotate the stage node' },
|
|
55
|
+
{ value: 'scale', Icon: Square2StackIcon, title: 'Scale the stage node' },
|
|
64
56
|
];
|
|
65
57
|
const squareButtonStyle = {
|
|
66
58
|
...buttonStyle,
|
|
@@ -83,18 +75,82 @@ const activeSquareButtonStyle = {
|
|
|
83
75
|
export function EffectEditorHost(props) {
|
|
84
76
|
const canvasRef = useRef(null);
|
|
85
77
|
const counterRef = useRef(null);
|
|
78
|
+
const fpsRef = useRef(null);
|
|
86
79
|
const playheadRef = useRef(null);
|
|
87
|
-
const
|
|
80
|
+
const galleryFolderInputRef = useRef(null);
|
|
81
|
+
const importJsonRef = useRef(() => { });
|
|
82
|
+
const importGalleryRef = useRef(async () => { });
|
|
83
|
+
const selectGalleryEntryRef = useRef(() => { });
|
|
84
|
+
const toggleGalleryPreviewRef = useRef(() => { });
|
|
85
|
+
const removeGalleryFromSceneRef = useRef(() => { });
|
|
86
|
+
const placeGalleryDropRef = useRef(() => { });
|
|
87
|
+
const createGalleryDefaultRef = useRef(() => { });
|
|
88
|
+
const dropMarkerRef = useRef(null);
|
|
89
|
+
const selectionMarkerRef = useRef(null);
|
|
90
|
+
const emitterWireframesRef = useRef(null);
|
|
91
|
+
const viewportPickerRef = useRef(null);
|
|
92
|
+
const galleryDragDepthRef = useRef(0);
|
|
88
93
|
const stateRef = useRef(null);
|
|
89
94
|
const gizmoManagerRef = useRef(null);
|
|
90
95
|
const [binding, setBinding] = useState(null);
|
|
91
96
|
const [, force] = useReducer((x) => x + 1, 0);
|
|
92
|
-
const playbackRef = useRef(
|
|
93
|
-
const
|
|
97
|
+
const playbackRef = useRef(createPlaybackState());
|
|
98
|
+
const focusTimelineMetaRef = useRef({ rootId: -1, revision: -1, span: 1, anyLooping: false });
|
|
99
|
+
const [stagePlaying, setStagePlaying] = useState(true);
|
|
94
100
|
const [speed, setSpeed] = useState(1);
|
|
95
101
|
const [renamingRoot, setRenamingRoot] = useState(false);
|
|
96
102
|
const [gizmoMode, setGizmoMode] = useState('none');
|
|
97
103
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
104
|
+
const [gallery, setGallery] = useState([]);
|
|
105
|
+
const [galleryLoading, setGalleryLoading] = useState(null);
|
|
106
|
+
const [selectedGalleryRoot, setSelectedGalleryRoot] = useState(null);
|
|
107
|
+
const [previewRootIds, setPreviewRootIds] = useState([]);
|
|
108
|
+
const [messageDialog, setMessageDialog] = useState(null);
|
|
109
|
+
const [galleryDragHover, setGalleryDragHover] = useState(null);
|
|
110
|
+
const [showEmitterWireframes, setShowEmitterWireframes] = useState(false);
|
|
111
|
+
const isGalleryLoading = galleryLoading !== null;
|
|
112
|
+
const showMessage = (title, message) => setMessageDialog({ title, message });
|
|
113
|
+
/** Appends JSON effects to the catalog without replacing the open effect. */
|
|
114
|
+
const runGalleryImport = async (files) => {
|
|
115
|
+
const jsonFiles = collectJsonFiles(files);
|
|
116
|
+
if (jsonFiles.length === 0) {
|
|
117
|
+
showMessage('Import to gallery', files.length === 0
|
|
118
|
+
? 'Import was cancelled or the browser returned no files.\n\nTry Chrome or Edge and pick .json files or a folder that contains them.'
|
|
119
|
+
: `No .json files found among ${files.length} file(s) in the selection.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await importGalleryRef.current(jsonFiles);
|
|
124
|
+
stateRef.current?.history.clear();
|
|
125
|
+
force();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
129
|
+
showMessage('Import to gallery', message);
|
|
130
|
+
console.error('Failed to import effects into gallery:', err);
|
|
131
|
+
force();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const handleImportFolder = () => {
|
|
135
|
+
if (typeof window.showDirectoryPicker !== 'function') {
|
|
136
|
+
galleryFolderInputRef.current?.click();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
void (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const picked = await pickGalleryJsonFiles();
|
|
142
|
+
if (picked === null || picked === undefined) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await runGalleryImport(picked);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
149
|
+
showMessage('Import to gallery', message);
|
|
150
|
+
console.error('Failed to open folder picker:', err);
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
};
|
|
98
154
|
useEffect(() => {
|
|
99
155
|
// Register the shared ground-plane collider before any effect (default or imported)
|
|
100
156
|
// is built, so ApplyCollision behaviors deserialized by QuarksLoader resolve against it.
|
|
@@ -122,83 +178,421 @@ export function EffectEditorHost(props) {
|
|
|
122
178
|
groundMaterial.opacity = 0.5;
|
|
123
179
|
const ground = MeshBuilder.CreateGround('ground', { width: 100, height: 100 }, scene);
|
|
124
180
|
ground.material = groundMaterial;
|
|
125
|
-
const stageRoot = new TransformNode('StageRoot', scene);
|
|
126
181
|
const gizmoUtilityLayer = new UtilityLayerRenderer(scene);
|
|
127
182
|
const gizmoManager = new GizmoManager(scene, undefined, gizmoUtilityLayer);
|
|
128
183
|
gizmoManager.usePointerToAttachGizmos = false;
|
|
129
|
-
gizmoManager.attachToNode(stageRoot);
|
|
130
184
|
gizmoManagerRef.current = gizmoManager;
|
|
131
185
|
const renderer = new BatchedRenderer('quarks-editor', scene);
|
|
132
186
|
const history = new EffectHistory();
|
|
187
|
+
const galleryRoot = new TransformNode('GalleryCatalog', scene);
|
|
188
|
+
galleryRoot.setEnabled(false);
|
|
189
|
+
let galleryEntries = [];
|
|
190
|
+
const dropMarker = new GalleryDropMarker(scene);
|
|
191
|
+
dropMarkerRef.current = dropMarker;
|
|
192
|
+
const selectionMarker = new GallerySelectionMarker(scene);
|
|
193
|
+
selectionMarkerRef.current = selectionMarker;
|
|
194
|
+
const emitterWireframes = new EmitterShapeWireframes(scene);
|
|
195
|
+
emitterWireframesRef.current = emitterWireframes;
|
|
196
|
+
const viewportPicker = new GalleryViewportPicker(scene);
|
|
197
|
+
viewportPickerRef.current = viewportPicker;
|
|
198
|
+
const handleViewportPointerLeave = () => viewportPicker.setHover(null);
|
|
199
|
+
canvasRef.current?.addEventListener('pointerleave', handleViewportPointerLeave);
|
|
200
|
+
const pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
|
|
201
|
+
if (galleryDragDepthRef.current > 0) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const event = pointerInfo.event;
|
|
205
|
+
if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
|
|
206
|
+
const effectRoot = viewportPicker.pick(event.clientX, event.clientY);
|
|
207
|
+
const focusedRoot = stateRef.current?.binding?.root ?? null;
|
|
208
|
+
viewportPicker.setHover(effectRoot && effectRoot !== focusedRoot ? effectRoot : null);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (pointerInfo.type !== PointerEventTypes.POINTERDOWN || event.button !== 0) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const effectRoot = viewportPicker.pick(event.clientX, event.clientY);
|
|
215
|
+
if (!effectRoot) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const snap = stateRef.current;
|
|
219
|
+
if (!snap) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const entry = snap.galleryEntries.find((item) => item.root === effectRoot);
|
|
223
|
+
if (entry && snap.binding?.root !== effectRoot) {
|
|
224
|
+
selectGalleryEntryRef.current(entry);
|
|
225
|
+
}
|
|
226
|
+
setSelectedNode(effectRoot);
|
|
227
|
+
});
|
|
228
|
+
const clearGalleryDragPreview = () => {
|
|
229
|
+
galleryDragDepthRef.current = 0;
|
|
230
|
+
dropMarker.hide();
|
|
231
|
+
setGalleryDragHover(null);
|
|
232
|
+
setGalleryDragSession(null);
|
|
233
|
+
};
|
|
234
|
+
const onWindowDragEnd = () => clearGalleryDragPreview();
|
|
235
|
+
window.addEventListener('dragend', onWindowDragEnd);
|
|
236
|
+
const syncStateRef = (bindingValue) => {
|
|
237
|
+
stateRef.current = {
|
|
238
|
+
engine,
|
|
239
|
+
scene,
|
|
240
|
+
camera,
|
|
241
|
+
ground,
|
|
242
|
+
renderer,
|
|
243
|
+
binding: bindingValue,
|
|
244
|
+
history,
|
|
245
|
+
galleryRoot,
|
|
246
|
+
galleryEntries,
|
|
247
|
+
previewRootIds: stateRef.current?.previewRootIds ?? [],
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
const focusCatalogEntry = (entry) => {
|
|
251
|
+
const next = bindingFromGalleryEntry(entry);
|
|
252
|
+
history.clear();
|
|
253
|
+
history.attach(next);
|
|
254
|
+
next.subscribe(force);
|
|
255
|
+
syncStateRef(next);
|
|
256
|
+
setBinding(next);
|
|
257
|
+
setSelectedNode(entry.root);
|
|
258
|
+
setSelectedGalleryRoot(entry.root);
|
|
259
|
+
playbackRef.current.focusRootId = entry.root.uniqueId;
|
|
260
|
+
if (entry.inScene) {
|
|
261
|
+
selectionMarker.show(entry.root);
|
|
262
|
+
focusGalleryEntry(camera, entry.root);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
selectionMarker.hide();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const toggleGalleryPreview = (entry) => {
|
|
269
|
+
if (!entry.inScene) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const id = entry.root.uniqueId;
|
|
273
|
+
const removing = (stateRef.current?.previewRootIds ?? []).includes(id);
|
|
274
|
+
setPreviewRootIds((prev) => {
|
|
275
|
+
const next = new Set(prev);
|
|
276
|
+
if (next.has(id)) {
|
|
277
|
+
next.delete(id);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
next.add(id);
|
|
281
|
+
clearFocusFinished(playbackRef.current, id);
|
|
282
|
+
}
|
|
283
|
+
const ids = [...next];
|
|
284
|
+
if (stateRef.current) {
|
|
285
|
+
stateRef.current.previewRootIds = ids;
|
|
286
|
+
}
|
|
287
|
+
return ids;
|
|
288
|
+
});
|
|
289
|
+
if (removing) {
|
|
290
|
+
clearGalleryEntrySimulation(entry, renderer);
|
|
291
|
+
clearFocusFinished(playbackRef.current, id);
|
|
292
|
+
}
|
|
293
|
+
renderer.refreshBatches();
|
|
294
|
+
scene.render();
|
|
295
|
+
force();
|
|
296
|
+
};
|
|
297
|
+
const removeGalleryFromScene = (entry) => {
|
|
298
|
+
if (!entry.inScene) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const id = entry.root.uniqueId;
|
|
302
|
+
removeGalleryEntryFromScene(entry, renderer, galleryRoot);
|
|
303
|
+
setPreviewRootIds((prev) => {
|
|
304
|
+
const next = prev.filter((rootId) => rootId !== id);
|
|
305
|
+
if (stateRef.current) {
|
|
306
|
+
stateRef.current.previewRootIds = next;
|
|
307
|
+
}
|
|
308
|
+
return next;
|
|
309
|
+
});
|
|
310
|
+
clearFocusFinished(playbackRef.current, id);
|
|
311
|
+
playbackRef.current.focusElapsedByRoot.delete(id);
|
|
312
|
+
galleryEntries = [...galleryEntries];
|
|
313
|
+
if (stateRef.current) {
|
|
314
|
+
stateRef.current.galleryEntries = galleryEntries;
|
|
315
|
+
}
|
|
316
|
+
setGallery(galleryEntries);
|
|
317
|
+
if (stateRef.current?.binding?.root === entry.root) {
|
|
318
|
+
selectionMarker.hide();
|
|
319
|
+
}
|
|
320
|
+
applyGalleryPlayback(galleryEntries, new Set(stateRef.current?.previewRootIds ?? []), playbackRef.current.stagePlaying, playbackRef.current.focusFinishedByRoot);
|
|
321
|
+
renderer.refreshBatches();
|
|
322
|
+
scene.render();
|
|
323
|
+
force();
|
|
324
|
+
};
|
|
325
|
+
const addGalleryPreview = (...entries) => {
|
|
326
|
+
const ids = entries.filter((entry) => entry.inScene).map((entry) => entry.root.uniqueId);
|
|
327
|
+
if (ids.length === 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
setPreviewRootIds((prev) => {
|
|
331
|
+
const next = [...new Set([...prev, ...ids])];
|
|
332
|
+
if (stateRef.current) {
|
|
333
|
+
stateRef.current.previewRootIds = next;
|
|
334
|
+
}
|
|
335
|
+
return next;
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
const placeGalleryDropAt = (clientX, clientY, dataTransfer) => {
|
|
339
|
+
const payload = (dataTransfer && readGalleryDrag(dataTransfer)) ?? getGalleryDragSession();
|
|
340
|
+
if (!payload) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const { entries: toPlace } = describeGalleryDrag(galleryEntries, payload);
|
|
344
|
+
if (toPlace.length === 0) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const position = pickGroundPosition(scene, clientX, clientY);
|
|
348
|
+
if (toPlace.length === 1) {
|
|
349
|
+
placeGalleryEntryInScene(toPlace[0], renderer, position);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
placeGalleryEntriesAt(toPlace, renderer, position);
|
|
353
|
+
}
|
|
354
|
+
galleryEntries = [...galleryEntries];
|
|
355
|
+
if (stateRef.current) {
|
|
356
|
+
stateRef.current.galleryEntries = galleryEntries;
|
|
357
|
+
}
|
|
358
|
+
setGallery(galleryEntries);
|
|
359
|
+
addGalleryPreview(...toPlace);
|
|
360
|
+
focusCatalogEntry(toPlace[0]);
|
|
361
|
+
dropMarkerRef.current?.hide();
|
|
362
|
+
setGalleryDragHover(null);
|
|
363
|
+
setGalleryDragSession(null);
|
|
364
|
+
force();
|
|
365
|
+
};
|
|
133
366
|
const mount = (nextBinding) => {
|
|
134
|
-
nextBinding.root.parent =
|
|
367
|
+
nextBinding.root.parent = null;
|
|
135
368
|
history.attach(nextBinding);
|
|
136
369
|
nextBinding.subscribe(force);
|
|
137
|
-
|
|
370
|
+
syncStateRef(nextBinding);
|
|
138
371
|
setBinding(nextBinding);
|
|
139
|
-
setSelectedNode(nextBinding.
|
|
372
|
+
setSelectedNode(nextBinding.root);
|
|
373
|
+
props.onReady?.(makeHandle());
|
|
374
|
+
};
|
|
375
|
+
const appendGalleryFromFiles = async (files) => {
|
|
376
|
+
setGalleryLoading({ done: 0, total: 0, loaded: 0, failed: 0 });
|
|
377
|
+
const incoming = await loadEffectGallery(scene, files, galleryRoot, (progress) => setGalleryLoading(progress));
|
|
378
|
+
const { merged, added } = mergeGalleryEntries(galleryEntries, incoming, renderer);
|
|
379
|
+
galleryEntries = merged;
|
|
380
|
+
if (stateRef.current) {
|
|
381
|
+
stateRef.current.galleryRoot = galleryRoot;
|
|
382
|
+
stateRef.current.galleryEntries = merged;
|
|
383
|
+
}
|
|
384
|
+
setGalleryLoading(null);
|
|
385
|
+
setGallery(merged);
|
|
386
|
+
if (incoming.length === 0) {
|
|
387
|
+
throw new Error('No particle effects found in the selected file(s).');
|
|
388
|
+
}
|
|
389
|
+
if (added.length === 0) {
|
|
390
|
+
throw new Error('All selected effects are already in the catalog.');
|
|
391
|
+
}
|
|
392
|
+
focusCatalogEntry(added[0]);
|
|
393
|
+
props.onReady?.(makeHandle());
|
|
394
|
+
};
|
|
395
|
+
const appendGalleryDefault = () => {
|
|
396
|
+
const entry = createGalleryDefaultEntry(scene, galleryRoot, galleryEntries, {
|
|
397
|
+
resolveTexture: props.resolveTexture,
|
|
398
|
+
textureUrl: props.textureOptions?.[0]?.url,
|
|
399
|
+
});
|
|
400
|
+
galleryEntries = [...galleryEntries, entry];
|
|
401
|
+
if (stateRef.current) {
|
|
402
|
+
stateRef.current.galleryRoot = galleryRoot;
|
|
403
|
+
stateRef.current.galleryEntries = galleryEntries;
|
|
404
|
+
}
|
|
405
|
+
setGallery(galleryEntries);
|
|
406
|
+
focusCatalogEntry(entry);
|
|
140
407
|
props.onReady?.(makeHandle());
|
|
141
408
|
};
|
|
142
409
|
const makeHandle = () => ({
|
|
143
|
-
// Live getter: stays correct after presets/imports/undo replace the binding.
|
|
144
410
|
get binding() {
|
|
145
|
-
|
|
411
|
+
const b = stateRef.current?.binding;
|
|
412
|
+
if (!b) {
|
|
413
|
+
throw new Error('Gallery mode — import a single JSON to edit one effect.');
|
|
414
|
+
}
|
|
415
|
+
return b;
|
|
146
416
|
},
|
|
147
417
|
history,
|
|
148
418
|
scene,
|
|
149
419
|
renderer,
|
|
150
420
|
importJson,
|
|
151
|
-
|
|
421
|
+
importGallery: appendGalleryFromFiles,
|
|
422
|
+
exportJson: () => makeHandle().binding.exportJSON('EditorEffect'),
|
|
152
423
|
});
|
|
153
424
|
const importJson = (json) => {
|
|
154
|
-
const current = stateRef.current
|
|
155
|
-
// Load first so a bad/empty JSON leaves the current effect intact.
|
|
425
|
+
const current = stateRef.current?.binding;
|
|
156
426
|
const { root, systems } = loadEffectFromJson(scene, renderer, json);
|
|
157
427
|
if (systems.length === 0) {
|
|
158
428
|
disposeEffect(root, systems, renderer);
|
|
159
429
|
throw new Error('No particle systems found in the JSON.');
|
|
160
430
|
}
|
|
161
|
-
|
|
431
|
+
if (current) {
|
|
432
|
+
const catalogEntry = stateRef.current?.galleryEntries.find((entry) => entry.root === current.root);
|
|
433
|
+
disposeEffect(current.root, [current.system, ...current.subSystems], renderer, catalogEntry?.inScene ?? true);
|
|
434
|
+
}
|
|
162
435
|
const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
|
|
163
|
-
|
|
436
|
+
const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
|
|
437
|
+
setSelectedGalleryRoot(null);
|
|
438
|
+
setPreviewRootIds([]);
|
|
439
|
+
selectionMarker.hide();
|
|
440
|
+
mount(next);
|
|
441
|
+
playbackRef.current.focusRootId = next.root.uniqueId;
|
|
442
|
+
};
|
|
443
|
+
importJsonRef.current = importJson;
|
|
444
|
+
importGalleryRef.current = appendGalleryFromFiles;
|
|
445
|
+
selectGalleryEntryRef.current = focusCatalogEntry;
|
|
446
|
+
toggleGalleryPreviewRef.current = toggleGalleryPreview;
|
|
447
|
+
removeGalleryFromSceneRef.current = removeGalleryFromScene;
|
|
448
|
+
placeGalleryDropRef.current = placeGalleryDropAt;
|
|
449
|
+
createGalleryDefaultRef.current = appendGalleryDefault;
|
|
450
|
+
const galleryDefaults = {
|
|
451
|
+
resolveTexture: props.resolveTexture,
|
|
452
|
+
textureUrl: props.textureOptions?.[0]?.url,
|
|
164
453
|
};
|
|
165
|
-
let
|
|
454
|
+
let initialEntry;
|
|
166
455
|
if (props.effectJson) {
|
|
167
456
|
const { root, systems } = loadEffectFromJson(scene, renderer, props.effectJson);
|
|
168
|
-
|
|
169
|
-
|
|
457
|
+
root.parent = null;
|
|
458
|
+
const name = root.name || 'Effect';
|
|
459
|
+
initialEntry = { name, path: `_opened/${name}.json`, root, systems, inScene: true };
|
|
170
460
|
}
|
|
171
461
|
else {
|
|
172
|
-
|
|
173
|
-
renderer.
|
|
174
|
-
// Root is a plain group (not the system's own emitter) so a new top-level system
|
|
175
|
-
// can be added as a sibling later, instead of always nesting under this one.
|
|
176
|
-
const root = new TransformNode('Effect', scene);
|
|
177
|
-
system.emitter.parent = root;
|
|
178
|
-
initial = new EffectBinding(system, [], root);
|
|
462
|
+
initialEntry = createGalleryDefaultEntry(scene, galleryRoot, [], galleryDefaults);
|
|
463
|
+
placeGalleryEntryInScene(initialEntry, renderer, BVector3.Zero());
|
|
179
464
|
}
|
|
180
|
-
|
|
465
|
+
galleryEntries = [initialEntry];
|
|
466
|
+
const initialPreviewIds = initialEntry.inScene ? [initialEntry.root.uniqueId] : [];
|
|
467
|
+
if (stateRef.current) {
|
|
468
|
+
stateRef.current.galleryRoot = galleryRoot;
|
|
469
|
+
stateRef.current.galleryEntries = galleryEntries;
|
|
470
|
+
stateRef.current.previewRootIds = initialPreviewIds;
|
|
471
|
+
}
|
|
472
|
+
setPreviewRootIds(initialPreviewIds);
|
|
473
|
+
setGallery(galleryEntries);
|
|
474
|
+
focusCatalogEntry(initialEntry);
|
|
475
|
+
props.onReady?.(makeHandle());
|
|
181
476
|
engine.runRenderLoop(() => {
|
|
182
477
|
const playback = playbackRef.current;
|
|
478
|
+
const snapshot = stateRef.current;
|
|
183
479
|
if (!playback.scrubbing) {
|
|
184
480
|
let delta = (engine.getDeltaTime() / 1000) * playback.speed;
|
|
185
|
-
|
|
186
|
-
|
|
481
|
+
const stepQueued = playback.stepQueued;
|
|
482
|
+
if (!playback.stagePlaying) {
|
|
483
|
+
delta = stepQueued ? 1 / 60 : 0;
|
|
187
484
|
playback.stepQueued = false;
|
|
188
485
|
}
|
|
189
|
-
if (delta > 0) {
|
|
190
|
-
|
|
191
|
-
playback.
|
|
486
|
+
if (delta > 0 && snapshot) {
|
|
487
|
+
const previewIds = new Set(snapshot.previewRootIds);
|
|
488
|
+
const simulate = hasActivePreview(snapshot.galleryEntries, previewIds) && (playback.stagePlaying || stepQueued);
|
|
489
|
+
if (simulate) {
|
|
490
|
+
renderer.update(delta);
|
|
491
|
+
let needsRefresh = false;
|
|
492
|
+
if (pauseFinishedPreviews(snapshot.galleryEntries, previewIds, playback.focusFinishedByRoot)) {
|
|
493
|
+
needsRefresh = true;
|
|
494
|
+
}
|
|
495
|
+
const binding = snapshot.binding;
|
|
496
|
+
const focusId = playback.focusRootId ?? binding?.root.uniqueId ?? null;
|
|
497
|
+
if (focusId != null &&
|
|
498
|
+
binding &&
|
|
499
|
+
playback.stagePlaying &&
|
|
500
|
+
!playback.focusFinishedByRoot.has(focusId) &&
|
|
501
|
+
!focusTimelineMetaRef.current.anyLooping &&
|
|
502
|
+
QuarksUtil.isEffectFinished(binding.root)) {
|
|
503
|
+
const settleAt = getFocusElapsed(playback, focusId);
|
|
504
|
+
scrubFocusToSettled(binding, renderer, settleAt);
|
|
505
|
+
markFocusFinished(playback, focusId);
|
|
506
|
+
pauseFocus(binding);
|
|
507
|
+
needsRefresh = true;
|
|
508
|
+
}
|
|
509
|
+
else if (focusId != null &&
|
|
510
|
+
playback.stagePlaying &&
|
|
511
|
+
!playback.focusFinishedByRoot.has(focusId)) {
|
|
512
|
+
const prev = getFocusElapsed(playback, focusId);
|
|
513
|
+
setFocusElapsed(playback, focusId, prev + delta);
|
|
514
|
+
}
|
|
515
|
+
if (needsRefresh) {
|
|
516
|
+
renderer.refreshBatches();
|
|
517
|
+
}
|
|
518
|
+
if (snapshot.galleryEntries.length > 0) {
|
|
519
|
+
logRenderMismatch(renderer, countGalleryParticles(snapshot.galleryEntries, previewIds));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else if (snapshot && snapshot.galleryEntries.length > 0) {
|
|
523
|
+
renderer.refreshBatches();
|
|
524
|
+
}
|
|
192
525
|
}
|
|
193
526
|
}
|
|
194
527
|
scene.render();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
528
|
+
selectionMarker.follow();
|
|
529
|
+
viewportPickerRef.current?.follow();
|
|
530
|
+
const wireframes = emitterWireframesRef.current;
|
|
531
|
+
if (wireframes && snapshot?.binding) {
|
|
532
|
+
const b = snapshot.binding;
|
|
533
|
+
wireframes.sync([b.system, ...b.subSystems], `${b.root.uniqueId}:${b.getRevision()}`);
|
|
534
|
+
}
|
|
535
|
+
const picker = viewportPickerRef.current;
|
|
536
|
+
if (picker && snapshot) {
|
|
537
|
+
const b = snapshot.binding;
|
|
538
|
+
const galleryKey = snapshot.galleryEntries
|
|
539
|
+
.map((entry) => `${entry.root.uniqueId}:${entry.inScene ? 1 : 0}`)
|
|
540
|
+
.join('|');
|
|
541
|
+
const key = `${galleryKey}::${b?.root.uniqueId ?? 'none'}`;
|
|
542
|
+
picker.sync(snapshot.galleryEntries, b?.root ?? null, key);
|
|
543
|
+
}
|
|
544
|
+
if (!snapshot)
|
|
545
|
+
return;
|
|
546
|
+
if (fpsRef.current) {
|
|
547
|
+
fpsRef.current.textContent = `${Math.round(engine.getFps())} FPS`;
|
|
199
548
|
}
|
|
200
|
-
|
|
201
|
-
|
|
549
|
+
const previewIds = new Set(snapshot.previewRootIds);
|
|
550
|
+
const binding = snapshot.binding;
|
|
551
|
+
if (binding && (binding.root.uniqueId !== focusTimelineMetaRef.current.rootId || binding.getRevision() !== focusTimelineMetaRef.current.revision)) {
|
|
552
|
+
const rows = computeTimelineRows(binding);
|
|
553
|
+
focusTimelineMetaRef.current = {
|
|
554
|
+
rootId: binding.root.uniqueId,
|
|
555
|
+
revision: binding.getRevision(),
|
|
556
|
+
span: computeTimelineSpan(rows),
|
|
557
|
+
anyLooping: rows.some((row) => row.kind === 'track' && row.looping),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (snapshot.galleryEntries.length > 0) {
|
|
561
|
+
const playing = countActivePreviews(snapshot.galleryEntries, previewIds);
|
|
562
|
+
const count = countGalleryParticles(snapshot.galleryEntries, previewIds);
|
|
563
|
+
const inScene = snapshot.galleryEntries.filter((entry) => entry.inScene).length;
|
|
564
|
+
const focused = snapshot.galleryEntries.find((entry) => entry.root === binding?.root);
|
|
565
|
+
const focusId = playback.focusRootId ?? focused?.root.uniqueId ?? null;
|
|
566
|
+
const focusElapsed = focusId != null ? getFocusElapsed(playback, focusId) : 0;
|
|
567
|
+
const focusEnded = focusId != null && isFocusFinished(playback, focusId);
|
|
568
|
+
const meta = focusTimelineMetaRef.current;
|
|
569
|
+
if (counterRef.current) {
|
|
570
|
+
const focusLabel = focused ? ` · focus: ${focused.name}` : '';
|
|
571
|
+
const stageLabel = playback.stagePlaying ? 'playing' : 'paused';
|
|
572
|
+
const timeLabel = meta.anyLooping
|
|
573
|
+
? `${focusElapsed.toFixed(1)}s ⟳`
|
|
574
|
+
: focusEnded
|
|
575
|
+
? `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s (end)`
|
|
576
|
+
: `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s`;
|
|
577
|
+
counterRef.current.textContent = `${playing} preview · ${stageLabel}${focusLabel} · ${timeLabel} · ${count} particles · ${inScene}/${snapshot.galleryEntries.length} in catalog`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else if (binding) {
|
|
581
|
+
const b = binding;
|
|
582
|
+
const count = b.system.particleNum + b.subSystems.reduce((sum, sub) => sum + sub.particleNum, 0);
|
|
583
|
+
const focusId = playback.focusRootId ?? b.root.uniqueId;
|
|
584
|
+
const focusElapsed = getFocusElapsed(playback, focusId);
|
|
585
|
+
const focusEnded = isFocusFinished(playback, focusId);
|
|
586
|
+
const meta = focusTimelineMetaRef.current;
|
|
587
|
+
if (counterRef.current) {
|
|
588
|
+
const stageLabel = playback.stagePlaying ? 'playing' : 'paused';
|
|
589
|
+
const timeLabel = meta.anyLooping
|
|
590
|
+
? `${focusElapsed.toFixed(1)}s ⟳`
|
|
591
|
+
: focusEnded
|
|
592
|
+
? `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s (end)`
|
|
593
|
+
: `${focusElapsed.toFixed(1)}s / ${meta.span.toFixed(1)}s`;
|
|
594
|
+
counterRef.current.textContent = `${stageLabel} · ${timeLabel} · ${count} particles`;
|
|
595
|
+
}
|
|
202
596
|
}
|
|
203
597
|
});
|
|
204
598
|
const resizeObserver = new ResizeObserver(() => engine.resize());
|
|
@@ -217,12 +611,48 @@ export function EffectEditorHost(props) {
|
|
|
217
611
|
return () => {
|
|
218
612
|
resizeObserver.disconnect();
|
|
219
613
|
window.removeEventListener('keydown', onKey);
|
|
614
|
+
window.removeEventListener('dragend', onWindowDragEnd);
|
|
615
|
+
clearGalleryDragPreview();
|
|
616
|
+
cancelGalleryCameraFocus();
|
|
617
|
+
dropMarker.dispose();
|
|
618
|
+
dropMarkerRef.current = null;
|
|
619
|
+
selectionMarker.dispose();
|
|
620
|
+
selectionMarkerRef.current = null;
|
|
621
|
+
emitterWireframes.dispose();
|
|
622
|
+
emitterWireframesRef.current = null;
|
|
623
|
+
viewportPicker.dispose();
|
|
624
|
+
viewportPickerRef.current = null;
|
|
625
|
+
canvasRef.current?.removeEventListener('pointerleave', handleViewportPointerLeave);
|
|
626
|
+
scene.onPointerObservable.remove(pointerObserver);
|
|
220
627
|
gizmoManager.dispose();
|
|
221
628
|
gizmoUtilityLayer.dispose();
|
|
222
629
|
engine.dispose();
|
|
223
630
|
};
|
|
224
631
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
225
632
|
}, []);
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
if (stateRef.current) {
|
|
635
|
+
stateRef.current.previewRootIds = previewRootIds;
|
|
636
|
+
}
|
|
637
|
+
}, [previewRootIds]);
|
|
638
|
+
useEffect(() => {
|
|
639
|
+
playbackRef.current.stagePlaying = stagePlaying;
|
|
640
|
+
const snap = stateRef.current;
|
|
641
|
+
if (!snap) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const isCatalogEntry = snap.galleryEntries.length > 0;
|
|
645
|
+
if (!isCatalogEntry) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
applyGalleryPlayback(snap.galleryEntries, new Set(previewRootIds), stagePlaying, playbackRef.current.focusFinishedByRoot);
|
|
649
|
+
if (!stagePlaying && stateRef.current?.renderer) {
|
|
650
|
+
stateRef.current.renderer.refreshBatches();
|
|
651
|
+
}
|
|
652
|
+
}, [stagePlaying, previewRootIds, gallery]);
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
emitterWireframesRef.current?.setVisible(showEmitterWireframes);
|
|
655
|
+
}, [showEmitterWireframes]);
|
|
226
656
|
useEffect(() => {
|
|
227
657
|
const gm = gizmoManagerRef.current;
|
|
228
658
|
if (!gm)
|
|
@@ -231,48 +661,262 @@ export function EffectEditorHost(props) {
|
|
|
231
661
|
gm.rotationGizmoEnabled = gizmoMode === 'rotation';
|
|
232
662
|
gm.scaleGizmoEnabled = gizmoMode === 'scale';
|
|
233
663
|
const activeGizmo = gizmoMode === 'position' ? gm.gizmos.positionGizmo : gizmoMode === 'rotation' ? gm.gizmos.rotationGizmo : gizmoMode === 'scale' ? gm.gizmos.scaleGizmo : null;
|
|
234
|
-
|
|
664
|
+
let dragRaf = 0;
|
|
665
|
+
const scheduleTransformRefresh = () => {
|
|
666
|
+
if (dragRaf !== 0) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
dragRaf = requestAnimationFrame(() => {
|
|
670
|
+
dragRaf = 0;
|
|
671
|
+
notifyTransformChanged();
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
const dragObserver = activeGizmo?.onDragObservable.add(scheduleTransformRefresh);
|
|
675
|
+
const dragEndObserver = activeGizmo?.onDragEndObservable.add(() => notifyTransformChanged());
|
|
235
676
|
return () => {
|
|
236
|
-
if (
|
|
237
|
-
|
|
677
|
+
if (dragRaf !== 0) {
|
|
678
|
+
cancelAnimationFrame(dragRaf);
|
|
679
|
+
}
|
|
680
|
+
if (activeGizmo && dragObserver) {
|
|
681
|
+
activeGizmo.onDragObservable.remove(dragObserver);
|
|
682
|
+
}
|
|
683
|
+
if (activeGizmo && dragEndObserver) {
|
|
684
|
+
activeGizmo.onDragEndObservable.remove(dragEndObserver);
|
|
685
|
+
}
|
|
238
686
|
};
|
|
239
687
|
}, [gizmoMode]);
|
|
240
688
|
useEffect(() => {
|
|
241
689
|
const gm = gizmoManagerRef.current;
|
|
242
|
-
|
|
243
|
-
if (!gm || !stageRoot)
|
|
690
|
+
if (!gm)
|
|
244
691
|
return;
|
|
245
692
|
if (selectedNode && selectedNode.isDisposed()) {
|
|
246
|
-
setSelectedNode(stateRef.current?.binding.system.emitter ?? null);
|
|
693
|
+
setSelectedNode(stateRef.current?.binding?.root ?? stateRef.current?.binding?.system.emitter ?? null);
|
|
247
694
|
return;
|
|
248
695
|
}
|
|
249
|
-
|
|
250
|
-
|
|
696
|
+
if (isGalleryLoading) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const target = selectedNode && !selectedNode.isDisposed()
|
|
700
|
+
? selectedNode
|
|
701
|
+
: binding?.root && !binding.root.isDisposed()
|
|
702
|
+
? binding.root
|
|
703
|
+
: null;
|
|
704
|
+
if (target) {
|
|
705
|
+
gm.attachToNode(target);
|
|
706
|
+
}
|
|
707
|
+
}, [selectedNode, binding, isGalleryLoading]);
|
|
251
708
|
const state = stateRef.current;
|
|
252
|
-
const gizmoNode = selectedNode && !selectedNode.isDisposed()
|
|
709
|
+
const gizmoNode = selectedNode && !selectedNode.isDisposed()
|
|
710
|
+
? selectedNode
|
|
711
|
+
: binding?.root && !binding.root.isDisposed()
|
|
712
|
+
? binding.root
|
|
713
|
+
: null;
|
|
253
714
|
const historyAction = (json) => {
|
|
254
|
-
if (json && state) {
|
|
715
|
+
if (json && state?.binding) {
|
|
255
716
|
const current = state.binding;
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
717
|
+
const inGallery = state.galleryEntries.length > 0;
|
|
718
|
+
const entryIndex = inGallery ? state.galleryEntries.findIndex((e) => e.root === current.root) : -1;
|
|
719
|
+
const prev = entryIndex >= 0 ? state.galleryEntries[entryIndex] : null;
|
|
720
|
+
const wasInScene = prev?.inScene ?? true;
|
|
721
|
+
const savedPosition = wasInScene ? current.root.position.clone() : null;
|
|
722
|
+
if (inGallery && entryIndex >= 0 && prev) {
|
|
723
|
+
disposeEffect(current.root, [current.system, ...current.subSystems], state.renderer, wasInScene);
|
|
724
|
+
const { root, systems } = parseEffectFromJson(state.scene, wasInScene ? state.renderer : null, json, {
|
|
725
|
+
registerRenderer: wasInScene,
|
|
726
|
+
autoplay: false,
|
|
727
|
+
});
|
|
728
|
+
if (systems.length === 0) {
|
|
729
|
+
disposeLoadedEffect(root, systems, state.renderer, wasInScene);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (wasInScene && savedPosition) {
|
|
733
|
+
root.position.copyFrom(savedPosition);
|
|
734
|
+
root.parent = null;
|
|
735
|
+
root.setEnabled(true);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
root.parent = state.galleryRoot;
|
|
739
|
+
root.setEnabled(false);
|
|
740
|
+
}
|
|
741
|
+
if (!root.name || root.name === 'Effect' || root.name === 'Object3D') {
|
|
742
|
+
root.name = prev.name;
|
|
743
|
+
}
|
|
744
|
+
const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
|
|
745
|
+
const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
|
|
746
|
+
const oldRootId = current.root.uniqueId;
|
|
747
|
+
const updated = [...state.galleryEntries];
|
|
748
|
+
updated[entryIndex] = { name: prev.name, path: prev.path, root, systems, inScene: wasInScene };
|
|
749
|
+
state.galleryEntries = updated;
|
|
750
|
+
setGallery(updated);
|
|
751
|
+
setPreviewRootIds((ids) => {
|
|
752
|
+
const next = ids.map((id) => (id === oldRootId ? root.uniqueId : id));
|
|
753
|
+
if (stateRef.current) {
|
|
754
|
+
stateRef.current.previewRootIds = next;
|
|
755
|
+
}
|
|
756
|
+
return next;
|
|
757
|
+
});
|
|
758
|
+
state.history.attach(next);
|
|
759
|
+
next.subscribe(force);
|
|
760
|
+
state.binding = next;
|
|
761
|
+
setBinding(next);
|
|
762
|
+
setSelectedNode(root);
|
|
763
|
+
setSelectedGalleryRoot(root);
|
|
764
|
+
playbackRef.current.focusRootId = root.uniqueId;
|
|
765
|
+
if (wasInScene) {
|
|
766
|
+
selectionMarkerRef.current?.show(root);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
selectionMarkerRef.current?.hide();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
const { root, systems } = loadEffectFromJson(state.scene, state.renderer, json);
|
|
774
|
+
if (systems.length === 0) {
|
|
775
|
+
disposeEffect(root, systems, state.renderer);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
disposeEffect(current.root, [current.system, ...current.subSystems], state.renderer);
|
|
779
|
+
const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
|
|
780
|
+
const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
|
|
781
|
+
next.root.parent = null;
|
|
782
|
+
state.history.attach(next);
|
|
783
|
+
next.subscribe(force);
|
|
784
|
+
state.binding = next;
|
|
785
|
+
setBinding(next);
|
|
786
|
+
setSelectedNode(root);
|
|
787
|
+
setSelectedGalleryRoot(null);
|
|
788
|
+
selectionMarkerRef.current?.hide();
|
|
789
|
+
state.ground.scaling.x = 1;
|
|
790
|
+
state.ground.scaling.z = 1;
|
|
791
|
+
state.camera.radius = 9;
|
|
792
|
+
state.camera.setTarget(new BVector3(0, 1.4, 0));
|
|
793
|
+
}
|
|
266
794
|
}
|
|
267
795
|
force();
|
|
268
796
|
};
|
|
269
797
|
const allSystems = binding ? [binding.system, ...binding.subSystems] : [];
|
|
270
798
|
const activeSystem = binding ? (allSystems.find((s) => s.emitter === selectedNode) ?? binding.system) : null;
|
|
271
|
-
const gizmoTargetLabel =
|
|
272
|
-
|
|
799
|
+
const gizmoTargetLabel = gizmoNode?.name || 'Node';
|
|
800
|
+
const selectedGalleryEntry = selectedGalleryRoot
|
|
801
|
+
? gallery.find((entry) => entry.root === selectedGalleryRoot) ?? null
|
|
802
|
+
: null;
|
|
803
|
+
/** Timeline is always shown when an effect is selected; playback only when it is on stage. */
|
|
804
|
+
const showTimeline = Boolean(binding && activeSystem && state);
|
|
805
|
+
const timelinePlaybackEnabled = !selectedGalleryEntry || selectedGalleryEntry.inScene;
|
|
806
|
+
const previewRootIdSet = useMemo(() => new Set(previewRootIds), [previewRootIds]);
|
|
807
|
+
const resolveTextureForEditor = useCallback((url) => {
|
|
808
|
+
const scene = stateRef.current?.scene;
|
|
809
|
+
return scene && props.resolveTexture ? props.resolveTexture(url, scene) : undefined;
|
|
810
|
+
}, [props.resolveTexture]);
|
|
811
|
+
const resolveGeometryForEditor = useCallback((file) => {
|
|
812
|
+
const scene = stateRef.current?.scene;
|
|
813
|
+
if (!scene || !props.resolveGeometry) {
|
|
814
|
+
return Promise.reject(new Error('Geometry loader is not available.'));
|
|
815
|
+
}
|
|
816
|
+
return props.resolveGeometry(file, scene);
|
|
817
|
+
}, [props.resolveGeometry]);
|
|
818
|
+
return (_jsxs("div", { className: "qe-root", style: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%', fontFamily: theme.font, color: theme.text, background: '#070b16' }, children: [_jsx("style", { children: globalEditorStyles }), _jsxs("div", { style: { display: 'flex', flex: 1, minHeight: 0 }, children: [_jsxs("aside", { style: {
|
|
819
|
+
width: GALLERY_PANEL_WIDTH,
|
|
820
|
+
flexShrink: 0,
|
|
821
|
+
borderRight: `1px solid ${theme.border}`,
|
|
822
|
+
background: theme.panelBg,
|
|
823
|
+
display: 'flex',
|
|
824
|
+
flexDirection: 'column',
|
|
825
|
+
minHeight: 0,
|
|
826
|
+
}, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsxs("div", { style: { fontSize: 14, fontWeight: 600 }, children: ["Gallery", gallery.length > 0 ? ` · ${gallery.length}` : ''] }), _jsx("div", { style: { fontSize: 11, color: theme.textDim, marginTop: 4, lineHeight: 1.4 }, children: "Drag into viewport to place \u00B7 \u2713 preview \u00B7 \u00D7 remove from stage" })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'hidden', padding: '8px 12px 14px', display: 'flex', flexDirection: 'column' }, children: _jsx(GalleryPanel, { entries: gallery, focusedRoot: selectedGalleryRoot, previewRootIds: previewRootIdSet, loading: galleryLoading, folderInputRef: galleryFolderInputRef, onFocus: (entry) => selectGalleryEntryRef.current(entry), onTogglePreview: (entry) => toggleGalleryPreviewRef.current(entry), onRemoveFromScene: (entry) => removeGalleryFromSceneRef.current(entry), onImportFiles: (files) => void runGalleryImport(files), onImportFolder: () => void handleImportFolder(), onCreateDefault: () => createGalleryDefaultRef.current() }) })] }), _jsxs("div", { style: { position: 'relative', flex: 1, minWidth: 0 }, onDragEnter: (e) => {
|
|
827
|
+
if (!e.dataTransfer.types.includes(GALLERY_DRAG_MIME))
|
|
828
|
+
return;
|
|
829
|
+
galleryDragDepthRef.current += 1;
|
|
830
|
+
}, onDragLeave: (e) => {
|
|
831
|
+
if (!e.dataTransfer.types.includes(GALLERY_DRAG_MIME))
|
|
832
|
+
return;
|
|
833
|
+
galleryDragDepthRef.current -= 1;
|
|
834
|
+
if (galleryDragDepthRef.current <= 0) {
|
|
835
|
+
galleryDragDepthRef.current = 0;
|
|
836
|
+
dropMarkerRef.current?.hide();
|
|
837
|
+
setGalleryDragHover(null);
|
|
838
|
+
}
|
|
839
|
+
}, onDragOver: (e) => {
|
|
840
|
+
if (!e.dataTransfer.types.includes(GALLERY_DRAG_MIME))
|
|
841
|
+
return;
|
|
842
|
+
e.preventDefault();
|
|
843
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
844
|
+
const payload = getGalleryDragSession();
|
|
845
|
+
const snap = stateRef.current;
|
|
846
|
+
if (!payload || !snap)
|
|
847
|
+
return;
|
|
848
|
+
const described = describeGalleryDrag(snap.galleryEntries, payload);
|
|
849
|
+
if (described.entries.length === 0)
|
|
850
|
+
return;
|
|
851
|
+
const position = pickGroundPosition(snap.scene, e.clientX, e.clientY);
|
|
852
|
+
dropMarkerRef.current?.show(position, described.entries.length);
|
|
853
|
+
setGalleryDragHover({ label: described.label, count: described.entries.length });
|
|
854
|
+
}, onDrop: (e) => {
|
|
855
|
+
if (!e.dataTransfer.types.includes(GALLERY_DRAG_MIME))
|
|
856
|
+
return;
|
|
857
|
+
e.preventDefault();
|
|
858
|
+
galleryDragDepthRef.current = 0;
|
|
859
|
+
placeGalleryDropRef.current(e.clientX, e.clientY, e.dataTransfer);
|
|
860
|
+
}, children: [_jsx("canvas", { ref: canvasRef, style: { width: '100%', height: '100%', display: 'block', touchAction: 'none', outline: 'none' } }), _jsx("div", { style: {
|
|
861
|
+
position: 'absolute',
|
|
862
|
+
bottom: 12,
|
|
863
|
+
left: 12,
|
|
864
|
+
background: theme.panelBg,
|
|
865
|
+
border: `1px solid ${theme.border}`,
|
|
866
|
+
borderRadius: 8,
|
|
867
|
+
padding: '4px 10px',
|
|
868
|
+
fontSize: 12,
|
|
869
|
+
color: theme.textDim,
|
|
870
|
+
fontVariantNumeric: 'tabular-nums',
|
|
871
|
+
pointerEvents: 'none',
|
|
872
|
+
fontFamily: theme.font,
|
|
873
|
+
}, children: _jsx("span", { ref: fpsRef, children: "\u2014 FPS" }) }), galleryDragHover && (_jsx("div", { style: {
|
|
874
|
+
position: 'absolute',
|
|
875
|
+
inset: 10,
|
|
876
|
+
border: '2px dashed rgba(120, 165, 255, 0.75)',
|
|
877
|
+
borderRadius: 12,
|
|
878
|
+
pointerEvents: 'none',
|
|
879
|
+
boxShadow: 'inset 0 0 0 1px rgba(120, 165, 255, 0.15)',
|
|
880
|
+
}, children: _jsxs("div", { style: {
|
|
881
|
+
position: 'absolute',
|
|
882
|
+
left: '50%',
|
|
883
|
+
bottom: 18,
|
|
884
|
+
transform: 'translateX(-50%)',
|
|
885
|
+
background: 'rgba(7, 11, 22, 0.88)',
|
|
886
|
+
border: `1px solid ${theme.border}`,
|
|
887
|
+
borderRadius: 8,
|
|
888
|
+
padding: '8px 14px',
|
|
889
|
+
fontSize: 13,
|
|
890
|
+
color: theme.text,
|
|
891
|
+
whiteSpace: 'nowrap',
|
|
892
|
+
}, children: ["Drop ", galleryDragHover.count === 1 ? 'effect' : `${galleryDragHover.count} effects`, " \u2014 ", galleryDragHover.label] }) })), galleryLoading && (_jsx("div", { style: {
|
|
893
|
+
position: 'absolute',
|
|
894
|
+
inset: 0,
|
|
895
|
+
display: 'flex',
|
|
896
|
+
alignItems: 'center',
|
|
897
|
+
justifyContent: 'center',
|
|
898
|
+
background: 'rgba(7, 11, 22, 0.72)',
|
|
899
|
+
pointerEvents: 'none',
|
|
900
|
+
}, children: _jsxs("div", { style: { background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 10, padding: '16px 22px', textAlign: 'center' }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Loading gallery\u2026" }), _jsxs("div", { style: { fontSize: 13, color: theme.textDim }, children: [galleryLoading.done, " / ", galleryLoading.total || '…', " files", galleryLoading.failed > 0 ? ` · ${galleryLoading.failed} skipped` : ''] })] }) })), props.backLink && (_jsxs("a", { className: "qe-hover", href: props.backLink.href, style: {
|
|
901
|
+
position: 'absolute',
|
|
902
|
+
top: 12,
|
|
903
|
+
left: 12,
|
|
904
|
+
display: 'inline-flex',
|
|
905
|
+
alignItems: 'center',
|
|
906
|
+
gap: 6,
|
|
907
|
+
background: theme.panelBg,
|
|
908
|
+
border: `1px solid ${theme.border}`,
|
|
909
|
+
borderRadius: 8,
|
|
910
|
+
padding: '6px 12px',
|
|
911
|
+
fontSize: 13,
|
|
912
|
+
fontWeight: 500,
|
|
913
|
+
color: theme.text,
|
|
914
|
+
textDecoration: 'none',
|
|
915
|
+
fontFamily: theme.font,
|
|
916
|
+
}, children: [_jsx(ArrowLeftIcon, { style: iconStyle(14) }), props.backLink.label.replace(/^←\s*/, '')] })), _jsxs("div", { style: { position: 'absolute', top: 12, right: 12, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }, children: [_jsxs("div", { style: { display: 'flex', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4 }, children: [_jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Undo (Ctrl+Z)", disabled: !state?.history.canUndo || !binding, onClick: () => historyAction(state.history.undo()), children: _jsx(ArrowUturnLeftIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Redo (Ctrl+Shift+Z)", disabled: !state?.history.canRedo || !binding, onClick: () => historyAction(state.history.redo()), children: _jsx(ArrowUturnRightIcon, { style: iconStyle(14) }) })] }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4 }, children: GIZMO_MODE_BUTTONS.map((m) => (_jsx("button", { className: "qe-hover", style: { ...squareButtonStyle, ...(gizmoMode === m.value ? activeSquareButtonStyle : {}) }, title: m.title, onClick: () => setGizmoMode(m.value), children: _jsx(m.Icon, { style: iconStyle(14) }) }, m.value))) }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4 }, children: _jsx("button", { className: "qe-hover", style: { ...squareButtonStyle, ...(showEmitterWireframes ? activeSquareButtonStyle : {}) }, title: "Show emitter shape wireframes", disabled: !binding, onClick: () => setShowEmitterWireframes((on) => !on), children: _jsx(CubeTransparentIcon, { style: iconStyle(14) }) }) })] })] }), _jsxs("aside", { style: { width: 340, flexShrink: 0, borderLeft: `1px solid ${theme.border}`, background: theme.panelBg, display: 'flex', flexDirection: 'column', minHeight: 0 }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 600, color: theme.textDim, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 6 }, children: "Inspector" }), _jsx("div", { className: "qe-hover-bg", title: binding ? 'Rename effect' : undefined, style: { fontSize: 16, fontWeight: 600, cursor: binding ? 'pointer' : 'default', borderRadius: 6, padding: '2px 4px', margin: '-2px -4px' }, onClick: () => binding && setRenamingRoot(true), children: (binding?.root.name || props.title) ?? 'Effect editor' }), renamingRoot && binding && (_jsx(PromptDialog, { title: "Rename effect", defaultValue: binding.root.name, onSubmit: (name) => {
|
|
273
917
|
binding.apply(() => (binding.root.name = name));
|
|
274
918
|
setRenamingRoot(false);
|
|
275
|
-
}, onCancel: () => setRenamingRoot(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, onClick: () => {
|
|
919
|
+
}, onCancel: () => setRenamingRoot(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => {
|
|
276
920
|
if (!binding)
|
|
277
921
|
return;
|
|
278
922
|
const blob = new Blob([binding.exportJSON('EditorEffect')], { type: 'application/json' });
|
|
@@ -281,21 +925,7 @@ export function EffectEditorHost(props) {
|
|
|
281
925
|
link.download = 'effect.json';
|
|
282
926
|
link.click();
|
|
283
927
|
URL.revokeObjectURL(link.href);
|
|
284
|
-
}, children: "Export" }), _jsx("button", { className: "qe-hover", style: buttonStyle, onClick: () =>
|
|
285
|
-
const file = e.target.files?.[0];
|
|
286
|
-
e.target.value = '';
|
|
287
|
-
if (!file || !state)
|
|
288
|
-
return;
|
|
289
|
-
try {
|
|
290
|
-
const json = JSON.parse(await file.text());
|
|
291
|
-
historyAction(json);
|
|
292
|
-
state.history.clear();
|
|
293
|
-
force();
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
console.error('Failed to import effect JSON:', err);
|
|
297
|
-
}
|
|
298
|
-
} })] })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto', padding: '6px 14px 20px' }, children: binding && activeSystem && gizmoNode && (_jsx(EffectEditor, { binding: binding, selectedSystem: activeSystem, gizmoTargetNode: gizmoNode, gizmoTargetLabel: gizmoTargetLabel, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture && state ? (url) => props.resolveTexture(url, state.scene) : undefined, resolveGeometry: props.resolveGeometry && state ? (file) => props.resolveGeometry(file, state.scene) : undefined })) })] })] }), binding && activeSystem && state && (_jsx(TimelinePanel, { binding: binding, selectedNode: selectedNode, onSelectNode: setSelectedNode, scene: state.scene, renderer: state.renderer, playbackRef: playbackRef, playheadRef: playheadRef, counterRef: counterRef, paused: paused, setPaused: setPaused, speed: speed, setSpeed: setSpeed }))] }));
|
|
928
|
+
}, children: "Export" }), props.onSave && (_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => binding && props.onSave(binding.exportJSON('EditorEffect')), children: "Save" }))] })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '6px 14px 20px' }, children: binding && activeSystem && gizmoNode ? (_jsx(EffectEditor, { binding: binding, selectedSystem: activeSystem, gizmoTargetNode: gizmoNode, gizmoTargetLabel: gizmoTargetLabel, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture ? resolveTextureForEditor : undefined, resolveGeometry: props.resolveGeometry ? resolveGeometryForEditor : undefined })) : (_jsx("div", { style: { fontSize: 13, color: theme.textDim, lineHeight: 1.5 }, children: "No effect selected." })) })] })] }), showTimeline && binding && activeSystem && state && (_jsx(TimelinePanel, { binding: binding, selectedNode: selectedNode, onSelectNode: setSelectedNode, scene: state.scene, renderer: state.renderer, playbackRef: playbackRef, playheadRef: playheadRef, counterRef: counterRef, stagePlaying: stagePlaying, setStagePlaying: setStagePlaying, speed: speed, setSpeed: setSpeed, playbackEnabled: timelinePlaybackEnabled })), messageDialog && (_jsx(MessageDialog, { title: messageDialog.title, message: messageDialog.message, onClose: () => setMessageDialog(null) }))] }));
|
|
299
929
|
}
|
|
300
930
|
/** NME-style imperative entry point for hosts like BabylonJS Editor windows. */
|
|
301
931
|
export const QuarksEffectEditor = {
|