babylon.quarks-editor 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import type {ValueGenerator} from 'babylon.quarks';
2
+ import {RandomBetweenCurves} from 'babylon.quarks';
2
3
  import React from 'react';
3
4
  import {
4
5
  AnyGenerator,
@@ -8,6 +9,7 @@ import {
8
9
  buildScalar,
9
10
  readPieces,
10
11
  readScalar,
12
+ withCurves,
11
13
  } from '../core/values.js';
12
14
  import {CurveEditor} from './CurveEditor.js';
13
15
  import {NumberField, Row, SelectField} from './fields.js';
@@ -51,13 +53,15 @@ function isValueGenerator(generator: ScalarGenerator): generator is ValueGenerat
51
53
  }
52
54
 
53
55
  /**
54
- * Unity-like value field: Constant | Random Between Two Constants | Curve, bound to a quarks value
55
- * generator. With `curve={false}` it offers only the first two, for settings sampled once.
56
+ * Unity-like value field: Constant | Random Between Two Constants | Curve | Random Between Two
57
+ * Curves, bound to a quarks value generator. With `curve={false}` it offers only the first two,
58
+ * for settings sampled once.
56
59
  */
57
60
  export function ValueField(props: CurveValueFieldProps | ConstantValueFieldProps): React.ReactElement {
58
61
  const read = readScalar(props.generator);
59
62
  // A setting without curves shows a curve it was loaded with as a constant, so an edit replaces it.
60
- const state: ScalarValueState = props.curve === false && read.mode === 'curve' ? {...read, mode: 'constant'} : read;
63
+ const curved = read.mode === 'curve' || read.mode === 'curves';
64
+ const state: ScalarValueState = props.curve === false && curved ? {...read, mode: 'constant'} : read;
61
65
 
62
66
  const emit = (generator: ScalarGenerator) => {
63
67
  if (props.curve !== false) {
@@ -76,9 +80,19 @@ export function ValueField(props: CurveValueFieldProps | ConstantValueFieldProps
76
80
  options={[
77
81
  {value: 'constant', label: 'Constant'},
78
82
  {value: 'random', label: 'Random between two'},
79
- ...(props.curve !== false ? [{value: 'curve' as const, label: 'Curve'}] : []),
83
+ ...(props.curve !== false
84
+ ? [
85
+ {value: 'curve' as const, label: 'Curve'},
86
+ {value: 'curves' as const, label: 'Random between two curves'},
87
+ ]
88
+ : []),
80
89
  ]}
81
- onChange={(mode) => commit({mode})}
90
+ onChange={(mode) =>
91
+ // Between one curve and two, every segment is kept; other switches start afresh.
92
+ (mode === 'curve' || mode === 'curves') && curved
93
+ ? emit(withCurves(props.generator, mode))
94
+ : commit({mode})
95
+ }
82
96
  />
83
97
  </Row>
84
98
  {state.mode === 'constant' && (
@@ -105,6 +119,32 @@ export function ValueField(props: CurveValueFieldProps | ConstantValueFieldProps
105
119
  />
106
120
  </div>
107
121
  )}
122
+ {state.mode === 'curves' && (
123
+ <>
124
+ {(['lower', 'upper'] as const).map((bound, index) => (
125
+ <div key={bound} style={{marginTop: 6}}>
126
+ <span style={captionStyle}>{bound}</span>
127
+ <CurveEditor
128
+ pieces={readPieces(props.generator, index === 1)}
129
+ maxValue={props.curveMax}
130
+ onChange={(pieces) =>
131
+ emit(
132
+ index === 0
133
+ ? new RandomBetweenCurves(
134
+ buildCurve(pieces),
135
+ buildCurve(readPieces(props.generator, true))
136
+ )
137
+ : new RandomBetweenCurves(
138
+ buildCurve(readPieces(props.generator)),
139
+ buildCurve(pieces)
140
+ )
141
+ )
142
+ }
143
+ />
144
+ </div>
145
+ ))}
146
+ </>
147
+ )}
108
148
  </>
109
149
  );
110
150
  }
@@ -29,17 +29,19 @@ import {
29
29
  TextureSequencer,
30
30
  TurbulenceField,
31
31
  Vector3,
32
+ Vector3Function,
32
33
  VelocityOverLife,
33
34
  WidthOverLength,
34
35
  } from 'babylon.quarks';
35
36
  import React from 'react';
36
37
  import {EffectBinding} from '../core/binding.js';
37
38
  import {buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops} from '../core/colors.js';
38
- import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values.js';
39
- import {CurveEditor} from './CurveEditor.js';
39
+ import {buildScalar, readScalar} from '../core/values.js';
40
+ import {AxesCurveField} from './AxesCurveField.js';
40
41
  import {CheckboxField, NumberField, Row, SelectField} from './fields.js';
41
42
  import {GradientEditor} from './GradientEditor.js';
42
43
  import {iconStyle} from './icons.js';
44
+ import {LifeCurveField} from './LifeCurveField.js';
43
45
  import type {TextureOption} from './modules.js';
44
46
  import {ModuleSection, type ModuleProps} from './ModuleSection.js';
45
47
  import {theme} from './theme.js';
@@ -94,10 +96,10 @@ export function SpeedOverLifeModule({binding}: ModuleProps) {
94
96
  >
95
97
  {(behavior) => (
96
98
  <div style={{marginTop: 6}}>
97
- <CurveEditor
98
- pieces={readPieces(behavior.speed)}
99
+ <LifeCurveField
100
+ generator={behavior.speed}
99
101
  maxValue={2}
100
- onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces)))}
102
+ onChange={(curve) => binding.apply(() => (behavior.speed = curve))}
101
103
  />
102
104
  </div>
103
105
  )}
