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.
Files changed (37) hide show
  1. package/dist/core/timeline.d.ts +1 -1
  2. package/dist/core/timeline.js +3 -2
  3. package/dist/react/EffectEditor.d.ts +3 -0
  4. package/dist/react/EffectEditor.js +6 -1
  5. package/dist/react/EffectEditorHost.js +72 -11
  6. package/dist/react/ObjectModule.d.ts +6 -0
  7. package/dist/react/ObjectModule.js +28 -0
  8. package/dist/react/TimelinePanel.d.ts +4 -3
  9. package/dist/react/TimelinePanel.js +60 -10
  10. package/dist/react/TimelineTrackRow.d.ts +2 -2
  11. package/dist/react/TimelineTrackRow.js +16 -12
  12. package/dist/react/theme.js +44 -21
  13. package/package.json +68 -63
  14. package/src/core/binding.ts +105 -105
  15. package/src/core/collision.ts +40 -40
  16. package/src/core/groups.ts +29 -29
  17. package/src/core/loadEffect.ts +102 -102
  18. package/src/core/scrub.ts +28 -28
  19. package/src/core/shapes.ts +133 -133
  20. package/src/core/systems.ts +98 -98
  21. package/src/core/timeline.ts +95 -94
  22. package/src/core/values.ts +113 -113
  23. package/src/index.ts +32 -32
  24. package/src/react/CurveEditor.tsx +168 -168
  25. package/src/react/EffectEditor.tsx +125 -111
  26. package/src/react/EffectEditorHost.tsx +466 -377
  27. package/src/react/GradientEditor.tsx +127 -127
  28. package/src/react/ModuleSection.tsx +54 -54
  29. package/src/react/ObjectModule.tsx +47 -0
  30. package/src/react/PromptDialog.tsx +85 -85
  31. package/src/react/TimelinePanel.tsx +412 -354
  32. package/src/react/TimelineTrackRow.tsx +291 -267
  33. package/src/react/behaviorModules.tsx +658 -658
  34. package/src/react/fields.tsx +76 -76
  35. package/src/react/index.ts +43 -43
  36. package/src/react/modules.tsx +1034 -1034
  37. 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 {EffectBinding} from '../core/binding';
3
- import {
4
- ColorOverLifeModule,
5
- EmissionModule,
6
- MainModule,
7
- RendererModule,
8
- ShapeModule,
9
- SizeOverLifeModule,
10
- } from './modules';
11
- import type {GeometryData, GeometryOption, TextureOption} from './modules';
12
- import {
13
- CollisionModule,
14
- ColorBySpeedModule,
15
- EmitDirectionModule,
16
- ForceOverLifeModule,
17
- GravityModule,
18
- InheritVelocityModule,
19
- LimitSpeedOverLifeModule,
20
- NoiseModule,
21
- OrbitOverLifeModule,
22
- RotationBySpeedModule,
23
- RotationOverLifeModule,
24
- SizeBySpeedModule,
25
- SpeedOverLifeModule,
26
- TextureSheetModule,
27
- TurbulenceModule,
28
- VelocityOverLifeModule,
29
- WidthOverTrailModule,
30
- } from './behaviorModules';
31
- import {SubEmittersModule} from './SubEmittersModule';
32
- import {theme} from './theme';
33
- import type {ParticleSystem} from 'babylon.quarks';
34
-
35
- export interface EffectEditorProps {
36
- binding: EffectBinding;
37
- /** Currently selected system, lifted up so the timeline panel and this module stack agree. */
38
- selectedSystem: ParticleSystem;
39
- /** Texture presets offered in the Renderer module (host supplies the loader). */
40
- textureOptions?: TextureOption[];
41
- /** Mesh presets offered for the Mesh render mode (host supplies the geometry buffers). */
42
- geometryOptions?: GeometryOption[];
43
- resolveTexture?: (url: string) => unknown;
44
- /** Host loader for "Load from file…" in the Mesh render mode (e.g. a GLB importer). */
45
- resolveGeometry?: (file: File) => Promise<GeometryData>;
46
- }
47
-
48
- /**
49
- * Shuriken-style effect editor: a stacked module inspector for the selected system.
50
- * Embed it anywhere React runs — the standalone examples page or a BabylonJS Editor plugin.
51
- * Module order mirrors Unity's Particle System inspector.
52
- */
53
- export function EffectEditor(props: EffectEditorProps) {
54
- useSyncExternalStore(
55
- (onStoreChange) => props.binding.subscribe(onStoreChange),
56
- () => props.binding.getRevision()
57
- );
58
- const allSystems = [props.binding.system, ...props.binding.subSystems];
59
- // Selection survives re-renders but falls back to root if the system was removed/replaced.
60
- const activeSystem = allSystems.includes(props.selectedSystem) ? props.selectedSystem : props.binding.system;
61
-
62
- // Inspector edits go through a per-system binding; changes route into the root
63
- // binding's undo history and re-render via the subscription below.
64
- const binding = React.useMemo(() => {
65
- if (activeSystem === props.binding.system) {
66
- return props.binding;
67
- }
68
- const nested = new EffectBinding(activeSystem);
69
- nested.onBeforeChange = () => props.binding.onBeforeChange?.();
70
- return nested;
71
- }, [props.binding, activeSystem]);
72
- useSyncExternalStore(
73
- (onStoreChange) => binding.subscribe(onStoreChange),
74
- () => binding.getRevision()
75
- );
76
-
77
- return (
78
- <div style={{fontFamily: theme.font, color: theme.text}}>
79
- <MainModule binding={binding} />
80
- <EmissionModule binding={binding} />
81
- <ShapeModule binding={binding} />
82
- <EmitDirectionModule binding={binding} />
83
- <VelocityOverLifeModule binding={binding} />
84
- <SpeedOverLifeModule binding={binding} />
85
- <LimitSpeedOverLifeModule binding={binding} />
86
- <InheritVelocityModule binding={binding} />
87
- <ForceOverLifeModule binding={binding} />
88
- <GravityModule binding={binding} />
89
- <OrbitOverLifeModule binding={binding} />
90
- <ColorOverLifeModule binding={binding} />
91
- <ColorBySpeedModule binding={binding} />
92
- <SizeOverLifeModule binding={binding} />
93
- <SizeBySpeedModule binding={binding} />
94
- <RotationOverLifeModule binding={binding} />
95
- <RotationBySpeedModule binding={binding} />
96
- <NoiseModule binding={binding} />
97
- <TurbulenceModule binding={binding} />
98
- <CollisionModule binding={binding} />
99
- <WidthOverTrailModule binding={binding} />
100
- <SubEmittersModule binding={binding} rootBinding={props.binding} />
101
- <TextureSheetModule binding={binding} />
102
- <RendererModule
103
- binding={binding}
104
- textureOptions={props.textureOptions}
105
- geometryOptions={props.geometryOptions}
106
- resolveTexture={props.resolveTexture}
107
- resolveGeometry={props.resolveGeometry}
108
- />
109
- </div>
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
+ }