babylon.quarks-editor 0.17.7 → 0.17.8
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/timeline.d.ts +1 -1
- package/dist/core/timeline.js +3 -2
- package/dist/react/EffectEditor.d.ts +3 -0
- package/dist/react/EffectEditor.js +6 -1
- package/dist/react/EffectEditorHost.js +72 -11
- package/dist/react/ObjectModule.d.ts +6 -0
- package/dist/react/ObjectModule.js +28 -0
- package/dist/react/TimelinePanel.d.ts +4 -3
- package/dist/react/TimelinePanel.js +60 -10
- package/dist/react/TimelineTrackRow.d.ts +2 -2
- package/dist/react/TimelineTrackRow.js +16 -12
- package/dist/react/theme.js +44 -21
- package/package.json +68 -63
- package/src/core/binding.ts +105 -105
- package/src/core/collision.ts +40 -40
- package/src/core/groups.ts +29 -29
- package/src/core/loadEffect.ts +102 -102
- package/src/core/scrub.ts +28 -28
- package/src/core/shapes.ts +133 -133
- package/src/core/systems.ts +98 -98
- package/src/core/timeline.ts +95 -94
- package/src/core/values.ts +113 -113
- package/src/index.ts +32 -32
- package/src/react/CurveEditor.tsx +168 -168
- package/src/react/EffectEditor.tsx +125 -111
- package/src/react/EffectEditorHost.tsx +466 -377
- package/src/react/GradientEditor.tsx +127 -127
- package/src/react/ModuleSection.tsx +54 -54
- package/src/react/ObjectModule.tsx +47 -0
- package/src/react/PromptDialog.tsx +85 -85
- package/src/react/TimelinePanel.tsx +412 -354
- package/src/react/TimelineTrackRow.tsx +291 -267
- package/src/react/behaviorModules.tsx +658 -658
- package/src/react/fields.tsx +76 -76
- package/src/react/index.ts +43 -43
- package/src/react/modules.tsx +1034 -1034
- package/src/react/theme.ts +108 -85
package/dist/core/timeline.d.ts
CHANGED
|
@@ -31,5 +31,5 @@ export declare function computeStartOffset(gen: ValueGenerator | FunctionValueGe
|
|
|
31
31
|
};
|
|
32
32
|
/** Flattens the effect's group/emitter tree into display rows, preserving tree-walk order. */
|
|
33
33
|
export declare function computeTimelineRows(binding: EffectBinding): TimelineRow[];
|
|
34
|
-
/** Overall
|
|
34
|
+
/** Overall timeline span: the furthest track extent (start offset + duration). */
|
|
35
35
|
export declare function computeTimelineSpan(rows: TimelineRow[]): number;
|
package/dist/core/timeline.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConstantValue, IntervalValue } from 'babylon.quarks';
|
|
2
|
-
|
|
2
|
+
/** Guards against a literally zero-width ruler (e.g. no tracks yet) — not a usability stretch. */
|
|
3
|
+
const MIN_TIMELINE_SPAN = 0.01;
|
|
3
4
|
/**
|
|
4
5
|
* Reads a representative start-delay value for display without invoking the generator's
|
|
5
6
|
* stateful `genValue` (IntervalValue caches its sampled slot on first call and returns NaN
|
|
@@ -57,7 +58,7 @@ export function computeTimelineRows(binding) {
|
|
|
57
58
|
walk(binding.getTree());
|
|
58
59
|
return rows;
|
|
59
60
|
}
|
|
60
|
-
/** Overall
|
|
61
|
+
/** Overall timeline span: the furthest track extent (start offset + duration). */
|
|
61
62
|
export function computeTimelineSpan(rows) {
|
|
62
63
|
let span = 0;
|
|
63
64
|
for (const row of rows) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
2
3
|
import { EffectBinding } from '../core/binding';
|
|
3
4
|
import type { GeometryData, GeometryOption, TextureOption } from './modules';
|
|
4
5
|
import type { ParticleSystem } from 'babylon.quarks';
|
|
@@ -6,6 +7,8 @@ export interface EffectEditorProps {
|
|
|
6
7
|
binding: EffectBinding;
|
|
7
8
|
/** Currently selected system, lifted up so the timeline panel and this module stack agree. */
|
|
8
9
|
selectedSystem: ParticleSystem;
|
|
10
|
+
gizmoTargetNode: TransformNode;
|
|
11
|
+
gizmoTargetLabel: string;
|
|
9
12
|
/** Texture presets offered in the Renderer module (host supplies the loader). */
|
|
10
13
|
textureOptions?: TextureOption[];
|
|
11
14
|
/** Mesh presets offered for the Mesh render mode (host supplies the geometry buffers). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useSyncExternalStore } from 'react';
|
|
3
3
|
import { EffectBinding } from '../core/binding';
|
|
4
|
+
import { ObjectModule } from './ObjectModule';
|
|
4
5
|
import { ColorOverLifeModule, EmissionModule, MainModule, RendererModule, ShapeModule, SizeOverLifeModule, } from './modules';
|
|
5
6
|
import { CollisionModule, ColorBySpeedModule, EmitDirectionModule, ForceOverLifeModule, GravityModule, InheritVelocityModule, LimitSpeedOverLifeModule, NoiseModule, OrbitOverLifeModule, RotationBySpeedModule, RotationOverLifeModule, SizeBySpeedModule, SpeedOverLifeModule, TextureSheetModule, TurbulenceModule, VelocityOverLifeModule, WidthOverTrailModule, } from './behaviorModules';
|
|
6
7
|
import { SubEmittersModule } from './SubEmittersModule';
|
|
@@ -26,5 +27,9 @@ export function EffectEditor(props) {
|
|
|
26
27
|
return nested;
|
|
27
28
|
}, [props.binding, activeSystem]);
|
|
28
29
|
useSyncExternalStore((onStoreChange) => binding.subscribe(onStoreChange), () => binding.getRevision());
|
|
29
|
-
|
|
30
|
+
const gizmoTargetIsEmitter = allSystems.some((s) => s.emitter === props.gizmoTargetNode);
|
|
31
|
+
if (!gizmoTargetIsEmitter) {
|
|
32
|
+
return (_jsx("div", { style: { fontFamily: theme.font, color: theme.text }, children: _jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel }) }));
|
|
33
|
+
}
|
|
34
|
+
return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel }), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
|
|
30
35
|
}
|
|
@@ -9,6 +9,8 @@ import { Vector3 as BVector3 } from '@babylonjs/core/Maths/math.vector';
|
|
|
9
9
|
import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
|
|
10
10
|
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
|
|
11
11
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
12
|
+
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
|
|
13
|
+
import { UtilityLayerRenderer } from '@babylonjs/core/Rendering/utilityLayerRenderer';
|
|
12
14
|
import { GridMaterial } from '@babylonjs/materials/grid/gridMaterial';
|
|
13
15
|
import { BatchedRenderer, ColorOverLife, ConeEmitter, ConstantColor, ConstantValue, IntervalValue, ParticleSystem, RenderMode, Vector4, } from 'babylon.quarks';
|
|
14
16
|
import { EffectBinding } from '../core/binding';
|
|
@@ -54,6 +56,26 @@ function disposeEffect(root, systems, renderer) {
|
|
|
54
56
|
root.dispose(false, true);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
59
|
+
const GIZMO_MODE_BUTTONS = [
|
|
60
|
+
{ value: 'none', icon: '↖', title: 'Select (hide stage gizmo)' },
|
|
61
|
+
{ value: 'position', icon: 'M', title: 'Move the stage node (position)' },
|
|
62
|
+
{ value: 'rotation', icon: 'R', title: 'Rotate the stage node' },
|
|
63
|
+
{ value: 'scale', icon: 'S', title: 'Scale the stage node' },
|
|
64
|
+
];
|
|
65
|
+
const squareButtonStyle = {
|
|
66
|
+
...buttonStyle,
|
|
67
|
+
width: 28,
|
|
68
|
+
height: 28,
|
|
69
|
+
padding: 0,
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
justifyContent: 'center',
|
|
73
|
+
fontWeight: 700,
|
|
74
|
+
};
|
|
75
|
+
const activeSquareButtonStyle = {
|
|
76
|
+
border: `1px solid ${theme.borderActive}`,
|
|
77
|
+
background: 'rgba(120, 165, 255, 0.35)',
|
|
78
|
+
};
|
|
57
79
|
/**
|
|
58
80
|
* Self-contained effect editor: live Babylon preview + hierarchy/inspector sidebar.
|
|
59
81
|
* Designed for embedding — see QuarksEffectEditor.Show for the NME-style entry point.
|
|
@@ -64,13 +86,15 @@ export function EffectEditorHost(props) {
|
|
|
64
86
|
const playheadRef = useRef(null);
|
|
65
87
|
const importRef = useRef(null);
|
|
66
88
|
const stateRef = useRef(null);
|
|
89
|
+
const gizmoManagerRef = useRef(null);
|
|
67
90
|
const [binding, setBinding] = useState(null);
|
|
68
91
|
const [, force] = useReducer((x) => x + 1, 0);
|
|
69
92
|
const playbackRef = useRef({ paused: false, speed: 1, stepQueued: false, elapsed: 0, scrubbing: false });
|
|
70
93
|
const [paused, setPaused] = useState(false);
|
|
71
94
|
const [speed, setSpeed] = useState(1);
|
|
72
|
-
const [selectedSystem, setSelectedSystem] = useState(null);
|
|
73
95
|
const [renamingRoot, setRenamingRoot] = useState(false);
|
|
96
|
+
const [gizmoMode, setGizmoMode] = useState('none');
|
|
97
|
+
const [selectedNode, setSelectedNode] = useState(null);
|
|
74
98
|
useEffect(() => {
|
|
75
99
|
// Register the shared ground-plane collider before any effect (default or imported)
|
|
76
100
|
// is built, so ApplyCollision behaviors deserialized by QuarksLoader resolve against it.
|
|
@@ -98,13 +122,21 @@ export function EffectEditorHost(props) {
|
|
|
98
122
|
groundMaterial.opacity = 0.5;
|
|
99
123
|
const ground = MeshBuilder.CreateGround('ground', { width: 100, height: 100 }, scene);
|
|
100
124
|
ground.material = groundMaterial;
|
|
125
|
+
const stageRoot = new TransformNode('StageRoot', scene);
|
|
126
|
+
const gizmoUtilityLayer = new UtilityLayerRenderer(scene);
|
|
127
|
+
const gizmoManager = new GizmoManager(scene, undefined, gizmoUtilityLayer);
|
|
128
|
+
gizmoManager.usePointerToAttachGizmos = false;
|
|
129
|
+
gizmoManager.attachToNode(stageRoot);
|
|
130
|
+
gizmoManagerRef.current = gizmoManager;
|
|
101
131
|
const renderer = new BatchedRenderer('quarks-editor', scene);
|
|
102
132
|
const history = new EffectHistory();
|
|
103
133
|
const mount = (nextBinding) => {
|
|
134
|
+
nextBinding.root.parent = stageRoot;
|
|
104
135
|
history.attach(nextBinding);
|
|
105
136
|
nextBinding.subscribe(force);
|
|
106
|
-
stateRef.current = { engine, scene, renderer, binding: nextBinding, history };
|
|
137
|
+
stateRef.current = { engine, scene, renderer, binding: nextBinding, history, stageRoot };
|
|
107
138
|
setBinding(nextBinding);
|
|
139
|
+
setSelectedNode(nextBinding.system.emitter);
|
|
108
140
|
props.onReady?.(makeHandle());
|
|
109
141
|
};
|
|
110
142
|
const makeHandle = () => ({
|
|
@@ -169,7 +201,8 @@ export function EffectEditorHost(props) {
|
|
|
169
201
|
counterRef.current.textContent = `${count} particles · ${playback.elapsed.toFixed(2)}s`;
|
|
170
202
|
}
|
|
171
203
|
});
|
|
172
|
-
const
|
|
204
|
+
const resizeObserver = new ResizeObserver(() => engine.resize());
|
|
205
|
+
resizeObserver.observe(canvasRef.current);
|
|
173
206
|
const onKey = (e) => {
|
|
174
207
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
|
|
175
208
|
e.preventDefault();
|
|
@@ -180,16 +213,43 @@ export function EffectEditorHost(props) {
|
|
|
180
213
|
force();
|
|
181
214
|
}
|
|
182
215
|
};
|
|
183
|
-
window.addEventListener('resize', onResize);
|
|
184
216
|
window.addEventListener('keydown', onKey);
|
|
185
217
|
return () => {
|
|
186
|
-
|
|
218
|
+
resizeObserver.disconnect();
|
|
187
219
|
window.removeEventListener('keydown', onKey);
|
|
220
|
+
gizmoManager.dispose();
|
|
221
|
+
gizmoUtilityLayer.dispose();
|
|
188
222
|
engine.dispose();
|
|
189
223
|
};
|
|
190
224
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
191
225
|
}, []);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
const gm = gizmoManagerRef.current;
|
|
228
|
+
if (!gm)
|
|
229
|
+
return;
|
|
230
|
+
gm.positionGizmoEnabled = gizmoMode === 'position';
|
|
231
|
+
gm.rotationGizmoEnabled = gizmoMode === 'rotation';
|
|
232
|
+
gm.scaleGizmoEnabled = gizmoMode === 'scale';
|
|
233
|
+
const activeGizmo = gizmoMode === 'position' ? gm.gizmos.positionGizmo : gizmoMode === 'rotation' ? gm.gizmos.rotationGizmo : gizmoMode === 'scale' ? gm.gizmos.scaleGizmo : null;
|
|
234
|
+
const observer = activeGizmo?.onDragObservable.add(() => force());
|
|
235
|
+
return () => {
|
|
236
|
+
if (activeGizmo && observer)
|
|
237
|
+
activeGizmo.onDragObservable.remove(observer);
|
|
238
|
+
};
|
|
239
|
+
}, [gizmoMode]);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const gm = gizmoManagerRef.current;
|
|
242
|
+
const stageRoot = stateRef.current?.stageRoot;
|
|
243
|
+
if (!gm || !stageRoot)
|
|
244
|
+
return;
|
|
245
|
+
if (selectedNode && selectedNode.isDisposed()) {
|
|
246
|
+
setSelectedNode(stateRef.current?.binding.system.emitter ?? null);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
gm.attachToNode(selectedNode ?? stageRoot);
|
|
250
|
+
}, [selectedNode, binding]);
|
|
192
251
|
const state = stateRef.current;
|
|
252
|
+
const gizmoNode = selectedNode && !selectedNode.isDisposed() ? selectedNode : state?.stageRoot;
|
|
193
253
|
const historyAction = (json) => {
|
|
194
254
|
if (json && state) {
|
|
195
255
|
const current = state.binding;
|
|
@@ -197,21 +257,22 @@ export function EffectEditorHost(props) {
|
|
|
197
257
|
disposeEffect(current.root, [current.system, ...current.subSystems], state.renderer);
|
|
198
258
|
const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
|
|
199
259
|
const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
|
|
260
|
+
next.root.parent = state.stageRoot;
|
|
200
261
|
state.history.attach(next);
|
|
201
262
|
next.subscribe(force);
|
|
202
263
|
state.binding = next;
|
|
203
264
|
setBinding(next);
|
|
265
|
+
setSelectedNode(next.system.emitter);
|
|
204
266
|
}
|
|
205
267
|
force();
|
|
206
268
|
};
|
|
207
|
-
// Selection survives re-renders but falls back to root if the system was removed/replaced
|
|
208
|
-
// (e.g. after undo/redo or import swaps the whole binding out from under it).
|
|
209
269
|
const allSystems = binding ? [binding.system, ...binding.subSystems] : [];
|
|
210
|
-
const activeSystem = binding ? (
|
|
211
|
-
|
|
270
|
+
const activeSystem = binding ? (allSystems.find((s) => s.emitter === selectedNode) ?? binding.system) : null;
|
|
271
|
+
const gizmoTargetLabel = selectedNode ? gizmoNode?.name || 'Node' : 'Stage (preview parent)';
|
|
272
|
+
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("div", { style: { position: 'relative', flex: 1, minWidth: 0 }, children: [_jsx("canvas", { ref: canvasRef, style: { width: '100%', height: '100%', display: 'block', touchAction: 'none', outline: 'none' } }), _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, onClick: () => historyAction(state.history.undo()), children: "\u21B6" }), _jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Redo (Ctrl+Shift+Z)", disabled: !state?.history.canRedo, onClick: () => historyAction(state.history.redo()), children: "\u21B7" })] }), _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: m.icon }, m.value))) })] })] }), _jsxs("aside", { style: { width: 340, flexShrink: 0, borderLeft: `1px solid ${theme.border}`, background: theme.panelBg, display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_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) => {
|
|
212
273
|
binding.apply(() => (binding.root.name = name));
|
|
213
274
|
setRenamingRoot(false);
|
|
214
|
-
}, onCancel: () => setRenamingRoot(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle,
|
|
275
|
+
}, onCancel: () => setRenamingRoot(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, onClick: () => {
|
|
215
276
|
if (!binding)
|
|
216
277
|
return;
|
|
217
278
|
const blob = new Blob([binding.exportJSON('EditorEffect')], { type: 'application/json' });
|
|
@@ -234,7 +295,7 @@ export function EffectEditorHost(props) {
|
|
|
234
295
|
catch (err) {
|
|
235
296
|
console.error('Failed to import effect JSON:', err);
|
|
236
297
|
}
|
|
237
|
-
} })] })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto', padding: '6px 14px 20px' }, children: binding && activeSystem && (_jsx(EffectEditor, { binding: binding, selectedSystem: activeSystem, 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,
|
|
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 }))] }));
|
|
238
299
|
}
|
|
239
300
|
/** NME-style imperative entry point for hosts like BabylonJS Editor windows. */
|
|
240
301
|
export const QuarksEffectEditor = {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Quaternion } from '@babylonjs/core/Maths/math.vector';
|
|
3
|
+
import { ModuleSection } from './ModuleSection';
|
|
4
|
+
import { NumberField, Row } from './fields';
|
|
5
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
6
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
7
|
+
function getEulerDegrees(node) {
|
|
8
|
+
const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation;
|
|
9
|
+
return { x: euler.x * RAD_TO_DEG, y: euler.y * RAD_TO_DEG, z: euler.z * RAD_TO_DEG };
|
|
10
|
+
}
|
|
11
|
+
function setEulerDegreeAxis(node, axis, degrees) {
|
|
12
|
+
const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation.clone();
|
|
13
|
+
euler[axis] = degrees * DEG_TO_RAD;
|
|
14
|
+
if (node.rotationQuaternion) {
|
|
15
|
+
node.rotationQuaternion = Quaternion.FromEulerVector(euler);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
node.rotation = euler;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function AxisRow(props) {
|
|
22
|
+
return (_jsx(Row, { label: props.label, children: _jsxs("div", { style: { display: 'flex', gap: 4 }, children: [_jsx(NumberField, { value: props.values.x, step: props.step, onChange: (v) => props.onChange('x', v) }), _jsx(NumberField, { value: props.values.y, step: props.step, onChange: (v) => props.onChange('y', v) }), _jsx(NumberField, { value: props.values.z, step: props.step, onChange: (v) => props.onChange('z', v) })] }) }));
|
|
23
|
+
}
|
|
24
|
+
export function ObjectModule(props) {
|
|
25
|
+
const { node } = props;
|
|
26
|
+
const euler = getEulerDegrees(node);
|
|
27
|
+
return (_jsxs(ModuleSection, { title: `Object — ${props.label}`, children: [_jsx(AxisRow, { label: "Position", values: { x: node.position.x, y: node.position.y, z: node.position.z }, step: 0.1, onChange: (axis, v) => (node.position[axis] = v) }), _jsx(AxisRow, { label: "Rotation", values: euler, step: 1, onChange: (axis, v) => setEulerDegreeAxis(node, axis, v) }), _jsx(AxisRow, { label: "Scale", values: { x: node.scaling.x, y: node.scaling.y, z: node.scaling.z }, step: 0.1, onChange: (axis, v) => (node.scaling[axis] = v) })] }));
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BatchedRenderer } from 'babylon.quarks';
|
|
3
3
|
import type { Scene } from '@babylonjs/core/scene';
|
|
4
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
4
5
|
import type { EffectBinding } from '../core/binding';
|
|
5
6
|
export interface PlaybackState {
|
|
6
7
|
paused: boolean;
|
|
@@ -11,8 +12,8 @@ export interface PlaybackState {
|
|
|
11
12
|
}
|
|
12
13
|
export interface TimelinePanelProps {
|
|
13
14
|
binding: EffectBinding;
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
selectedNode: TransformNode | null;
|
|
16
|
+
onSelectNode: (node: TransformNode | null) => void;
|
|
16
17
|
scene: Scene;
|
|
17
18
|
renderer: BatchedRenderer;
|
|
18
19
|
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
@@ -7,9 +7,12 @@ import { TimelineTrackRow } from './TimelineTrackRow';
|
|
|
7
7
|
import { buttonStyle, theme } from './theme';
|
|
8
8
|
const LABEL_WIDTH = 200;
|
|
9
9
|
const RULER_HEIGHT = 22;
|
|
10
|
+
const STAGE_ROW_HEIGHT = 26;
|
|
10
11
|
const PANEL_HEIGHT = 220;
|
|
11
12
|
/** Small gap between the label column and the first pixel of the time axis. */
|
|
12
13
|
export const TIMELINE_INSET = 10;
|
|
14
|
+
/** Gap reserved at the right edge so the full-duration mark doesn't sit flush against the panel edge. */
|
|
15
|
+
const RIGHT_PADDING = 16;
|
|
13
16
|
function pickTickInterval(pxPerSecond) {
|
|
14
17
|
if (pxPerSecond >= 60)
|
|
15
18
|
return 1;
|
|
@@ -24,7 +27,7 @@ function pickTickInterval(pxPerSecond) {
|
|
|
24
27
|
* draggable playhead (approximate scrub — see core/scrub.ts), and one track row per system.
|
|
25
28
|
*/
|
|
26
29
|
export function TimelinePanel(props) {
|
|
27
|
-
const { binding,
|
|
30
|
+
const { binding, selectedNode, onSelectNode, scene, renderer, playbackRef, playheadRef, counterRef, paused, setPaused, speed, setSpeed } = props;
|
|
28
31
|
const [bodyWidthPx, setBodyWidthPx] = useState(600);
|
|
29
32
|
const [collapsedGroups, setCollapsedGroups] = useState(() => new Set());
|
|
30
33
|
const [hiddenSystems, setHiddenSystems] = useState(() => new Set());
|
|
@@ -87,24 +90,53 @@ export function TimelinePanel(props) {
|
|
|
87
90
|
return () => observer.disconnect();
|
|
88
91
|
}, []);
|
|
89
92
|
const rows = useMemo(() => computeTimelineRows(binding), [binding, binding.getRevision()]);
|
|
93
|
+
// The ruler spans exactly the furthest track extent (no artificial stretch), minus a
|
|
94
|
+
// right-hand gap so the full-duration mark never sits flush against the panel edge.
|
|
90
95
|
const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
|
|
91
|
-
const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET) / timelineSpan;
|
|
96
|
+
const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET - RIGHT_PADDING) / timelineSpan;
|
|
92
97
|
// Follows playbackRef.elapsed independently of Babylon's render loop (Host only owns
|
|
93
|
-
// simulation time; this owns translating it into pixels for the playhead line).
|
|
94
|
-
//
|
|
95
|
-
//
|
|
98
|
+
// simulation time; this owns translating it into pixels for the playhead line). Whether
|
|
99
|
+
// the *whole* effect loops is an OR across tracks (there's no single canonical "root" —
|
|
100
|
+
// an effect can have several top-level systems): if any track loops, elapsed grows
|
|
101
|
+
// unbounded and we wrap it at timelineSpan. If none loop, finishing a run snaps playback
|
|
102
|
+
// straight back to frame 0 (paused) — a one-shot preview parked and ready for the next Play.
|
|
103
|
+
const anyLooping = useMemo(() => rows.some((r) => r.kind === 'track' && r.looping), [rows]);
|
|
104
|
+
// Tracks elapsed from the previous tick so the finish-reset below only fires on a genuine
|
|
105
|
+
// crossing during live playback. Without this, manually dragging the playhead to (or past)
|
|
106
|
+
// the end pins elapsed at timelineSpan while scrubbing=true skips the reset — but the very
|
|
107
|
+
// next tick after release would otherwise see elapsed >= timelineSpan with no memory of
|
|
108
|
+
// *how* it got there, and wrongly treat a drag as a finished run.
|
|
109
|
+
const prevElapsedRef = useRef(0);
|
|
96
110
|
React.useEffect(() => {
|
|
97
111
|
let raf;
|
|
98
112
|
const tick = () => {
|
|
113
|
+
const state = playbackRef.current;
|
|
114
|
+
const elapsed = state.elapsed;
|
|
115
|
+
let shown = elapsed;
|
|
116
|
+
if (anyLooping) {
|
|
117
|
+
shown = timelineSpan > 0 ? elapsed % timelineSpan : 0;
|
|
118
|
+
}
|
|
119
|
+
else if (elapsed >= timelineSpan) {
|
|
120
|
+
shown = timelineSpan;
|
|
121
|
+
const justFinishedPlaying = !state.scrubbing && !state.paused && prevElapsedRef.current < timelineSpan;
|
|
122
|
+
if (justFinishedPlaying) {
|
|
123
|
+
scrubTo(binding, renderer, 0);
|
|
124
|
+
state.elapsed = 0;
|
|
125
|
+
state.paused = true;
|
|
126
|
+
setPaused(true);
|
|
127
|
+
scene.render();
|
|
128
|
+
shown = 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
prevElapsedRef.current = elapsed;
|
|
99
132
|
if (playheadRef.current) {
|
|
100
|
-
|
|
101
|
-
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + wrapped * pxPerSecond}px`;
|
|
133
|
+
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + shown * pxPerSecond}px`;
|
|
102
134
|
}
|
|
103
135
|
raf = requestAnimationFrame(tick);
|
|
104
136
|
};
|
|
105
137
|
raf = requestAnimationFrame(tick);
|
|
106
138
|
return () => cancelAnimationFrame(raf);
|
|
107
|
-
}, [pxPerSecond, timelineSpan, playbackRef, playheadRef]);
|
|
139
|
+
}, [pxPerSecond, timelineSpan, anyLooping, playbackRef, playheadRef, setPaused, binding, renderer, scene]);
|
|
108
140
|
const visibleRows = useMemo(() => {
|
|
109
141
|
let hideUntilDepth = null;
|
|
110
142
|
const visible = rows.filter((row) => {
|
|
@@ -174,6 +206,8 @@ export function TimelinePanel(props) {
|
|
|
174
206
|
fontFamily: theme.font,
|
|
175
207
|
color: theme.text,
|
|
176
208
|
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsx("button", { className: "qe-hover", style: overlayButton, title: paused ? 'Play' : 'Pause', onClick: () => {
|
|
209
|
+
// A finished one-shot effect already snapped itself back to frame 0 and
|
|
210
|
+
// paused (see the tick effect above), so a plain toggle is enough here.
|
|
177
211
|
playbackRef.current.paused = !paused;
|
|
178
212
|
setPaused(!paused);
|
|
179
213
|
}, children: paused ? '▶' : '⏸' }), _jsx("button", { className: "qe-hover", style: overlayButton, title: "Step one frame", disabled: !paused, onClick: () => (playbackRef.current.stepQueued = true), children: "\u23ED" }), _jsx("button", { className: "qe-hover", style: overlayButton, title: "Restart", onClick: restart, children: "\u27F2" }), _jsx("select", { className: "qe-hover", style: { ...overlayButton, padding: '5px 4px' }, title: "Playback speed", value: speed, onChange: (e) => {
|
|
@@ -200,11 +234,27 @@ export function TimelinePanel(props) {
|
|
|
200
234
|
}, onPointerUp: (e) => {
|
|
201
235
|
playbackRef.current.scrubbing = false;
|
|
202
236
|
e.target.releasePointerCapture?.(e.pointerId);
|
|
203
|
-
}, children: ticks.map((t) => (_jsxs("div", { style: { position: 'absolute', left: TIMELINE_INSET + t * pxPerSecond, top: 0, bottom: 0, borderLeft: `1px solid ${theme.border}`, fontSize: 10, color: theme.textDim, paddingLeft: 3 }, children: [t, "s"] }, t))) })] }),
|
|
237
|
+
}, children: ticks.map((t) => (_jsxs("div", { style: { position: 'absolute', left: TIMELINE_INSET + t * pxPerSecond, top: 0, bottom: 0, borderLeft: `1px solid ${theme.border}`, fontSize: 10, color: theme.textDim, paddingLeft: 3 }, children: [t, "s"] }, t))) })] }), _jsxs("div", { className: "qe-hover-bg", title: "Preview parent \u2014 simulates effect.parent = someNode; not saved in the exported JSON", style: {
|
|
204
238
|
position: 'absolute',
|
|
205
239
|
top: RULER_HEIGHT,
|
|
206
240
|
left: 0,
|
|
207
241
|
right: 0,
|
|
242
|
+
height: STAGE_ROW_HEIGHT,
|
|
243
|
+
display: 'flex',
|
|
244
|
+
alignItems: 'center',
|
|
245
|
+
gap: 6,
|
|
246
|
+
padding: '0 8px',
|
|
247
|
+
fontSize: 12.5,
|
|
248
|
+
fontStyle: 'italic',
|
|
249
|
+
cursor: 'pointer',
|
|
250
|
+
color: selectedNode === null ? theme.text : theme.textDim,
|
|
251
|
+
borderBottom: `1px solid ${theme.border}`,
|
|
252
|
+
...(selectedNode === null ? { background: 'rgba(60, 105, 209, 0.3)' } : {}),
|
|
253
|
+
}, onClick: () => onSelectNode(null), children: [_jsx("span", { style: { fontSize: 10, width: 10 }, children: "\u25CE" }), _jsx("span", { children: "Stage (preview parent)" })] }), _jsx("div", { style: {
|
|
254
|
+
position: 'absolute',
|
|
255
|
+
top: RULER_HEIGHT + STAGE_ROW_HEIGHT,
|
|
256
|
+
left: 0,
|
|
257
|
+
right: 0,
|
|
208
258
|
bottom: 0,
|
|
209
259
|
overflowY: 'auto',
|
|
210
260
|
overflowX: 'hidden',
|
|
@@ -212,7 +262,7 @@ export function TimelinePanel(props) {
|
|
|
212
262
|
display: 'grid',
|
|
213
263
|
gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
|
|
214
264
|
gridAutoRows: 'min-content',
|
|
215
|
-
}, children: visibleRows.map((row) => (_jsx(TimelineTrackRow, { row: row, binding: binding, selected: row.
|
|
265
|
+
}, children: visibleRows.map((row) => (_jsx(TimelineTrackRow, { row: row, binding: binding, selected: row.node === selectedNode, onSelectNode: onSelectNode, pxPerSecond: pxPerSecond, offsetPx: TIMELINE_INSET, collapsed: row.kind === 'group' && collapsedGroups.has(row.node), onToggleCollapse: row.kind === 'group' ? () => toggleCollapse(row.node) : undefined, hidden: row.kind === 'track' && hiddenSystems.has(row.system), onToggleVisible: row.kind === 'track' ? () => toggleVisible(row.system) : undefined, groupSelected: groupSelection.has(row.node), onToggleGroupSelect: () => toggleGroupSelect(row.node) }, row.node.uniqueId))) }) }), _jsx("div", { style: { position: 'absolute', top: 0, bottom: 0, left: LABEL_WIDTH, width: 1, background: theme.border, pointerEvents: 'none', zIndex: 3 } }), _jsx("div", { ref: playheadRef, style: {
|
|
216
266
|
position: 'absolute',
|
|
217
267
|
top: 0,
|
|
218
268
|
bottom: 0,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
3
3
|
import type { EffectBinding } from '../core/binding';
|
|
4
4
|
import type { TimelineRow } from '../core/timeline';
|
|
5
5
|
export interface TimelineTrackRowProps {
|
|
6
6
|
row: TimelineRow;
|
|
7
7
|
binding: EffectBinding;
|
|
8
8
|
selected: boolean;
|
|
9
|
-
|
|
9
|
+
onSelectNode: (node: TransformNode | null) => void;
|
|
10
10
|
pxPerSecond: number;
|
|
11
11
|
/** Matches the ruler's left gutter so bars line up under the correct tick. */
|
|
12
12
|
offsetPx: number;
|
|
@@ -10,7 +10,7 @@ const ROW_HEIGHT = 28;
|
|
|
10
10
|
const checkboxButton = { ...iconButton, fontSize: 12, width: 12 };
|
|
11
11
|
/** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
|
|
12
12
|
export function TimelineTrackRow(props) {
|
|
13
|
-
const { row, binding, selected,
|
|
13
|
+
const { row, binding, selected, onSelectNode, pxPerSecond, offsetPx, collapsed, onToggleCollapse, hidden, onToggleVisible, groupSelected, onToggleGroupSelect } = props;
|
|
14
14
|
const [renaming, setRenaming] = useState(false);
|
|
15
15
|
const isRoot = row.node === binding.root;
|
|
16
16
|
const labelStyle = {
|
|
@@ -23,17 +23,21 @@ export function TimelineTrackRow(props) {
|
|
|
23
23
|
borderRadius: 6,
|
|
24
24
|
cursor: row.kind === 'track' ? 'pointer' : onToggleCollapse ? 'pointer' : 'default',
|
|
25
25
|
fontSize: 12.5,
|
|
26
|
-
color:
|
|
26
|
+
color: selected ? theme.text : theme.textDim,
|
|
27
27
|
whiteSpace: 'nowrap',
|
|
28
|
+
boxSizing: 'border-box',
|
|
28
29
|
// Left unset (rather than 'transparent') when not selected so the .qe-row-bg hover
|
|
29
30
|
// tint isn't shadowed by an inline background of the same property.
|
|
30
|
-
...(
|
|
31
|
+
...(selected ? { background: 'rgba(60, 105, 209, 0.3)' } : {}),
|
|
31
32
|
};
|
|
32
33
|
if (row.kind === 'group') {
|
|
33
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, borderTop: `1px solid ${theme.border}` }, onClick:
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, borderTop: `1px solid ${theme.border}` }, onClick: () => onSelectNode(row.node), children: [!isRoot && (_jsx("button", { className: "qe-hover", title: groupSelected ? 'Remove from group selection' : 'Select for grouping', style: { ...checkboxButton, color: groupSelected ? theme.accent : theme.textDim }, onClick: (e) => {
|
|
34
35
|
e.stopPropagation();
|
|
35
36
|
onToggleGroupSelect();
|
|
36
|
-
}, children: groupSelected ? '☑' : '☐' })), _jsx("span", {
|
|
37
|
+
}, children: groupSelected ? '☑' : '☐' })), _jsx("span", { className: "qe-hover", title: collapsed ? 'Expand' : 'Collapse', style: { color: theme.textDim, fontSize: 10, width: 10, cursor: 'pointer' }, onClick: (e) => {
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
onToggleCollapse?.();
|
|
40
|
+
}, children: collapsed ? '▸' : '▾' }), _jsx("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: row.name }), _jsx("button", { className: "qe-hover", title: "Add system to this group", style: { ...iconButton, color: theme.accent, fontSize: 13 }, onClick: (e) => {
|
|
37
41
|
e.stopPropagation();
|
|
38
42
|
let created;
|
|
39
43
|
binding.apply(() => {
|
|
@@ -45,7 +49,7 @@ export function TimelineTrackRow(props) {
|
|
|
45
49
|
});
|
|
46
50
|
if (created) {
|
|
47
51
|
binding.addSubSystem(created);
|
|
48
|
-
|
|
52
|
+
onSelectNode(created.emitter);
|
|
49
53
|
}
|
|
50
54
|
}, children: "+" }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
|
|
51
55
|
e.stopPropagation();
|
|
@@ -55,9 +59,9 @@ export function TimelineTrackRow(props) {
|
|
|
55
59
|
binding.apply(() => ungroupNode(row.node));
|
|
56
60
|
}, children: "\u26F6" })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Remove group", style: { ...iconButton, color: '#e08c8c', fontSize: 12 }, onClick: (e) => {
|
|
57
61
|
e.stopPropagation();
|
|
58
|
-
removeSystems(binding, collectSystems(buildEffectTree(row.node)),
|
|
62
|
+
removeSystems(binding, collectSystems(buildEffectTree(row.node)), (s) => onSelectNode(s.emitter));
|
|
59
63
|
binding.apply(() => row.node.dispose(true, false));
|
|
60
|
-
}, children: "\u2715" }))] }), _jsx("div", { className: "qe-hover-bg", style: { height: ROW_HEIGHT, borderTop: `1px solid ${theme.border}` }, onClick:
|
|
64
|
+
}, children: "\u2715" }))] }), _jsx("div", { className: "qe-hover-bg", style: { height: ROW_HEIGHT, borderTop: `1px solid ${theme.border}` }, onClick: () => onSelectNode(row.node) }), renaming && (_jsx(PromptDialog, { title: "Rename", defaultValue: row.name, onSubmit: (name) => {
|
|
61
65
|
binding.apply(() => (row.node.name = name));
|
|
62
66
|
setRenaming(false);
|
|
63
67
|
}, onCancel: () => setRenaming(false) }))] }));
|
|
@@ -65,7 +69,7 @@ export function TimelineTrackRow(props) {
|
|
|
65
69
|
const { system } = row;
|
|
66
70
|
const barLeft = offsetPx + row.startOffset * pxPerSecond;
|
|
67
71
|
const barWidth = Math.max(2, row.duration * pxPerSecond);
|
|
68
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, opacity: hidden ? 0.5 : 1 }, onClick: () =>
|
|
72
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, opacity: hidden ? 0.5 : 1 }, onClick: () => onSelectNode(row.node), children: [!isRoot && (_jsx("button", { className: "qe-hover", title: groupSelected ? 'Remove from group selection' : 'Select for grouping', style: { ...checkboxButton, color: groupSelected ? theme.accent : theme.textDim }, onClick: (e) => {
|
|
69
73
|
e.stopPropagation();
|
|
70
74
|
onToggleGroupSelect();
|
|
71
75
|
}, children: groupSelected ? '☑' : '☐' })), _jsx("button", { className: "qe-hover", title: hidden ? 'Show in viewport' : 'Hide in viewport', style: { ...iconButton, color: hidden ? theme.textDim : theme.accent, fontSize: 12 }, onClick: (e) => {
|
|
@@ -76,7 +80,7 @@ export function TimelineTrackRow(props) {
|
|
|
76
80
|
setRenaming(true);
|
|
77
81
|
}, children: "\u270E" })), !isRoot && row.removable && (_jsx("button", { className: "qe-hover", title: "Remove system", style: { ...iconButton, color: '#e08c8c', fontSize: 12 }, onClick: (e) => {
|
|
78
82
|
e.stopPropagation();
|
|
79
|
-
removeSystems(binding, [system],
|
|
83
|
+
removeSystems(binding, [system], (s) => onSelectNode(s.emitter));
|
|
80
84
|
}, children: "\u2715" })), selected && (_jsx("button", { className: "qe-hover", title: "Add child system", style: { ...iconButton, color: theme.accent, fontSize: 13 }, onClick: (e) => {
|
|
81
85
|
e.stopPropagation();
|
|
82
86
|
let created;
|
|
@@ -86,9 +90,9 @@ export function TimelineTrackRow(props) {
|
|
|
86
90
|
});
|
|
87
91
|
if (created) {
|
|
88
92
|
binding.addSubSystem(created);
|
|
89
|
-
|
|
93
|
+
onSelectNode(created.emitter);
|
|
90
94
|
}
|
|
91
|
-
}, children: "+" }))] }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: ROW_HEIGHT }, onClick: () =>
|
|
95
|
+
}, children: "+" }))] }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: ROW_HEIGHT }, onClick: () => onSelectNode(row.node), children: _jsx("div", { className: "qe-hover", title: row.startOffsetVariable ? 'Start time varies per spawn' : undefined, style: {
|
|
92
96
|
position: 'absolute',
|
|
93
97
|
left: barLeft,
|
|
94
98
|
width: barWidth,
|
package/dist/react/theme.js
CHANGED
|
@@ -54,25 +54,48 @@ export const buttonStyle = {
|
|
|
54
54
|
* rows), since brightening transparent does nothing. Must not be combined with an inline
|
|
55
55
|
* `background` on the same element or the tint would need `!important` to show through.
|
|
56
56
|
*/
|
|
57
|
-
export const globalEditorStyles = `
|
|
58
|
-
.qe-hover, .qe-hover-bg {
|
|
59
|
-
transition: filter 0.12s ease, background 0.12s ease;
|
|
60
|
-
}
|
|
61
|
-
.qe-hover:hover:not(:disabled) {
|
|
62
|
-
filter: brightness(1.3);
|
|
63
|
-
}
|
|
64
|
-
.qe-hover:active:not(:disabled) {
|
|
65
|
-
filter: brightness(0.85);
|
|
66
|
-
}
|
|
67
|
-
.qe-hover:disabled {
|
|
68
|
-
opacity: 0.45;
|
|
69
|
-
cursor: default;
|
|
70
|
-
}
|
|
71
|
-
.qe-hover-bg:hover {
|
|
72
|
-
background: rgba(255, 255, 255, 0.07);
|
|
73
|
-
}
|
|
74
|
-
select.qe-hover:hover, input.qe-hover:hover, select.qe-hover:focus, input.qe-hover:focus {
|
|
75
|
-
border-color: ${theme.borderActive};
|
|
76
|
-
filter: none;
|
|
77
|
-
}
|
|
57
|
+
export const globalEditorStyles = `
|
|
58
|
+
.qe-hover, .qe-hover-bg {
|
|
59
|
+
transition: filter 0.12s ease, background 0.12s ease;
|
|
60
|
+
}
|
|
61
|
+
.qe-hover:hover:not(:disabled) {
|
|
62
|
+
filter: brightness(1.3);
|
|
63
|
+
}
|
|
64
|
+
.qe-hover:active:not(:disabled) {
|
|
65
|
+
filter: brightness(0.85);
|
|
66
|
+
}
|
|
67
|
+
.qe-hover:disabled {
|
|
68
|
+
opacity: 0.45;
|
|
69
|
+
cursor: default;
|
|
70
|
+
}
|
|
71
|
+
.qe-hover-bg:hover {
|
|
72
|
+
background: rgba(255, 255, 255, 0.07);
|
|
73
|
+
}
|
|
74
|
+
select.qe-hover:hover, input.qe-hover:hover, select.qe-hover:focus, input.qe-hover:focus {
|
|
75
|
+
border-color: ${theme.borderActive};
|
|
76
|
+
filter: none;
|
|
77
|
+
}
|
|
78
|
+
.qe-root, .qe-root * {
|
|
79
|
+
scrollbar-width: thin;
|
|
80
|
+
scrollbar-color: ${theme.border} transparent;
|
|
81
|
+
}
|
|
82
|
+
.qe-root ::-webkit-scrollbar {
|
|
83
|
+
width: 10px;
|
|
84
|
+
height: 10px;
|
|
85
|
+
}
|
|
86
|
+
.qe-root ::-webkit-scrollbar-track {
|
|
87
|
+
background: transparent;
|
|
88
|
+
}
|
|
89
|
+
.qe-root ::-webkit-scrollbar-thumb {
|
|
90
|
+
background-color: ${theme.border};
|
|
91
|
+
background-clip: padding-box;
|
|
92
|
+
border: 2px solid transparent;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
}
|
|
95
|
+
.qe-root ::-webkit-scrollbar-thumb:hover {
|
|
96
|
+
background-color: ${theme.borderActive};
|
|
97
|
+
}
|
|
98
|
+
.qe-root ::-webkit-scrollbar-corner {
|
|
99
|
+
background: transparent;
|
|
100
|
+
}
|
|
78
101
|
`;
|