@@ -119,12 +121,24 @@ export function LimitSpeedOverLifeModule({binding}: ModuleProps) {
119
121
  {(behavior) => (
120
122
  <>
121
123
  <div style={{marginTop: 6}}>
122
- <CurveEditor
123
- pieces={readPieces(behavior.speed)}
124
+ <AxesCurveField
125
+ generator={behavior.speed}
124
126
  maxValue={8}
125
- onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces)))}
127
+ onChange={(speed) => binding.apply(() => (behavior.speed = speed))}
126
128
  />
127
129
  </div>
130
+ {behavior.speed instanceof Vector3Function && (
131
+ <Row label="Space" hint="The axes each limit is measured along: the emitter's or the world's.">
132
+ <SelectField
133
+ value={behavior.space}
134
+ options={[
135
+ {value: 'local', label: 'Local'},
136
+ {value: 'world', label: 'World'},
137
+ ]}
138
+ onChange={(v) => binding.apply(() => (behavior.space = v))}
139
+ />
140
+ </Row>
141
+ )}
128
142
  <Row label="Dampen">
129
143
  <NumberField
130
144
  value={behavior.dampen}
@@ -942,10 +956,10 @@ export function SizeBySpeedModule({binding}: ModuleProps) {
942
956
  {(behavior) => (
943
957
  <>
944
958
  <div style={{marginTop: 6}}>
945
- <CurveEditor
946
- pieces={readPieces(behavior.size)}
959
+ <AxesCurveField
960
+ generator={behavior.size}
947
961
  maxValue={2}
948
- onChange={(pieces) => binding.apply(() => (behavior.size = buildCurve(pieces)))}
962
+ onChange={(size) => binding.apply(() => (behavior.size = size))}
949
963
  />
950
964
  </div>
951
965
  <SpeedRangeRows binding={binding} behavior={behavior} />
@@ -1015,10 +1029,10 @@ export function WidthOverTrailModule({binding}: ModuleProps) {
1015
1029
  {(behavior) => (
1016
1030
  <>
1017
1031
  <div style={{marginTop: 6}}>
1018
- <CurveEditor
1019
- pieces={readPieces(behavior.width)}
1032
+ <LifeCurveField
1033
+ generator={behavior.width}
1020
1034
  maxValue={2}
1021
- onChange={(pieces) => binding.apply(() => (behavior.width = buildCurve(pieces)))}
1035
+ onChange={(curve) => binding.apply(() => (behavior.width = curve))}
1022
1036
  />
1023
1037
  </div>
1024
1038
  {/* Off: the curve multiplies the particle size. On: it is the width in world units (files from 0.21 and earlier). */}
@@ -1265,10 +1279,10 @@ export function TextureSheetModule({binding}: ModuleProps) {
1265
1279
  )}
1266
1280
  </Row>
1267
1281
  <div style={{marginTop: 6}}>
1268
- <CurveEditor
1269
- pieces={readPieces(frameBehavior.frame)}
1282
+ <LifeCurveField
1283
+ generator={frameBehavior.frame}
1270
1284
  maxValue={Math.max(1, tiles - 1)}
1271
- onChange={(pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces)))}
1285
+ onChange={(curve) => binding.apply(() => (frameBehavior.frame = curve))}
1272
1286
  />
1273
1287
  </div>
1274
1288
  </>
@@ -33,7 +33,7 @@ const INSPECTOR_HINTS: Record<string, string> = {
33
33
  Looping: 'When enabled, the effect restarts from the beginning after duration ends.',
34
34
  'World space': 'Simulates in world coordinates so moving the emitter does not drag existing particles.',
35
35
  'Scaling mode':
36
- 'What the emitter’s scale does to a world-space system: only the shape and sizes, or speeds and forces too (Unity’s Scaling Mode).',
36
+ 'Which scale reaches the effect (Unity’s Scaling Mode): Local, the emitter’s own; Hierarchy, its parents’ too; Shape, the whole scale on the emission shape alone.',
37
37
  Prewarm: 'Simulates the effect once at start so it already looks mid-playback when play begins.',
38
38
  'Warm-up time':
39
39
  'Seconds of the cycle simulated before the first frame of a looping system (Niagara’s Warmup Time); 0 warms up a whole cycle, as Unity does.',
@@ -20,10 +20,12 @@ import {
20
20
  BoxEmitFrom,
21
21
  ColorOverLife,
22
22
  ColorRange,
23
+ ConeEmitFrom,
23
24
  ConstantColor,
24
25
  ConstantValue,
25
26
  EulerGenerator,
26
27
  Gradient,
28
+ GridSampling,
27
29
  IntervalValue,
28
30
  RandomColor,
29
31
  RandomColorBetweenGradient,
@@ -56,8 +58,8 @@ import {
56
58
  getShapeType,
57
59
  readShapeParams,
58
60
  } from '../core/shapes.js';
59
- import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values.js';
60
- import {CurveEditor} from './CurveEditor.js';
61
+ import {buildCurve, buildScalar, readScalar} from '../core/values.js';
62
+ import {AxesCurveField} from './AxesCurveField.js';
61
63
  import {GradientEditor} from './GradientEditor.js';
62
64
  import {InspectorInfoIcon} from './InspectorInfoIcon.js';
63
65
  import {ModuleSection, type ModuleProps} from './ModuleSection.js';
@@ -113,6 +115,7 @@ const PARAM_LABELS: {[key: string]: string} = {
113
115
  column: 'Columns',
114
116
  row: 'Rows',
115
117
  depth: 'Depth',
118
+ length: 'Length',
116
119
  speed: 'Speed (lengths/s)',
117
120
  };
118
121
 
@@ -260,18 +263,17 @@ export function MainModule({binding}: ModuleProps) {
260
263
  <Row label="World space">
261
264
  <CheckboxField value={system.worldSpace} onChange={(v) => binding.apply((s) => (s.worldSpace = v))} />
262
265
  </Row>
263
- {system.worldSpace && (
264
- <Row label="Scaling mode">
265
- <SelectField<ScalingMode>
266
- value={system.scalingMode}
267
- options={[
268
- {value: 'shape', label: 'Shape (sizes and shape only)'},
269
- {value: 'hierarchy', label: 'Hierarchy (motion scales too)'},
270
- ]}
271
- onChange={(mode) => binding.apply((s) => (s.scalingMode = mode))}
272
- />
273
- </Row>
274
- )}
266
+ <Row label="Scaling mode">
267
+ <SelectField<ScalingMode>
268
+ value={system.scalingMode}
269
+ options={[
270
+ {value: 'local', label: 'Local (own scale only)'},
271
+ {value: 'hierarchy', label: 'Hierarchy (parents too)'},
272
+ {value: 'shape', label: 'Shape (shape only)'},
273
+ ]}
274
+ onChange={(mode) => binding.apply((s) => (s.scalingMode = mode))}
275
+ />
276
+ </Row>
275
277
  <Row label="Prewarm">
276
278
  <CheckboxField value={system.prewarm} onChange={(v) => binding.apply((s) => (s.prewarm = v))} />
277
279
  </Row>
@@ -318,6 +320,20 @@ export function MainModule({binding}: ModuleProps) {
318
320
  onChange={(mode) => binding.apply((s) => (s.cullingMode = mode as CullingMode))}
319
321
  />
320
322
  </Row>
323
+ <Row
324
+ label="Stop action"
325
+ hint="What the system does once it has finished, as Unity's Stop Action. The editor saves it but does not apply it, so previews replay."
326
+ >
327
+ <SelectField
328
+ value={system.stopAction}
329
+ options={[
330
+ {value: 'none', label: 'None'},
331
+ {value: 'disable', label: 'Disable'},
332
+ {value: 'destroy', label: 'Destroy'},
333
+ ]}
334
+ onChange={(v) => binding.apply((s) => (s.stopAction = v))}
335
+ />
336
+ </Row>
321
337
  <ValueField
322
338
  label="Start delay"
323
339
  generator={system.startDelay}
@@ -725,17 +741,20 @@ export function ShapeModule({binding}: ModuleProps) {
725
741
  onChange={(next) => binding.apply((s) => (s.emitterShape = createShape(next, params)))}
726
742
  />
727
743
  </Row>
728
- {keys.map((key) => (
729
- <Row key={key} label={PARAM_LABELS[key]}>
730
- <NumberField
731
- value={params[key] as number}
732
- min={0}
733
- onChange={(v) =>
734
- binding.apply((s) => (s.emitterShape = createShape(type, {...params, [key]: v})))
735
- }
736
- />
737
- </Row>
738
- ))}
744
+ {keys
745
+ // A cone's Length only shapes Emit from Volume; Unity shows it for Cone Volume alone.
746
+ .filter((key) => key !== 'length' || params.coneEmitFrom === ConeEmitFrom.Volume)
747
+ .map((key) => (
748
+ <Row key={key} label={PARAM_LABELS[key]}>
749
+ <NumberField
750
+ value={params[key] as number}
751
+ min={0}
752
+ onChange={(v) =>
753
+ binding.apply((s) => (s.emitterShape = createShape(type, {...params, [key]: v})))
754
+ }
755
+ />
756
+ </Row>
757
+ ))}
739
758
  {type === 'mesh_surface' && (
740
759
  <Row label="Source mesh">
741
760
  <SelectField
@@ -767,6 +786,34 @@ export function ShapeModule({binding}: ModuleProps) {
767
786
  />
768
787
  </Row>
769
788
  )}
789
+ {type === 'cone' && (
790
+ <Row label="Emit from">
791
+ <SelectField
792
+ value={params.coneEmitFrom}
793
+ options={[
794
+ {value: ConeEmitFrom.Base, label: 'Base'},
795
+ {value: ConeEmitFrom.Volume, label: 'Volume'},
796
+ ]}
797
+ onChange={(coneEmitFrom) =>
798
+ binding.apply((s) => (s.emitterShape = createShape(type, {...params, coneEmitFrom})))
799
+ }
800
+ />
801
+ </Row>
802
+ )}
803
+ {type === 'grid' && (
804
+ <Row label="Sampling">
805
+ <SelectField
806
+ value={params.sampling}
807
+ options={[
808
+ {value: GridSampling.Random, label: 'Random'},
809
+ {value: GridSampling.Sequential, label: 'Sequential'},
810
+ ]}
811
+ onChange={(sampling) =>
812
+ binding.apply((s) => (s.emitterShape = createShape(type, {...params, sampling})))
813
+ }
814
+ />
815
+ </Row>
816
+ )}
770
817
  {!SHAPES_WITHOUT_MODE_SPREAD.has(type) && (
771
818
  <>
772
819
  <Row label="Emit mode">
@@ -824,13 +871,13 @@ export function SizeOverLifeModule({binding}: ModuleProps) {
824
871
  }
825
872
  >
826
873
  <div style={{marginTop: 6}}>
827
- <CurveEditor
828
- pieces={behavior ? readPieces(behavior.size) : [{start: 0, p: curve}]}
874
+ <AxesCurveField
875
+ generator={behavior ? behavior.size : buildCurve([{start: 0, p: curve}])}
829
876
  maxValue={2}
830
- onChange={(pieces) =>
877
+ onChange={(size) =>
831
878
  binding.apply((s) => {
832
879
  removeBehavior(s, 'SizeOverLife');
833
- s.addBehavior(new SizeOverLife(buildCurve(pieces)));
880
+ s.addBehavior(new SizeOverLife(size));
834
881
  })
835
882
  }
836
883
  />
@@ -1629,6 +1676,48 @@ export function RendererModule({
1629
1676
  />
1630
1677
  )}
1631
1678
  </Row>
1679
+ {system.maxParticles !== undefined && system.renderMode !== RenderMode.Trail && (
1680
+ <Row
1681
+ label="Ring buffer"
1682
+ hint="What a full system does with new particles, as Unity's Ring Buffer Mode."
1683
+ >
1684
+ <SelectField
1685
+ value={system.ringBufferMode}
1686
+ options={[
1687
+ {value: 'disabled', label: 'Disabled'},
1688
+ {value: 'pauseUntilReplaced', label: 'Pause until replaced'},
1689
+ {value: 'loopUntilReplaced', label: 'Loop until replaced'},
1690
+ ]}
1691
+ onChange={(v) => binding.apply((s) => (s.ringBufferMode = v))}
1692
+ />
1693
+ </Row>
1694
+ )}
1695
+ {system.maxParticles !== undefined &&
1696
+ system.renderMode !== RenderMode.Trail &&
1697
+ system.ringBufferMode === 'loopUntilReplaced' && (
1698
+ <Row label="Loop range" hint="The part of its life a particle plays again, from 0 to 1.">
1699
+ <div style={{display: 'flex', gap: 6}}>
1700
+ {(['x', 'y'] as const).map((end) => (
1701
+ <NumberField
1702
+ key={end}
1703
+ value={system.ringBufferLoopRange[end]}
1704
+ min={0}
1705
+ step={0.05}
1706
+ onChange={(v) =>
1707
+ binding.apply((s) => {
1708
+ const next = s.ringBufferLoopRange.clone();
1709
+ next[end] = Math.min(1, Math.max(0, v));
1710
+ s.ringBufferLoopRange = new Vector2(
1711
+ Math.min(next.x, next.y),
1712
+ Math.max(next.x, next.y)
1713
+ );
1714
+ })
1715
+ }
1716
+ />
1717
+ ))}
1718
+ </div>
1719
+ </Row>
1720
+ )}
1632
1721
  {system.renderMode !== RenderMode.Mesh && system.renderMode !== RenderMode.Trail && (
1633
1722
  <Row label="Pivot">
1634
1723
  <div style={{display: 'flex', gap: 6}}>
@@ -1730,6 +1819,12 @@ export function RendererModule({
1730
1819
  <Row label="Apply fog">
1731
1820
  <CheckboxField value={system.applyFog} onChange={(v) => binding.apply((s) => (s.applyFog = v))} />
1732
1821
  </Row>
1822
+ <Row label="Image processing">
1823
+ <CheckboxField
1824
+ value={system.applyImageProcessing}
1825
+ onChange={(v) => binding.apply((s) => (s.applyImageProcessing = v))}
1826
+ />
1827
+ </Row>
1733
1828
  <Row label="Soft particles">
1734
1829
  <CheckboxField
1735
1830
  value={system.softParticles}