babylon.quarks-editor 0.17.7
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 +13 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/core/binding.d.ts +43 -0
- package/dist/core/binding.js +82 -0
- package/dist/core/collision.d.ts +20 -0
- package/dist/core/collision.js +39 -0
- package/dist/core/colors.d.ts +31 -0
- package/dist/core/colors.js +60 -0
- package/dist/core/effectTree.d.ts +25 -0
- package/dist/core/effectTree.js +115 -0
- package/dist/core/groups.d.ts +12 -0
- package/dist/core/groups.js +27 -0
- package/dist/core/history.d.ts +25 -0
- package/dist/core/history.js +69 -0
- package/dist/core/loadEffect.d.ts +8 -0
- package/dist/core/loadEffect.js +87 -0
- package/dist/core/scrub.d.ts +11 -0
- package/dist/core/scrub.js +24 -0
- package/dist/core/shapes.d.ts +31 -0
- package/dist/core/shapes.js +98 -0
- package/dist/core/systems.d.ts +25 -0
- package/dist/core/systems.js +76 -0
- package/dist/core/textureExport.d.ts +9 -0
- package/dist/core/textureExport.js +58 -0
- package/dist/core/timeline.d.ts +35 -0
- package/dist/core/timeline.js +69 -0
- package/dist/core/values.d.ts +31 -0
- package/dist/core/values.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +9 -0
- package/dist/react/CurveEditor.d.ts +12 -0
- package/dist/react/CurveEditor.js +106 -0
- package/dist/react/EffectEditor.d.ts +22 -0
- package/dist/react/EffectEditor.js +30 -0
- package/dist/react/EffectEditorHost.d.ts +42 -0
- package/dist/react/EffectEditorHost.js +247 -0
- package/dist/react/GradientEditor.d.ts +7 -0
- package/dist/react/GradientEditor.js +72 -0
- package/dist/react/ModuleSection.d.ts +9 -0
- package/dist/react/ModuleSection.js +24 -0
- package/dist/react/PromptDialog.d.ts +14 -0
- package/dist/react/PromptDialog.js +45 -0
- package/dist/react/SubEmittersModule.d.ts +10 -0
- package/dist/react/SubEmittersModule.js +39 -0
- package/dist/react/TimelinePanel.d.ts +32 -0
- package/dist/react/TimelinePanel.js +225 -0
- package/dist/react/TimelineTrackRow.d.ts +23 -0
- package/dist/react/TimelineTrackRow.js +107 -0
- package/dist/react/ValueField.d.ts +13 -0
- package/dist/react/ValueField.js +19 -0
- package/dist/react/behaviorModules.d.ts +23 -0
- package/dist/react/behaviorModules.js +139 -0
- package/dist/react/fields.d.ts +24 -0
- package/dist/react/fields.js +30 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.js +14 -0
- package/dist/react/modules.d.ts +35 -0
- package/dist/react/modules.js +439 -0
- package/dist/react/theme.d.ts +26 -0
- package/dist/react/theme.js +78 -0
- package/package.json +63 -0
- package/src/core/binding.ts +105 -0
- package/src/core/collision.ts +40 -0
- package/src/core/colors.ts +84 -0
- package/src/core/effectTree.ts +143 -0
- package/src/core/groups.ts +29 -0
- package/src/core/history.ts +77 -0
- package/src/core/loadEffect.ts +102 -0
- package/src/core/scrub.ts +28 -0
- package/src/core/shapes.ts +133 -0
- package/src/core/systems.ts +98 -0
- package/src/core/textureExport.ts +63 -0
- package/src/core/timeline.ts +94 -0
- package/src/core/values.ts +113 -0
- package/src/index.ts +32 -0
- package/src/react/CurveEditor.tsx +168 -0
- package/src/react/EffectEditor.tsx +111 -0
- package/src/react/EffectEditorHost.tsx +377 -0
- package/src/react/GradientEditor.tsx +127 -0
- package/src/react/ModuleSection.tsx +54 -0
- package/src/react/PromptDialog.tsx +85 -0
- package/src/react/SubEmittersModule.tsx +80 -0
- package/src/react/TimelinePanel.tsx +354 -0
- package/src/react/TimelineTrackRow.tsx +267 -0
- package/src/react/ValueField.tsx +60 -0
- package/src/react/behaviorModules.tsx +658 -0
- package/src/react/fields.tsx +76 -0
- package/src/react/index.ts +43 -0
- package/src/react/modules.tsx +1034 -0
- package/src/react/theme.ts +85 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { hexToRgb, rgbToHex, stopsToCssGradient } from '../core/colors';
|
|
4
|
+
import { NumberField, Row } from './fields';
|
|
5
|
+
import { buttonStyle, theme } from './theme';
|
|
6
|
+
const BAR_WIDTH = 218;
|
|
7
|
+
/** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
|
|
8
|
+
export function GradientEditor(props) {
|
|
9
|
+
const [selected, setSelected] = useState(0);
|
|
10
|
+
const barRef = useRef(null);
|
|
11
|
+
const dragging = useRef(false);
|
|
12
|
+
const stops = props.stops;
|
|
13
|
+
const current = stops[Math.min(selected, stops.length - 1)];
|
|
14
|
+
const posFromEvent = (clientX) => {
|
|
15
|
+
const rect = barRef.current.getBoundingClientRect();
|
|
16
|
+
return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
|
|
17
|
+
};
|
|
18
|
+
const updateStop = (index, patch) => {
|
|
19
|
+
const next = stops.map((s, i) => (i === index ? { ...s, ...patch } : s));
|
|
20
|
+
props.onChange(next);
|
|
21
|
+
};
|
|
22
|
+
return (_jsxs("div", { children: [_jsx("div", { ref: barRef, className: "qe-hover", style: {
|
|
23
|
+
position: 'relative',
|
|
24
|
+
width: BAR_WIDTH,
|
|
25
|
+
height: 20,
|
|
26
|
+
borderRadius: 6,
|
|
27
|
+
border: `1px solid ${theme.border}`,
|
|
28
|
+
background: `${stopsToCssGradient(stops)}, repeating-conic-gradient(#333 0% 25%, #555 0% 50%) 0 0 / 10px 10px`,
|
|
29
|
+
cursor: 'copy',
|
|
30
|
+
}, onPointerDown: (e) => {
|
|
31
|
+
if (e.target !== barRef.current) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const pos = posFromEvent(e.clientX);
|
|
35
|
+
const base = current ?? { r: 1, g: 1, b: 1, a: 1, pos: 0 };
|
|
36
|
+
const next = [...stops, { ...base, pos }];
|
|
37
|
+
props.onChange(next);
|
|
38
|
+
setSelected(next.length - 1);
|
|
39
|
+
}, onPointerMove: (e) => {
|
|
40
|
+
if (dragging.current) {
|
|
41
|
+
updateStop(selected, { pos: posFromEvent(e.clientX) });
|
|
42
|
+
}
|
|
43
|
+
}, onPointerUp: () => {
|
|
44
|
+
dragging.current = false;
|
|
45
|
+
}, children: stops.map((stop, i) => (_jsx("div", { className: "qe-hover", style: {
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
left: `calc(${stop.pos * 100}% - 6px)`,
|
|
48
|
+
top: -4,
|
|
49
|
+
width: 12,
|
|
50
|
+
height: 26,
|
|
51
|
+
borderRadius: 4,
|
|
52
|
+
background: rgbToHex(stop.r, stop.g, stop.b),
|
|
53
|
+
border: `2px solid ${i === selected ? theme.accent : '#0a1020'}`,
|
|
54
|
+
cursor: 'ew-resize',
|
|
55
|
+
boxSizing: 'border-box',
|
|
56
|
+
}, onPointerDown: (e) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
setSelected(i);
|
|
59
|
+
dragging.current = true;
|
|
60
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
61
|
+
}, onPointerMove: (e) => {
|
|
62
|
+
if (dragging.current && i === selected) {
|
|
63
|
+
updateStop(i, { pos: posFromEvent(e.clientX) });
|
|
64
|
+
}
|
|
65
|
+
}, onPointerUp: (e) => {
|
|
66
|
+
dragging.current = false;
|
|
67
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
68
|
+
} }, i))) }), current && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Stop color", children: _jsx("input", { type: "color", className: "qe-hover", value: rgbToHex(current.r, current.g, current.b), onChange: (e) => updateStop(selected, hexToRgb(e.target.value)), style: { width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' } }) }), _jsx(Row, { label: "Stop alpha", children: _jsx(NumberField, { value: current.a, min: 0, step: 0.05, onChange: (a) => updateStop(selected, { a: Math.min(1, a) }) }) }), _jsx(Row, { label: "Stop position", children: _jsx(NumberField, { value: current.pos, min: 0, step: 0.05, onChange: (pos) => updateStop(selected, { pos: Math.min(1, pos) }) }) }), stops.length > 2 && (_jsx("button", { className: "qe-hover", style: { ...buttonStyle, marginTop: 6 }, onClick: () => {
|
|
69
|
+
props.onChange(stops.filter((_, i) => i !== selected));
|
|
70
|
+
setSelected(0);
|
|
71
|
+
}, children: "Remove stop" }))] }))] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/** Unity-style module foldout with an optional enable checkbox. */
|
|
3
|
+
export declare function ModuleSection(props: {
|
|
4
|
+
title: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
onToggle?: (enabled: boolean) => void;
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { theme } from './theme';
|
|
4
|
+
/** Unity-style module foldout with an optional enable checkbox. */
|
|
5
|
+
export function ModuleSection(props) {
|
|
6
|
+
const [open, setOpen] = useState(props.defaultOpen ?? true);
|
|
7
|
+
const toggleable = props.onToggle !== undefined;
|
|
8
|
+
const dimmed = toggleable && props.enabled === false;
|
|
9
|
+
return (_jsxs("section", { style: {
|
|
10
|
+
border: `1px solid ${theme.border}`,
|
|
11
|
+
borderRadius: 10,
|
|
12
|
+
background: theme.sectionBg,
|
|
13
|
+
marginTop: 8,
|
|
14
|
+
overflow: 'hidden',
|
|
15
|
+
}, children: [_jsxs("header", { className: "qe-hover", style: {
|
|
16
|
+
display: 'flex',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
gap: 8,
|
|
19
|
+
padding: '7px 10px',
|
|
20
|
+
cursor: 'pointer',
|
|
21
|
+
userSelect: 'none',
|
|
22
|
+
background: 'rgba(20, 30, 60, 0.5)',
|
|
23
|
+
}, onClick: () => setOpen(!open), children: [_jsx("span", { style: { color: theme.accent, fontSize: 10, width: 10 }, children: open ? '▼' : '►' }), toggleable && (_jsx("input", { type: "checkbox", className: "qe-hover", checked: props.enabled, onClick: (e) => e.stopPropagation(), onChange: (e) => props.onToggle?.(e.target.checked), style: { width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer' } })), _jsx("span", { style: { color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600 }, children: props.title })] }), open && !dimmed && _jsx("div", { style: { padding: '4px 10px 10px' }, children: props.children })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface PromptDialogProps {
|
|
3
|
+
title: string;
|
|
4
|
+
defaultValue?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
confirmLabel?: string;
|
|
7
|
+
/** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
|
|
8
|
+
allowEmpty?: boolean;
|
|
9
|
+
onSubmit: (value: string) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
|
|
13
|
+
* Enter, cancels on Escape or backdrop click. */
|
|
14
|
+
export declare function PromptDialog(props: PromptDialogProps): React.ReactElement;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { buttonStyle, inputStyle, theme } from './theme';
|
|
4
|
+
/** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
|
|
5
|
+
* Enter, cancels on Escape or backdrop click. */
|
|
6
|
+
export function PromptDialog(props) {
|
|
7
|
+
const { title, defaultValue = '', placeholder, confirmLabel = 'OK', allowEmpty = false, onSubmit, onCancel } = props;
|
|
8
|
+
const [value, setValue] = useState(defaultValue);
|
|
9
|
+
const inputRef = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
inputRef.current?.focus();
|
|
12
|
+
inputRef.current?.select();
|
|
13
|
+
}, []);
|
|
14
|
+
const submit = () => {
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (trimmed || allowEmpty) {
|
|
17
|
+
onSubmit(trimmed);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
onCancel();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return (_jsx("div", { style: {
|
|
24
|
+
position: 'fixed',
|
|
25
|
+
inset: 0,
|
|
26
|
+
background: 'rgba(3, 5, 12, 0.6)',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
zIndex: 1000,
|
|
31
|
+
fontFamily: theme.font,
|
|
32
|
+
}, onClick: onCancel, children: _jsxs("div", { style: {
|
|
33
|
+
background: theme.panelBg,
|
|
34
|
+
border: `1px solid ${theme.border}`,
|
|
35
|
+
borderRadius: 10,
|
|
36
|
+
padding: 16,
|
|
37
|
+
minWidth: 300,
|
|
38
|
+
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
|
|
39
|
+
}, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontSize: 13, color: theme.text, marginBottom: 10 }, children: title }), _jsx("input", { ref: inputRef, className: "qe-hover", style: { ...inputStyle, marginBottom: 14 }, value: value, placeholder: placeholder, onChange: (e) => setValue(e.target.value), onKeyDown: (e) => {
|
|
40
|
+
if (e.key === 'Enter')
|
|
41
|
+
submit();
|
|
42
|
+
if (e.key === 'Escape')
|
|
43
|
+
onCancel();
|
|
44
|
+
} }), _jsxs("div", { style: { display: 'flex', justifyContent: 'flex-end', gap: 8 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, onClick: onCancel, children: "Cancel" }), _jsx("button", { className: "qe-hover", style: { ...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent }, onClick: submit, children: confirmLabel })] })] }) }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EffectBinding } from '../core/binding';
|
|
3
|
+
/**
|
|
4
|
+
* Sub Emitters module for the currently selected system. The spawned target lives as a
|
|
5
|
+
* child system in the effect hierarchy — edit it by selecting it there (Unity pattern).
|
|
6
|
+
*/
|
|
7
|
+
export declare function SubEmittersModule(props: {
|
|
8
|
+
binding: EffectBinding;
|
|
9
|
+
rootBinding: EffectBinding;
|
|
10
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { EmitSubParticleSystem, SubParticleEmitMode } from 'babylon.quarks';
|
|
3
|
+
import { findBehavior } from '../core/colors';
|
|
4
|
+
import { createChildSystem } from '../core/systems';
|
|
5
|
+
import { ModuleSection } from './ModuleSection';
|
|
6
|
+
import { CheckboxField, NumberField, Row, SelectField } from './fields';
|
|
7
|
+
import { theme } from './theme';
|
|
8
|
+
/**
|
|
9
|
+
* Sub Emitters module for the currently selected system. The spawned target lives as a
|
|
10
|
+
* child system in the effect hierarchy — edit it by selecting it there (Unity pattern).
|
|
11
|
+
*/
|
|
12
|
+
export function SubEmittersModule(props) {
|
|
13
|
+
const { binding, rootBinding } = props;
|
|
14
|
+
const system = binding.system;
|
|
15
|
+
const behavior = findBehavior(system.behaviors, 'EmitSubParticleSystem');
|
|
16
|
+
const target = behavior?.subParticleSystem?.system;
|
|
17
|
+
return (_jsx(ModuleSection, { title: "Sub Emitters", enabled: !!behavior, defaultOpen: false, onToggle: (enabled) => binding.apply((s) => {
|
|
18
|
+
for (let i = s.behaviors.length - 1; i >= 0; i--) {
|
|
19
|
+
if (s.behaviors[i].type === 'EmitSubParticleSystem') {
|
|
20
|
+
s.behaviors.splice(i, 1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (enabled) {
|
|
24
|
+
const sub = createChildSystem(s, { oneShot: true });
|
|
25
|
+
s._renderer?.addSystem(sub);
|
|
26
|
+
rootBinding.addSubSystem(sub);
|
|
27
|
+
s.addBehavior(new EmitSubParticleSystem(s, false, sub.emitter, SubParticleEmitMode.Death, 1));
|
|
28
|
+
}
|
|
29
|
+
else if (target) {
|
|
30
|
+
s._renderer?.deleteSystem(target);
|
|
31
|
+
target.dispose();
|
|
32
|
+
rootBinding.removeSubSystem(target);
|
|
33
|
+
}
|
|
34
|
+
}), children: behavior && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Trigger", children: _jsx(SelectField, { value: behavior.mode, options: [
|
|
35
|
+
{ value: SubParticleEmitMode.Death, label: 'On death' },
|
|
36
|
+
{ value: SubParticleEmitMode.Birth, label: 'On birth' },
|
|
37
|
+
{ value: SubParticleEmitMode.Frame, label: 'Every frame' },
|
|
38
|
+
], onChange: (mode) => binding.apply(() => (behavior.mode = mode)) }) }), _jsx(Row, { label: "Probability", children: _jsx(NumberField, { value: behavior.emitProbability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.emitProbability = Math.min(1, v))) }) }), _jsx(Row, { label: "Use velocity", children: _jsx(CheckboxField, { value: behavior.useVelocityAsBasis, onChange: (v) => binding.apply(() => (behavior.useVelocityAsBasis = v)) }) }), _jsxs("p", { style: { margin: '8px 0 2px', color: theme.textDim, fontSize: 11.5 }, children: ["Spawns \u201C", target?.emitter.name ?? 'sub-emitter', "\u201D \u2014 select it in the hierarchy above to edit it."] })] })) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ParticleSystem, BatchedRenderer } from 'babylon.quarks';
|
|
3
|
+
import type { Scene } from '@babylonjs/core/scene';
|
|
4
|
+
import type { EffectBinding } from '../core/binding';
|
|
5
|
+
export interface PlaybackState {
|
|
6
|
+
paused: boolean;
|
|
7
|
+
speed: number;
|
|
8
|
+
stepQueued: boolean;
|
|
9
|
+
elapsed: number;
|
|
10
|
+
scrubbing: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface TimelinePanelProps {
|
|
13
|
+
binding: EffectBinding;
|
|
14
|
+
selectedSystem: ParticleSystem;
|
|
15
|
+
onSelect: (system: ParticleSystem) => void;
|
|
16
|
+
scene: Scene;
|
|
17
|
+
renderer: BatchedRenderer;
|
|
18
|
+
playbackRef: React.MutableRefObject<PlaybackState>;
|
|
19
|
+
playheadRef: React.RefObject<HTMLDivElement>;
|
|
20
|
+
counterRef: React.RefObject<HTMLSpanElement>;
|
|
21
|
+
paused: boolean;
|
|
22
|
+
setPaused: (paused: boolean) => void;
|
|
23
|
+
speed: number;
|
|
24
|
+
setSpeed: (speed: number) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Small gap between the label column and the first pixel of the time axis. */
|
|
27
|
+
export declare const TIMELINE_INSET = 10;
|
|
28
|
+
/**
|
|
29
|
+
* Bottom-docked, Premiere/Resolve-style timeline: transport controls, a time ruler with a
|
|
30
|
+
* draggable playhead (approximate scrub — see core/scrub.ts), and one track row per system.
|
|
31
|
+
*/
|
|
32
|
+
export declare function TimelinePanel(props: TimelinePanelProps): React.ReactElement;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { groupNodes } from '../core/groups';
|
|
4
|
+
import { scrubTo } from '../core/scrub';
|
|
5
|
+
import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
|
|
6
|
+
import { TimelineTrackRow } from './TimelineTrackRow';
|
|
7
|
+
import { buttonStyle, theme } from './theme';
|
|
8
|
+
const LABEL_WIDTH = 200;
|
|
9
|
+
const RULER_HEIGHT = 22;
|
|
10
|
+
const PANEL_HEIGHT = 220;
|
|
11
|
+
/** Small gap between the label column and the first pixel of the time axis. */
|
|
12
|
+
export const TIMELINE_INSET = 10;
|
|
13
|
+
function pickTickInterval(pxPerSecond) {
|
|
14
|
+
if (pxPerSecond >= 60)
|
|
15
|
+
return 1;
|
|
16
|
+
if (pxPerSecond >= 24)
|
|
17
|
+
return 2;
|
|
18
|
+
if (pxPerSecond >= 10)
|
|
19
|
+
return 5;
|
|
20
|
+
return 10;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Bottom-docked, Premiere/Resolve-style timeline: transport controls, a time ruler with a
|
|
24
|
+
* draggable playhead (approximate scrub — see core/scrub.ts), and one track row per system.
|
|
25
|
+
*/
|
|
26
|
+
export function TimelinePanel(props) {
|
|
27
|
+
const { binding, selectedSystem, onSelect, scene, renderer, playbackRef, playheadRef, counterRef, paused, setPaused, speed, setSpeed } = props;
|
|
28
|
+
const [bodyWidthPx, setBodyWidthPx] = useState(600);
|
|
29
|
+
const [collapsedGroups, setCollapsedGroups] = useState(() => new Set());
|
|
30
|
+
const [hiddenSystems, setHiddenSystems] = useState(() => new Set());
|
|
31
|
+
const [groupSelection, setGroupSelection] = useState(() => new Set());
|
|
32
|
+
const bodyRef = useRef(null);
|
|
33
|
+
const dragRef = useRef({ raf: null, pendingTime: null });
|
|
34
|
+
// A new effect (import/undo/redo/initial mount) replaces every system, so stale
|
|
35
|
+
// visibility toggles and grouping selection from the previous effect no longer refer to
|
|
36
|
+
// anything real.
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
setHiddenSystems(new Set());
|
|
39
|
+
setGroupSelection(new Set());
|
|
40
|
+
}, [binding]);
|
|
41
|
+
const toggleGroupSelect = (node) => {
|
|
42
|
+
setGroupSelection((prev) => {
|
|
43
|
+
const next = new Set(prev);
|
|
44
|
+
if (next.has(node)) {
|
|
45
|
+
next.delete(node);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
next.add(node);
|
|
49
|
+
}
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const groupSelectedNodes = () => {
|
|
54
|
+
if (groupSelection.size < 2) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const nodes = [...groupSelection];
|
|
58
|
+
binding.apply(() => {
|
|
59
|
+
groupNodes(nodes, `Group ${rows.filter((r) => r.kind === 'group').length + 1}`);
|
|
60
|
+
});
|
|
61
|
+
setGroupSelection(new Set());
|
|
62
|
+
};
|
|
63
|
+
const toggleVisible = (system) => {
|
|
64
|
+
setHiddenSystems((prev) => {
|
|
65
|
+
const next = new Set(prev);
|
|
66
|
+
if (next.has(system)) {
|
|
67
|
+
next.delete(system);
|
|
68
|
+
system.emitter.visible = true;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
next.add(system);
|
|
72
|
+
system.emitter.visible = false;
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
React.useLayoutEffect(() => {
|
|
78
|
+
const el = bodyRef.current;
|
|
79
|
+
if (!el)
|
|
80
|
+
return;
|
|
81
|
+
const observer = new ResizeObserver((entries) => {
|
|
82
|
+
const width = entries[0]?.contentRect.width;
|
|
83
|
+
if (width)
|
|
84
|
+
setBodyWidthPx(Math.max(1, width - LABEL_WIDTH));
|
|
85
|
+
});
|
|
86
|
+
observer.observe(el);
|
|
87
|
+
return () => observer.disconnect();
|
|
88
|
+
}, []);
|
|
89
|
+
const rows = useMemo(() => computeTimelineRows(binding), [binding, binding.getRevision()]);
|
|
90
|
+
const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
|
|
91
|
+
const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET) / timelineSpan;
|
|
92
|
+
// 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). Looping
|
|
94
|
+
// effects run forever, so elapsed grows unbounded — wrap it to the visible span instead
|
|
95
|
+
// of letting the playhead fly off past the right edge.
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
let raf;
|
|
98
|
+
const tick = () => {
|
|
99
|
+
if (playheadRef.current) {
|
|
100
|
+
const wrapped = timelineSpan > 0 ? playbackRef.current.elapsed % timelineSpan : 0;
|
|
101
|
+
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + wrapped * pxPerSecond}px`;
|
|
102
|
+
}
|
|
103
|
+
raf = requestAnimationFrame(tick);
|
|
104
|
+
};
|
|
105
|
+
raf = requestAnimationFrame(tick);
|
|
106
|
+
return () => cancelAnimationFrame(raf);
|
|
107
|
+
}, [pxPerSecond, timelineSpan, playbackRef, playheadRef]);
|
|
108
|
+
const visibleRows = useMemo(() => {
|
|
109
|
+
let hideUntilDepth = null;
|
|
110
|
+
const visible = rows.filter((row) => {
|
|
111
|
+
if (hideUntilDepth !== null) {
|
|
112
|
+
if (row.depth > hideUntilDepth)
|
|
113
|
+
return false;
|
|
114
|
+
hideUntilDepth = null;
|
|
115
|
+
}
|
|
116
|
+
if (row.kind === 'group' && collapsedGroups.has(row.node)) {
|
|
117
|
+
hideUntilDepth = row.depth;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
return visible;
|
|
122
|
+
}, [rows, collapsedGroups]);
|
|
123
|
+
const toggleCollapse = (node) => {
|
|
124
|
+
setCollapsedGroups((prev) => {
|
|
125
|
+
const next = new Set(prev);
|
|
126
|
+
if (next.has(node)) {
|
|
127
|
+
next.delete(node);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
next.add(node);
|
|
131
|
+
}
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const pixelToTime = (clientX) => {
|
|
136
|
+
const rect = bodyRef.current.getBoundingClientRect();
|
|
137
|
+
const x = clientX - rect.left - LABEL_WIDTH - TIMELINE_INSET;
|
|
138
|
+
return Math.min(timelineSpan, Math.max(0, x / pxPerSecond));
|
|
139
|
+
};
|
|
140
|
+
const applyScrub = (time) => {
|
|
141
|
+
scrubTo(binding, renderer, time);
|
|
142
|
+
playbackRef.current.elapsed = time;
|
|
143
|
+
if (playheadRef.current) {
|
|
144
|
+
playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + time * pxPerSecond}px`;
|
|
145
|
+
}
|
|
146
|
+
scene.render();
|
|
147
|
+
};
|
|
148
|
+
const queueScrub = (clientX) => {
|
|
149
|
+
dragRef.current.pendingTime = pixelToTime(clientX);
|
|
150
|
+
if (dragRef.current.raf == null) {
|
|
151
|
+
dragRef.current.raf = requestAnimationFrame(() => {
|
|
152
|
+
dragRef.current.raf = null;
|
|
153
|
+
const t = dragRef.current.pendingTime;
|
|
154
|
+
if (t != null)
|
|
155
|
+
applyScrub(t);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const restart = () => {
|
|
160
|
+
applyScrub(0);
|
|
161
|
+
};
|
|
162
|
+
const ticks = [];
|
|
163
|
+
for (let t = 0; t <= timelineSpan; t += pickTickInterval(pxPerSecond)) {
|
|
164
|
+
ticks.push(t);
|
|
165
|
+
}
|
|
166
|
+
const overlayButton = { ...buttonStyle, padding: '5px 9px', minWidth: 32 };
|
|
167
|
+
return (_jsxs("div", { style: {
|
|
168
|
+
flexShrink: 0,
|
|
169
|
+
height: PANEL_HEIGHT,
|
|
170
|
+
borderTop: `1px solid ${theme.border}`,
|
|
171
|
+
background: theme.panelBg,
|
|
172
|
+
display: 'flex',
|
|
173
|
+
flexDirection: 'column',
|
|
174
|
+
fontFamily: theme.font,
|
|
175
|
+
color: theme.text,
|
|
176
|
+
}, 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: () => {
|
|
177
|
+
playbackRef.current.paused = !paused;
|
|
178
|
+
setPaused(!paused);
|
|
179
|
+
}, 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) => {
|
|
180
|
+
const next = Number(e.target.value);
|
|
181
|
+
playbackRef.current.speed = next;
|
|
182
|
+
setSpeed(next);
|
|
183
|
+
}, children: [0.1, 0.25, 0.5, 1, 2, 4].map((s) => (_jsxs("option", { value: s, children: ["\u00D7", s] }, s))) }), _jsx("span", { ref: counterRef, style: { fontSize: 12, color: theme.textDim, fontVariantNumeric: 'tabular-nums', paddingLeft: 4, minWidth: 120 } }), groupSelection.size >= 2 && (_jsxs("button", { className: "qe-hover", style: overlayButton, title: "Group the checked rows", onClick: groupSelectedNodes, children: ["Group (", groupSelection.size, ")"] }))] }), _jsxs("div", { ref: bodyRef, style: { position: 'relative', flex: 1, minHeight: 0 }, children: [_jsxs("div", { style: {
|
|
184
|
+
position: 'absolute',
|
|
185
|
+
top: 0,
|
|
186
|
+
left: 0,
|
|
187
|
+
right: 0,
|
|
188
|
+
height: RULER_HEIGHT,
|
|
189
|
+
display: 'grid',
|
|
190
|
+
gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
|
|
191
|
+
background: theme.panelBg,
|
|
192
|
+
zIndex: 2,
|
|
193
|
+
}, children: [_jsx("div", { style: { height: RULER_HEIGHT } }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: RULER_HEIGHT, cursor: 'ew-resize', overflow: 'hidden' }, onPointerDown: (e) => {
|
|
194
|
+
playbackRef.current.scrubbing = true;
|
|
195
|
+
e.target.setPointerCapture(e.pointerId);
|
|
196
|
+
queueScrub(e.clientX);
|
|
197
|
+
}, onPointerMove: (e) => {
|
|
198
|
+
if (playbackRef.current.scrubbing)
|
|
199
|
+
queueScrub(e.clientX);
|
|
200
|
+
}, onPointerUp: (e) => {
|
|
201
|
+
playbackRef.current.scrubbing = false;
|
|
202
|
+
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))) })] }), _jsx("div", { style: {
|
|
204
|
+
position: 'absolute',
|
|
205
|
+
top: RULER_HEIGHT,
|
|
206
|
+
left: 0,
|
|
207
|
+
right: 0,
|
|
208
|
+
bottom: 0,
|
|
209
|
+
overflowY: 'auto',
|
|
210
|
+
overflowX: 'hidden',
|
|
211
|
+
}, children: _jsx("div", { style: {
|
|
212
|
+
display: 'grid',
|
|
213
|
+
gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
|
|
214
|
+
gridAutoRows: 'min-content',
|
|
215
|
+
}, children: visibleRows.map((row) => (_jsx(TimelineTrackRow, { row: row, binding: binding, selected: row.kind === 'track' && row.system === selectedSystem, onSelect: onSelect, 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
|
+
position: 'absolute',
|
|
217
|
+
top: 0,
|
|
218
|
+
bottom: 0,
|
|
219
|
+
left: LABEL_WIDTH + TIMELINE_INSET,
|
|
220
|
+
width: 1,
|
|
221
|
+
background: theme.accent,
|
|
222
|
+
pointerEvents: 'none',
|
|
223
|
+
zIndex: 3,
|
|
224
|
+
} })] })] }));
|
|
225
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ParticleSystem } from 'babylon.quarks';
|
|
3
|
+
import type { EffectBinding } from '../core/binding';
|
|
4
|
+
import type { TimelineRow } from '../core/timeline';
|
|
5
|
+
export interface TimelineTrackRowProps {
|
|
6
|
+
row: TimelineRow;
|
|
7
|
+
binding: EffectBinding;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
onSelect: (system: ParticleSystem) => void;
|
|
10
|
+
pxPerSecond: number;
|
|
11
|
+
/** Matches the ruler's left gutter so bars line up under the correct tick. */
|
|
12
|
+
offsetPx: number;
|
|
13
|
+
collapsed: boolean;
|
|
14
|
+
onToggleCollapse?: () => void;
|
|
15
|
+
/** Preview-only visibility (toggles `system.emitter.visible`); lets you isolate one track. */
|
|
16
|
+
hidden?: boolean;
|
|
17
|
+
onToggleVisible?: () => void;
|
|
18
|
+
/** Membership in the "group selected rows together" set (unrelated to inspector selection). */
|
|
19
|
+
groupSelected: boolean;
|
|
20
|
+
onToggleGroupSelect: () => void;
|
|
21
|
+
}
|
|
22
|
+
/** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
|
|
23
|
+
export declare function TimelineTrackRow(props: TimelineTrackRowProps): React.ReactElement;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { buildEffectTree, collectSystems } from '../core/effectTree';
|
|
4
|
+
import { createChildSystem, removeSystems } from '../core/systems';
|
|
5
|
+
import { ungroupNode } from '../core/groups';
|
|
6
|
+
import { PromptDialog } from './PromptDialog';
|
|
7
|
+
import { theme } from './theme';
|
|
8
|
+
const iconButton = { background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, padding: 0 };
|
|
9
|
+
const ROW_HEIGHT = 28;
|
|
10
|
+
const checkboxButton = { ...iconButton, fontSize: 12, width: 12 };
|
|
11
|
+
/** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
|
|
12
|
+
export function TimelineTrackRow(props) {
|
|
13
|
+
const { row, binding, selected, onSelect, pxPerSecond, offsetPx, collapsed, onToggleCollapse, hidden, onToggleVisible, groupSelected, onToggleGroupSelect } = props;
|
|
14
|
+
const [renaming, setRenaming] = useState(false);
|
|
15
|
+
const isRoot = row.node === binding.root;
|
|
16
|
+
const labelStyle = {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
gap: 6,
|
|
20
|
+
height: ROW_HEIGHT,
|
|
21
|
+
padding: '0 8px',
|
|
22
|
+
paddingLeft: 8 + row.depth * 14,
|
|
23
|
+
borderRadius: 6,
|
|
24
|
+
cursor: row.kind === 'track' ? 'pointer' : onToggleCollapse ? 'pointer' : 'default',
|
|
25
|
+
fontSize: 12.5,
|
|
26
|
+
color: row.kind === 'track' && selected ? theme.text : theme.textDim,
|
|
27
|
+
whiteSpace: 'nowrap',
|
|
28
|
+
// Left unset (rather than 'transparent') when not selected so the .qe-row-bg hover
|
|
29
|
+
// tint isn't shadowed by an inline background of the same property.
|
|
30
|
+
...(row.kind === 'track' && selected ? { background: 'rgba(60, 105, 209, 0.3)' } : {}),
|
|
31
|
+
};
|
|
32
|
+
if (row.kind === 'group') {
|
|
33
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, borderTop: `1px solid ${theme.border}` }, onClick: onToggleCollapse, 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
|
+
e.stopPropagation();
|
|
35
|
+
onToggleGroupSelect();
|
|
36
|
+
}, children: groupSelected ? '☑' : '☐' })), _jsx("span", { style: { color: theme.textDim, fontSize: 10, width: 10 }, 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
|
+
e.stopPropagation();
|
|
38
|
+
let created;
|
|
39
|
+
binding.apply(() => {
|
|
40
|
+
created = createChildSystem(binding.system, {
|
|
41
|
+
name: `system ${binding.subSystems.length + 1}`,
|
|
42
|
+
parentNode: row.node,
|
|
43
|
+
});
|
|
44
|
+
binding.system._renderer?.addSystem(created);
|
|
45
|
+
});
|
|
46
|
+
if (created) {
|
|
47
|
+
binding.addSubSystem(created);
|
|
48
|
+
onSelect(created);
|
|
49
|
+
}
|
|
50
|
+
}, children: "+" }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
setRenaming(true);
|
|
53
|
+
}, children: "\u270E" })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Ungroup (moves its contents up a level)", style: { ...iconButton, color: theme.textDim, fontSize: 12 }, onClick: (e) => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
binding.apply(() => ungroupNode(row.node));
|
|
56
|
+
}, children: "\u26F6" })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Remove group", style: { ...iconButton, color: '#e08c8c', fontSize: 12 }, onClick: (e) => {
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
removeSystems(binding, collectSystems(buildEffectTree(row.node)), onSelect);
|
|
59
|
+
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: onToggleCollapse }), renaming && (_jsx(PromptDialog, { title: "Rename", defaultValue: row.name, onSubmit: (name) => {
|
|
61
|
+
binding.apply(() => (row.node.name = name));
|
|
62
|
+
setRenaming(false);
|
|
63
|
+
}, onCancel: () => setRenaming(false) }))] }));
|
|
64
|
+
}
|
|
65
|
+
const { system } = row;
|
|
66
|
+
const barLeft = offsetPx + row.startOffset * pxPerSecond;
|
|
67
|
+
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: () => onSelect(system), 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
|
+
e.stopPropagation();
|
|
70
|
+
onToggleGroupSelect();
|
|
71
|
+
}, 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) => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
onToggleVisible?.();
|
|
74
|
+
}, children: hidden ? '◯' : '◉' }), _jsx("span", { style: { color: theme.accent, fontSize: 10, width: 10 }, children: "\u25C6" }), _jsxs("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: [row.name, system.onlyUsedByOther ? ' (sub)' : ''] }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
setRenaming(true);
|
|
77
|
+
}, children: "\u270E" })), !isRoot && row.removable && (_jsx("button", { className: "qe-hover", title: "Remove system", style: { ...iconButton, color: '#e08c8c', fontSize: 12 }, onClick: (e) => {
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
removeSystems(binding, [system], onSelect);
|
|
80
|
+
}, children: "\u2715" })), selected && (_jsx("button", { className: "qe-hover", title: "Add child system", style: { ...iconButton, color: theme.accent, fontSize: 13 }, onClick: (e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
let created;
|
|
83
|
+
binding.apply(() => {
|
|
84
|
+
created = createChildSystem(system, { name: `system ${binding.subSystems.length + 1}` });
|
|
85
|
+
binding.system._renderer?.addSystem(created);
|
|
86
|
+
});
|
|
87
|
+
if (created) {
|
|
88
|
+
binding.addSubSystem(created);
|
|
89
|
+
onSelect(created);
|
|
90
|
+
}
|
|
91
|
+
}, children: "+" }))] }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: ROW_HEIGHT }, onClick: () => onSelect(system), children: _jsx("div", { className: "qe-hover", title: row.startOffsetVariable ? 'Start time varies per spawn' : undefined, style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
left: barLeft,
|
|
94
|
+
width: barWidth,
|
|
95
|
+
top: 5,
|
|
96
|
+
height: ROW_HEIGHT - 10,
|
|
97
|
+
borderRadius: 4,
|
|
98
|
+
cursor: 'pointer',
|
|
99
|
+
background: system.onlyUsedByOther ? 'repeating-linear-gradient(135deg, rgba(158,185,255,0.25), rgba(158,185,255,0.25) 4px, rgba(158,185,255,0.4) 4px, rgba(158,185,255,0.4) 8px)' : selected ? theme.accent : '#3c5aa0',
|
|
100
|
+
border: system.onlyUsedByOther ? `1px dashed ${theme.accent}` : `1px solid ${theme.border}`,
|
|
101
|
+
opacity: hidden ? 0.3 : system.onlyUsedByOther ? 0.75 : 1,
|
|
102
|
+
boxSizing: 'border-box',
|
|
103
|
+
}, children: row.looping && (_jsx("span", { style: { position: 'absolute', right: 4, top: -1, fontSize: 10, color: theme.text }, children: "\u27F3" })) }) }), renaming && (_jsx(PromptDialog, { title: "Rename", defaultValue: row.name, onSubmit: (name) => {
|
|
104
|
+
binding.apply(() => (row.node.name = name));
|
|
105
|
+
setRenaming(false);
|
|
106
|
+
}, onCancel: () => setRenaming(false) }))] }));
|
|
107
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScalarGenerator } from '../core/values';
|
|
3
|
+
/**
|
|
4
|
+
* Unity-like value field: Constant | Random Between Two Constants | Curve,
|
|
5
|
+
* bound to a quarks value generator.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ValueField(props: {
|
|
8
|
+
label: string;
|
|
9
|
+
generator: ScalarGenerator | undefined;
|
|
10
|
+
onChange: (generator: ScalarGenerator) => void;
|
|
11
|
+
min?: number;
|
|
12
|
+
curveMax?: number;
|
|
13
|
+
}): React.JSX.Element;
|