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/src/index.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
export {EffectBinding} from './core/binding';
|
|
2
|
-
export type {EditorListener} from './core/binding';
|
|
3
|
-
export {EffectHistory} from './core/history';
|
|
4
|
-
export {createChildSystem} from './core/systems';
|
|
5
|
-
export {buildEffectTree, collectSystems, serializeEffectTree, serializeEffectForest} from './core/effectTree';
|
|
6
|
-
export type {EffectTreeNode} from './core/effectTree';
|
|
7
|
-
export type {ChildSystemOptions} from './core/systems';
|
|
8
|
-
export {readScalar, buildScalar, sampleCurve, readPieces, buildCurve, splitPieces, mergePieces} from './core/values';
|
|
9
|
-
export type {ScalarGenerator, ScalarMode, ScalarValueState, CurvePiece} from './core/values';
|
|
10
|
-
export {
|
|
11
|
-
DEFAULT_GRADIENT_STOPS,
|
|
12
|
-
buildGradient,
|
|
13
|
-
buildColorOverLife,
|
|
14
|
-
rgbaToVector4,
|
|
15
|
-
stopToCss,
|
|
16
|
-
stopsToCssGradient,
|
|
17
|
-
hexToRgb,
|
|
18
|
-
rgbToHex,
|
|
19
|
-
findBehavior,
|
|
20
|
-
} from './core/colors';
|
|
21
|
-
export type {GradientStop} from './core/colors';
|
|
22
|
-
export {
|
|
23
|
-
SHAPE_TYPES,
|
|
24
|
-
SHAPE_PARAM_KEYS,
|
|
25
|
-
DEFAULT_SHAPE_PARAMS,
|
|
26
|
-
getShapeType,
|
|
27
|
-
readShapeParams,
|
|
28
|
-
createShape,
|
|
29
|
-
} from './core/shapes';
|
|
30
|
-
export type {ShapeType, ShapeParams} from './core/shapes';
|
|
31
|
-
export {GroundPlaneResolver, ensureGroundResolver} from './core/collision';
|
|
32
|
-
export {ensurePortableTextureUrl} from './core/textureExport';
|
|
1
|
+
export {EffectBinding} from './core/binding';
|
|
2
|
+
export type {EditorListener} from './core/binding';
|
|
3
|
+
export {EffectHistory} from './core/history';
|
|
4
|
+
export {createChildSystem} from './core/systems';
|
|
5
|
+
export {buildEffectTree, collectSystems, serializeEffectTree, serializeEffectForest} from './core/effectTree';
|
|
6
|
+
export type {EffectTreeNode} from './core/effectTree';
|
|
7
|
+
export type {ChildSystemOptions} from './core/systems';
|
|
8
|
+
export {readScalar, buildScalar, sampleCurve, readPieces, buildCurve, splitPieces, mergePieces} from './core/values';
|
|
9
|
+
export type {ScalarGenerator, ScalarMode, ScalarValueState, CurvePiece} from './core/values';
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_GRADIENT_STOPS,
|
|
12
|
+
buildGradient,
|
|
13
|
+
buildColorOverLife,
|
|
14
|
+
rgbaToVector4,
|
|
15
|
+
stopToCss,
|
|
16
|
+
stopsToCssGradient,
|
|
17
|
+
hexToRgb,
|
|
18
|
+
rgbToHex,
|
|
19
|
+
findBehavior,
|
|
20
|
+
} from './core/colors';
|
|
21
|
+
export type {GradientStop} from './core/colors';
|
|
22
|
+
export {
|
|
23
|
+
SHAPE_TYPES,
|
|
24
|
+
SHAPE_PARAM_KEYS,
|
|
25
|
+
DEFAULT_SHAPE_PARAMS,
|
|
26
|
+
getShapeType,
|
|
27
|
+
readShapeParams,
|
|
28
|
+
createShape,
|
|
29
|
+
} from './core/shapes';
|
|
30
|
+
export type {ShapeType, ShapeParams} from './core/shapes';
|
|
31
|
+
export {GroundPlaneResolver, ensureGroundResolver} from './core/collision';
|
|
32
|
+
export {ensurePortableTextureUrl} from './core/textureExport';
|
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
import React, {useRef, useState} from 'react';
|
|
2
|
-
import {CurvePiece, mergePieces, sampleCurve, splitPieces} from '../core/values';
|
|
3
|
-
import {inputStyle, theme} from './theme';
|
|
4
|
-
|
|
5
|
-
const WIDTH = 218;
|
|
6
|
-
const HEIGHT = 84;
|
|
7
|
-
const PAD = 8;
|
|
8
|
-
|
|
9
|
-
interface PointRef {
|
|
10
|
-
piece: number;
|
|
11
|
-
/** 0 = segment start key, 1|2 = handles, 3 = final end key (last piece only). */
|
|
12
|
-
idx: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Multi-segment piecewise-bezier curve editor. Drag keys/handles vertically,
|
|
17
|
-
* drag inner keys horizontally, double-click the curve to add a key, double-click
|
|
18
|
-
* a key to remove it (Unity-like).
|
|
19
|
-
*/
|
|
20
|
-
export function CurveEditor(props: {pieces: CurvePiece[]; onChange: (pieces: CurvePiece[]) => void; maxValue?: number}) {
|
|
21
|
-
const svgRef = useRef<SVGSVGElement>(null);
|
|
22
|
-
const drag = useRef<PointRef | null>(null);
|
|
23
|
-
const [selected, setSelected] = useState<PointRef | null>(null);
|
|
24
|
-
|
|
25
|
-
const pieces = [...props.pieces].sort((a, b) => a.start - b.start);
|
|
26
|
-
const allValues = pieces.flatMap((piece) => piece.p);
|
|
27
|
-
const max = Math.max(props.maxValue ?? 1, ...allValues.map(Math.abs), 0.001);
|
|
28
|
-
|
|
29
|
-
const spanEnd = (i: number) => (i + 1 < pieces.length ? pieces[i + 1].start : 1);
|
|
30
|
-
const toX = (x: number) => PAD + x * (WIDTH - PAD * 2);
|
|
31
|
-
const toY = (v: number) => HEIGHT - PAD - (v / max) * (HEIGHT - PAD * 2);
|
|
32
|
-
const fromY = (y: number) => Math.round(Math.min(max, Math.max(0, ((HEIGHT - PAD - y) / (HEIGHT - PAD * 2)) * max)) * 1000) / 1000;
|
|
33
|
-
const fromX = (px: number) => Math.min(1, Math.max(0, (px - PAD) / (WIDTH - PAD * 2)));
|
|
34
|
-
|
|
35
|
-
// Control point positions: piece start key, two handles, plus the final end key.
|
|
36
|
-
const points: Array<{ref: PointRef; x: number; y: number; isKey: boolean}> = [];
|
|
37
|
-
pieces.forEach((piece, i) => {
|
|
38
|
-
const end = spanEnd(i);
|
|
39
|
-
const span = end - piece.start;
|
|
40
|
-
points.push({ref: {piece: i, idx: 0}, x: piece.start, y: piece.p[0], isKey: true});
|
|
41
|
-
points.push({ref: {piece: i, idx: 1}, x: piece.start + span / 3, y: piece.p[1], isKey: false});
|
|
42
|
-
points.push({ref: {piece: i, idx: 2}, x: piece.start + (2 * span) / 3, y: piece.p[2], isKey: false});
|
|
43
|
-
if (i === pieces.length - 1) {
|
|
44
|
-
points.push({ref: {piece: i, idx: 3}, x: 1, y: piece.p[3], isKey: true});
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const samples: string[] = [];
|
|
49
|
-
pieces.forEach((piece, i) => {
|
|
50
|
-
const end = spanEnd(i);
|
|
51
|
-
for (let s = 0; s <= 16; s++) {
|
|
52
|
-
const t = s / 16;
|
|
53
|
-
samples.push(`${toX(piece.start + t * (end - piece.start)).toFixed(1)},${toY(sampleCurve(piece.p, t)).toFixed(1)}`);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const applyDrag = (clientX: number, clientY: number) => {
|
|
58
|
-
const target = drag.current;
|
|
59
|
-
if (!target || !svgRef.current) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const rect = svgRef.current.getBoundingClientRect();
|
|
63
|
-
const v = fromY(clientY - rect.top);
|
|
64
|
-
const next = pieces.map((piece) => ({start: piece.start, p: [...piece.p] as CurvePiece['p']}));
|
|
65
|
-
const {piece, idx} = target;
|
|
66
|
-
if (idx === 0) {
|
|
67
|
-
next[piece].p[0] = v;
|
|
68
|
-
if (piece > 0) {
|
|
69
|
-
next[piece - 1].p[3] = v;
|
|
70
|
-
// Inner keys can also move horizontally between neighbours.
|
|
71
|
-
const x = fromX(clientX - rect.left);
|
|
72
|
-
const min = next[piece - 1].start + 0.02;
|
|
73
|
-
const maxX = (piece + 1 < next.length ? next[piece + 1].start : 1) - 0.02;
|
|
74
|
-
next[piece].start = Math.min(maxX, Math.max(min, x));
|
|
75
|
-
}
|
|
76
|
-
} else if (idx === 3) {
|
|
77
|
-
next[piece].p[3] = v;
|
|
78
|
-
} else {
|
|
79
|
-
next[piece].p[idx] = v;
|
|
80
|
-
}
|
|
81
|
-
props.onChange(next);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const selectedPoint = selected ? points.find((pt) => pt.ref.piece === selected.piece && pt.ref.idx === selected.idx) : null;
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div>
|
|
88
|
-
<svg
|
|
89
|
-
ref={svgRef}
|
|
90
|
-
width={WIDTH}
|
|
91
|
-
height={HEIGHT}
|
|
92
|
-
style={{background: 'rgba(5, 8, 18, 0.85)', border: `1px solid ${theme.border}`, borderRadius: 8, touchAction: 'none', display: 'block'}}
|
|
93
|
-
onPointerMove={(e) => applyDrag(e.clientX, e.clientY)}
|
|
94
|
-
onPointerUp={(e) => {
|
|
95
|
-
drag.current = null;
|
|
96
|
-
(e.target as Element).releasePointerCapture?.(e.pointerId);
|
|
97
|
-
}}
|
|
98
|
-
onDoubleClick={(e) => {
|
|
99
|
-
if ((e.target as Element).tagName === 'circle') {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const rect = svgRef.current!.getBoundingClientRect();
|
|
103
|
-
props.onChange(splitPieces(pieces, fromX(e.clientX - rect.left)));
|
|
104
|
-
}}
|
|
105
|
-
>
|
|
106
|
-
{[0.25, 0.5, 0.75].map((f) => (
|
|
107
|
-
<line key={f} x1={PAD} x2={WIDTH - PAD} y1={PAD + f * (HEIGHT - PAD * 2)} y2={PAD + f * (HEIGHT - PAD * 2)} stroke="#1c274a" strokeWidth={1} />
|
|
108
|
-
))}
|
|
109
|
-
<polyline points={samples.join(' ')} fill="none" stroke={theme.curveStroke} strokeWidth={2} />
|
|
110
|
-
{points.map((pt) => (
|
|
111
|
-
<circle
|
|
112
|
-
key={`${pt.ref.piece}-${pt.ref.idx}`}
|
|
113
|
-
className="qe-hover"
|
|
114
|
-
cx={toX(pt.x)}
|
|
115
|
-
cy={toY(pt.y)}
|
|
116
|
-
r={pt.isKey ? 5 : 4}
|
|
117
|
-
fill={pt.isKey ? theme.curveStroke : '#28407f'}
|
|
118
|
-
stroke={theme.curveStroke}
|
|
119
|
-
strokeWidth={1.5}
|
|
120
|
-
style={{cursor: pt.isKey && pt.ref.idx === 0 && pt.ref.piece > 0 ? 'move' : 'ns-resize'}}
|
|
121
|
-
onPointerDown={(e) => {
|
|
122
|
-
drag.current = pt.ref;
|
|
123
|
-
setSelected(pt.ref);
|
|
124
|
-
(e.target as Element).setPointerCapture(e.pointerId);
|
|
125
|
-
}}
|
|
126
|
-
onDoubleClick={(e) => {
|
|
127
|
-
e.stopPropagation();
|
|
128
|
-
if (pt.isKey && pt.ref.idx === 0 && pt.ref.piece > 0) {
|
|
129
|
-
setSelected(null);
|
|
130
|
-
props.onChange(mergePieces(pieces, pt.ref.piece));
|
|
131
|
-
}
|
|
132
|
-
}}
|
|
133
|
-
/>
|
|
134
|
-
))}
|
|
135
|
-
</svg>
|
|
136
|
-
<div style={{display: 'flex', gap: 4, marginTop: 4, alignItems: 'center'}}>
|
|
137
|
-
{selectedPoint ? (
|
|
138
|
-
<>
|
|
139
|
-
<span style={{fontSize: 10.5, color: theme.textDim}}>value</span>
|
|
140
|
-
<input
|
|
141
|
-
type="number"
|
|
142
|
-
className="qe-hover"
|
|
143
|
-
step={0.05}
|
|
144
|
-
style={{...inputStyle, fontSize: 11, padding: '3px 5px', width: 70}}
|
|
145
|
-
value={Math.round(selectedPoint.y * 1000) / 1000}
|
|
146
|
-
onChange={(e) => {
|
|
147
|
-
const parsed = parseFloat(e.target.value);
|
|
148
|
-
if (Number.isNaN(parsed)) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const next = pieces.map((piece) => ({start: piece.start, p: [...piece.p] as CurvePiece['p']}));
|
|
152
|
-
const {piece, idx} = selectedPoint.ref;
|
|
153
|
-
if (idx === 0 && piece > 0) {
|
|
154
|
-
next[piece - 1].p[3] = parsed;
|
|
155
|
-
}
|
|
156
|
-
next[piece].p[idx === 3 ? 3 : idx] = parsed;
|
|
157
|
-
props.onChange(next);
|
|
158
|
-
}}
|
|
159
|
-
/>
|
|
160
|
-
<span style={{fontSize: 10.5, color: theme.textDim}}>t={Math.round(selectedPoint.x * 100) / 100}</span>
|
|
161
|
-
</>
|
|
162
|
-
) : (
|
|
163
|
-
<span style={{fontSize: 10.5, color: theme.textDim}}>dbl-click curve: add key · dbl-click key: remove</span>
|
|
164
|
-
)}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
1
|
+
import React, {useRef, useState} from 'react';
|
|
2
|
+
import {CurvePiece, mergePieces, sampleCurve, splitPieces} from '../core/values';
|
|
3
|
+
import {inputStyle, theme} from './theme';
|
|
4
|
+
|
|
5
|
+
const WIDTH = 218;
|
|
6
|
+
const HEIGHT = 84;
|
|
7
|
+
const PAD = 8;
|
|
8
|
+
|
|
9
|
+
interface PointRef {
|
|
10
|
+
piece: number;
|
|
11
|
+
/** 0 = segment start key, 1|2 = handles, 3 = final end key (last piece only). */
|
|
12
|
+
idx: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Multi-segment piecewise-bezier curve editor. Drag keys/handles vertically,
|
|
17
|
+
* drag inner keys horizontally, double-click the curve to add a key, double-click
|
|
18
|
+
* a key to remove it (Unity-like).
|
|
19
|
+
*/
|
|
20
|
+
export function CurveEditor(props: {pieces: CurvePiece[]; onChange: (pieces: CurvePiece[]) => void; maxValue?: number}) {
|
|
21
|
+
const svgRef = useRef<SVGSVGElement>(null);
|
|
22
|
+
const drag = useRef<PointRef | null>(null);
|
|
23
|
+
const [selected, setSelected] = useState<PointRef | null>(null);
|
|
24
|
+
|
|
25
|
+
const pieces = [...props.pieces].sort((a, b) => a.start - b.start);
|
|
26
|
+
const allValues = pieces.flatMap((piece) => piece.p);
|
|
27
|
+
const max = Math.max(props.maxValue ?? 1, ...allValues.map(Math.abs), 0.001);
|
|
28
|
+
|
|
29
|
+
const spanEnd = (i: number) => (i + 1 < pieces.length ? pieces[i + 1].start : 1);
|
|
30
|
+
const toX = (x: number) => PAD + x * (WIDTH - PAD * 2);
|
|
31
|
+
const toY = (v: number) => HEIGHT - PAD - (v / max) * (HEIGHT - PAD * 2);
|
|
32
|
+
const fromY = (y: number) => Math.round(Math.min(max, Math.max(0, ((HEIGHT - PAD - y) / (HEIGHT - PAD * 2)) * max)) * 1000) / 1000;
|
|
33
|
+
const fromX = (px: number) => Math.min(1, Math.max(0, (px - PAD) / (WIDTH - PAD * 2)));
|
|
34
|
+
|
|
35
|
+
// Control point positions: piece start key, two handles, plus the final end key.
|
|
36
|
+
const points: Array<{ref: PointRef; x: number; y: number; isKey: boolean}> = [];
|
|
37
|
+
pieces.forEach((piece, i) => {
|
|
38
|
+
const end = spanEnd(i);
|
|
39
|
+
const span = end - piece.start;
|
|
40
|
+
points.push({ref: {piece: i, idx: 0}, x: piece.start, y: piece.p[0], isKey: true});
|
|
41
|
+
points.push({ref: {piece: i, idx: 1}, x: piece.start + span / 3, y: piece.p[1], isKey: false});
|
|
42
|
+
points.push({ref: {piece: i, idx: 2}, x: piece.start + (2 * span) / 3, y: piece.p[2], isKey: false});
|
|
43
|
+
if (i === pieces.length - 1) {
|
|
44
|
+
points.push({ref: {piece: i, idx: 3}, x: 1, y: piece.p[3], isKey: true});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const samples: string[] = [];
|
|
49
|
+
pieces.forEach((piece, i) => {
|
|
50
|
+
const end = spanEnd(i);
|
|
51
|
+
for (let s = 0; s <= 16; s++) {
|
|
52
|
+
const t = s / 16;
|
|
53
|
+
samples.push(`${toX(piece.start + t * (end - piece.start)).toFixed(1)},${toY(sampleCurve(piece.p, t)).toFixed(1)}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const applyDrag = (clientX: number, clientY: number) => {
|
|
58
|
+
const target = drag.current;
|
|
59
|
+
if (!target || !svgRef.current) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const rect = svgRef.current.getBoundingClientRect();
|
|
63
|
+
const v = fromY(clientY - rect.top);
|
|
64
|
+
const next = pieces.map((piece) => ({start: piece.start, p: [...piece.p] as CurvePiece['p']}));
|
|
65
|
+
const {piece, idx} = target;
|
|
66
|
+
if (idx === 0) {
|
|
67
|
+
next[piece].p[0] = v;
|
|
68
|
+
if (piece > 0) {
|
|
69
|
+
next[piece - 1].p[3] = v;
|
|
70
|
+
// Inner keys can also move horizontally between neighbours.
|
|
71
|
+
const x = fromX(clientX - rect.left);
|
|
72
|
+
const min = next[piece - 1].start + 0.02;
|
|
73
|
+
const maxX = (piece + 1 < next.length ? next[piece + 1].start : 1) - 0.02;
|
|
74
|
+
next[piece].start = Math.min(maxX, Math.max(min, x));
|
|
75
|
+
}
|
|
76
|
+
} else if (idx === 3) {
|
|
77
|
+
next[piece].p[3] = v;
|
|
78
|
+
} else {
|
|
79
|
+
next[piece].p[idx] = v;
|
|
80
|
+
}
|
|
81
|
+
props.onChange(next);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const selectedPoint = selected ? points.find((pt) => pt.ref.piece === selected.piece && pt.ref.idx === selected.idx) : null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
<svg
|
|
89
|
+
ref={svgRef}
|
|
90
|
+
width={WIDTH}
|
|
91
|
+
height={HEIGHT}
|
|
92
|
+
style={{background: 'rgba(5, 8, 18, 0.85)', border: `1px solid ${theme.border}`, borderRadius: 8, touchAction: 'none', display: 'block'}}
|
|
93
|
+
onPointerMove={(e) => applyDrag(e.clientX, e.clientY)}
|
|
94
|
+
onPointerUp={(e) => {
|
|
95
|
+
drag.current = null;
|
|
96
|
+
(e.target as Element).releasePointerCapture?.(e.pointerId);
|
|
97
|
+
}}
|
|
98
|
+
onDoubleClick={(e) => {
|
|
99
|
+
if ((e.target as Element).tagName === 'circle') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const rect = svgRef.current!.getBoundingClientRect();
|
|
103
|
+
props.onChange(splitPieces(pieces, fromX(e.clientX - rect.left)));
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{[0.25, 0.5, 0.75].map((f) => (
|
|
107
|
+
<line key={f} x1={PAD} x2={WIDTH - PAD} y1={PAD + f * (HEIGHT - PAD * 2)} y2={PAD + f * (HEIGHT - PAD * 2)} stroke="#1c274a" strokeWidth={1} />
|
|
108
|
+
))}
|
|
109
|
+
<polyline points={samples.join(' ')} fill="none" stroke={theme.curveStroke} strokeWidth={2} />
|
|
110
|
+
{points.map((pt) => (
|
|
111
|
+
<circle
|
|
112
|
+
key={`${pt.ref.piece}-${pt.ref.idx}`}
|
|
113
|
+
className="qe-hover"
|
|
114
|
+
cx={toX(pt.x)}
|
|
115
|
+
cy={toY(pt.y)}
|
|
116
|
+
r={pt.isKey ? 5 : 4}
|
|
117
|
+
fill={pt.isKey ? theme.curveStroke : '#28407f'}
|
|
118
|
+
stroke={theme.curveStroke}
|
|
119
|
+
strokeWidth={1.5}
|
|
120
|
+
style={{cursor: pt.isKey && pt.ref.idx === 0 && pt.ref.piece > 0 ? 'move' : 'ns-resize'}}
|
|
121
|
+
onPointerDown={(e) => {
|
|
122
|
+
drag.current = pt.ref;
|
|
123
|
+
setSelected(pt.ref);
|
|
124
|
+
(e.target as Element).setPointerCapture(e.pointerId);
|
|
125
|
+
}}
|
|
126
|
+
onDoubleClick={(e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
if (pt.isKey && pt.ref.idx === 0 && pt.ref.piece > 0) {
|
|
129
|
+
setSelected(null);
|
|
130
|
+
props.onChange(mergePieces(pieces, pt.ref.piece));
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
))}
|
|
135
|
+
</svg>
|
|
136
|
+
<div style={{display: 'flex', gap: 4, marginTop: 4, alignItems: 'center'}}>
|
|
137
|
+
{selectedPoint ? (
|
|
138
|
+
<>
|
|
139
|
+
<span style={{fontSize: 10.5, color: theme.textDim}}>value</span>
|
|
140
|
+
<input
|
|
141
|
+
type="number"
|
|
142
|
+
className="qe-hover"
|
|
143
|
+
step={0.05}
|
|
144
|
+
style={{...inputStyle, fontSize: 11, padding: '3px 5px', width: 70}}
|
|
145
|
+
value={Math.round(selectedPoint.y * 1000) / 1000}
|
|
146
|
+
onChange={(e) => {
|
|
147
|
+
const parsed = parseFloat(e.target.value);
|
|
148
|
+
if (Number.isNaN(parsed)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const next = pieces.map((piece) => ({start: piece.start, p: [...piece.p] as CurvePiece['p']}));
|
|
152
|
+
const {piece, idx} = selectedPoint.ref;
|
|
153
|
+
if (idx === 0 && piece > 0) {
|
|
154
|
+
next[piece - 1].p[3] = parsed;
|
|
155
|
+
}
|
|
156
|
+
next[piece].p[idx === 3 ? 3 : idx] = parsed;
|
|
157
|
+
props.onChange(next);
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
<span style={{fontSize: 10.5, color: theme.textDim}}>t={Math.round(selectedPoint.x * 100) / 100}</span>
|
|
161
|
+
</>
|
|
162
|
+
) : (
|
|
163
|
+
<span style={{fontSize: 10.5, color: theme.textDim}}>dbl-click curve: add key · dbl-click key: remove</span>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -1,111 +1,125 @@
|
|
|
1
|
-
import React, {useSyncExternalStore} from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
1
|
+
import React, {useSyncExternalStore} from 'react';
|
|
2
|
+
import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
|
|
3
|
+
import {EffectBinding} from '../core/binding';
|
|
4
|
+
import {ObjectModule} from './ObjectModule';
|
|
5
|
+
import {
|
|
6
|
+
ColorOverLifeModule,
|
|
7
|
+
EmissionModule,
|
|
8
|
+
MainModule,
|
|
9
|
+
RendererModule,
|
|
10
|
+
ShapeModule,
|
|
11
|
+
SizeOverLifeModule,
|
|
12
|
+
} from './modules';
|
|
13
|
+
import type {GeometryData, GeometryOption, TextureOption} from './modules';
|
|
14
|
+
import {
|
|
15
|
+
CollisionModule,
|
|
16
|
+
ColorBySpeedModule,
|
|
17
|
+
EmitDirectionModule,
|
|
18
|
+
ForceOverLifeModule,
|
|
19
|
+
GravityModule,
|
|
20
|
+
InheritVelocityModule,
|
|
21
|
+
LimitSpeedOverLifeModule,
|
|
22
|
+
NoiseModule,
|
|
23
|
+
OrbitOverLifeModule,
|
|
24
|
+
RotationBySpeedModule,
|
|
25
|
+
RotationOverLifeModule,
|
|
26
|
+
SizeBySpeedModule,
|
|
27
|
+
SpeedOverLifeModule,
|
|
28
|
+
TextureSheetModule,
|
|
29
|
+
TurbulenceModule,
|
|
30
|
+
VelocityOverLifeModule,
|
|
31
|
+
WidthOverTrailModule,
|
|
32
|
+
} from './behaviorModules';
|
|
33
|
+
import {SubEmittersModule} from './SubEmittersModule';
|
|
34
|
+
import {theme} from './theme';
|
|
35
|
+
import type {ParticleSystem} from 'babylon.quarks';
|
|
36
|
+
|
|
37
|
+
export interface EffectEditorProps {
|
|
38
|
+
binding: EffectBinding;
|
|
39
|
+
/** Currently selected system, lifted up so the timeline panel and this module stack agree. */
|
|
40
|
+
selectedSystem: ParticleSystem;
|
|
41
|
+
gizmoTargetNode: TransformNode;
|
|
42
|
+
gizmoTargetLabel: string;
|
|
43
|
+
/** Texture presets offered in the Renderer module (host supplies the loader). */
|
|
44
|
+
textureOptions?: TextureOption[];
|
|
45
|
+
/** Mesh presets offered for the Mesh render mode (host supplies the geometry buffers). */
|
|
46
|
+
geometryOptions?: GeometryOption[];
|
|
47
|
+
resolveTexture?: (url: string) => unknown;
|
|
48
|
+
/** Host loader for "Load from file…" in the Mesh render mode (e.g. a GLB importer). */
|
|
49
|
+
resolveGeometry?: (file: File) => Promise<GeometryData>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Shuriken-style effect editor: a stacked module inspector for the selected system.
|
|
54
|
+
* Embed it anywhere React runs — the standalone examples page or a BabylonJS Editor plugin.
|
|
55
|
+
* Module order mirrors Unity's Particle System inspector.
|
|
56
|
+
*/
|
|
57
|
+
export function EffectEditor(props: EffectEditorProps) {
|
|
58
|
+
useSyncExternalStore(
|
|
59
|
+
(onStoreChange) => props.binding.subscribe(onStoreChange),
|
|
60
|
+
() => props.binding.getRevision()
|
|
61
|
+
);
|
|
62
|
+
const allSystems = [props.binding.system, ...props.binding.subSystems];
|
|
63
|
+
// Selection survives re-renders but falls back to root if the system was removed/replaced.
|
|
64
|
+
const activeSystem = allSystems.includes(props.selectedSystem) ? props.selectedSystem : props.binding.system;
|
|
65
|
+
|
|
66
|
+
// Inspector edits go through a per-system binding; changes route into the root
|
|
67
|
+
// binding's undo history and re-render via the subscription below.
|
|
68
|
+
const binding = React.useMemo(() => {
|
|
69
|
+
if (activeSystem === props.binding.system) {
|
|
70
|
+
return props.binding;
|
|
71
|
+
}
|
|
72
|
+
const nested = new EffectBinding(activeSystem);
|
|
73
|
+
nested.onBeforeChange = () => props.binding.onBeforeChange?.();
|
|
74
|
+
return nested;
|
|
75
|
+
}, [props.binding, activeSystem]);
|
|
76
|
+
useSyncExternalStore(
|
|
77
|
+
(onStoreChange) => binding.subscribe(onStoreChange),
|
|
78
|
+
() => binding.getRevision()
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const gizmoTargetIsEmitter = allSystems.some((s) => s.emitter === props.gizmoTargetNode);
|
|
82
|
+
if (!gizmoTargetIsEmitter) {
|
|
83
|
+
return (
|
|
84
|
+
<div style={{fontFamily: theme.font, color: theme.text}}>
|
|
85
|
+
<ObjectModule node={props.gizmoTargetNode} label={props.gizmoTargetLabel} />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div style={{fontFamily: theme.font, color: theme.text}}>
|
|
92
|
+
<ObjectModule node={props.gizmoTargetNode} label={props.gizmoTargetLabel} />
|
|
93
|
+
<MainModule binding={binding} />
|
|
94
|
+
<EmissionModule binding={binding} />
|
|
95
|
+
<ShapeModule binding={binding} />
|
|
96
|
+
<EmitDirectionModule binding={binding} />
|
|
97
|
+
<VelocityOverLifeModule binding={binding} />
|
|
98
|
+
<SpeedOverLifeModule binding={binding} />
|
|
99
|
+
<LimitSpeedOverLifeModule binding={binding} />
|
|
100
|
+
<InheritVelocityModule binding={binding} />
|
|
101
|
+
<ForceOverLifeModule binding={binding} />
|
|
102
|
+
<GravityModule binding={binding} />
|
|
103
|
+
<OrbitOverLifeModule binding={binding} />
|
|
104
|
+
<ColorOverLifeModule binding={binding} />
|
|
105
|
+
<ColorBySpeedModule binding={binding} />
|
|
106
|
+
<SizeOverLifeModule binding={binding} />
|
|
107
|
+
<SizeBySpeedModule binding={binding} />
|
|
108
|
+
<RotationOverLifeModule binding={binding} />
|
|
109
|
+
<RotationBySpeedModule binding={binding} />
|
|
110
|
+
<NoiseModule binding={binding} />
|
|
111
|
+
<TurbulenceModule binding={binding} />
|
|
112
|
+
<CollisionModule binding={binding} />
|
|
113
|
+
<WidthOverTrailModule binding={binding} />
|
|
114
|
+
<SubEmittersModule binding={binding} rootBinding={props.binding} />
|
|
115
|
+
<TextureSheetModule binding={binding} />
|
|
116
|
+
<RendererModule
|
|
117
|
+
binding={binding}
|
|
118
|
+
textureOptions={props.textureOptions}
|
|
119
|
+
geometryOptions={props.geometryOptions}
|
|
120
|
+
resolveTexture={props.resolveTexture}
|
|
121
|
+
resolveGeometry={props.resolveGeometry}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|