babylon.quarks-editor 0.26.0 → 0.28.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.
Files changed (211) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +69 -9
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +6 -1
  5. package/dist/core/assets.d.ts +7 -0
  6. package/dist/core/assets.js +11 -7
  7. package/dist/core/binding.d.ts +11 -2
  8. package/dist/core/binding.js +9 -1
  9. package/dist/core/collision.d.ts +4 -0
  10. package/dist/core/collision.js +4 -0
  11. package/dist/core/colors.d.ts +16 -0
  12. package/dist/core/colors.js +8 -0
  13. package/dist/core/defaultEffect.d.ts +5 -4
  14. package/dist/core/defaultEffect.js +4 -4
  15. package/dist/core/editorScene.d.ts +1 -1
  16. package/dist/core/editorSession.d.ts +229 -0
  17. package/dist/core/editorSession.js +983 -0
  18. package/dist/core/effectTree.d.ts +6 -1
  19. package/dist/core/effectTree.js +13 -88
  20. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  21. package/dist/core/emitterShapeWireframe.js +45 -13
  22. package/dist/core/galleryDropMarker.d.ts +2 -2
  23. package/dist/core/galleryDropMarker.js +6 -6
  24. package/dist/core/galleryLayout.d.ts +3 -3
  25. package/dist/core/galleryLayout.js +1 -1
  26. package/dist/core/galleryPlayback.d.ts +1 -1
  27. package/dist/core/galleryPlayback.js +2 -1
  28. package/dist/core/galleryScene.d.ts +4 -4
  29. package/dist/core/galleryScene.js +3 -3
  30. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  31. package/dist/core/gallerySelectionMarker.js +5 -5
  32. package/dist/core/groups.d.ts +1 -1
  33. package/dist/core/groups.js +1 -1
  34. package/dist/core/history.d.ts +7 -1
  35. package/dist/core/history.js +4 -0
  36. package/dist/core/indexedDbProjectStore.d.ts +14 -1
  37. package/dist/core/indexedDbProjectStore.js +13 -0
  38. package/dist/core/lineShaders.d.ts +2 -2
  39. package/dist/core/lineShaders.js +2 -2
  40. package/dist/core/loadEffect.d.ts +2 -2
  41. package/dist/core/loadEffect.js +1 -1
  42. package/dist/core/material.d.ts +12 -2
  43. package/dist/core/pickAssetFiles.js +1 -1
  44. package/dist/core/playback.d.ts +6 -0
  45. package/dist/core/projectStore.d.ts +26 -0
  46. package/dist/core/projectStore.js +1 -0
  47. package/dist/core/sceneEntries.d.ts +7 -4
  48. package/dist/core/sceneEntries.js +2 -2
  49. package/dist/core/scrub.d.ts +2 -2
  50. package/dist/core/scrub.js +2 -2
  51. package/dist/core/shapes.d.ts +25 -1
  52. package/dist/core/shapes.js +5 -0
  53. package/dist/core/systems.d.ts +4 -2
  54. package/dist/core/systems.js +3 -1
  55. package/dist/core/textureExport.d.ts +1 -1
  56. package/dist/core/timeline.d.ts +25 -5
  57. package/dist/core/transport.d.ts +3 -3
  58. package/dist/core/transport.js +2 -2
  59. package/dist/core/values.d.ts +21 -4
  60. package/dist/core/values.js +5 -0
  61. package/dist/core/viewportPicker.d.ts +4 -4
  62. package/dist/core/viewportPicker.js +8 -7
  63. package/dist/index.d.ts +25 -21
  64. package/dist/index.js +12 -12
  65. package/dist/react/AssetsPanel.d.ts +9 -2
  66. package/dist/react/AssetsPanel.js +6 -6
  67. package/dist/react/ChoiceDialog.d.ts +7 -0
  68. package/dist/react/ChoiceDialog.js +1 -1
  69. package/dist/react/ContextMenu.d.ts +10 -1
  70. package/dist/react/ContextMenu.js +1 -1
  71. package/dist/react/CurveEditor.d.ts +1 -1
  72. package/dist/react/CurveEditor.js +2 -2
  73. package/dist/react/EditorWorkspace.d.ts +9 -2
  74. package/dist/react/EditorWorkspace.js +10 -7
  75. package/dist/react/EffectEditor.d.ts +11 -5
  76. package/dist/react/EffectEditor.js +7 -7
  77. package/dist/react/EffectEditorHost.d.ts +33 -29
  78. package/dist/react/EffectEditorHost.js +247 -1674
  79. package/dist/react/EffectTreeView.d.ts +4 -1
  80. package/dist/react/EffectTreeView.js +3 -3
  81. package/dist/react/GradientEditor.d.ts +1 -1
  82. package/dist/react/GradientEditor.js +3 -3
  83. package/dist/react/InspectorInfoIcon.js +3 -3
  84. package/dist/react/InspectorLabel.js +3 -3
  85. package/dist/react/MessageDialog.d.ts +5 -0
  86. package/dist/react/MessageDialog.js +1 -1
  87. package/dist/react/ModuleSection.d.ts +10 -0
  88. package/dist/react/ModuleSection.js +8 -5
  89. package/dist/react/ObjectModule.d.ts +1 -1
  90. package/dist/react/ObjectModule.js +4 -4
  91. package/dist/react/ProjectsPage.d.ts +7 -1
  92. package/dist/react/ProjectsPage.js +33 -17
  93. package/dist/react/PromptDialog.d.ts +7 -0
  94. package/dist/react/PromptDialog.js +1 -1
  95. package/dist/react/ScenePanel.d.ts +9 -2
  96. package/dist/react/ScenePanel.js +12 -10
  97. package/dist/react/SubEmittersModule.d.ts +1 -1
  98. package/dist/react/SubEmittersModule.js +5 -5
  99. package/dist/react/TextureSelect.d.ts +24 -0
  100. package/dist/react/TextureSelect.js +74 -0
  101. package/dist/react/TextureUVField.d.ts +14 -0
  102. package/dist/react/TextureUVField.js +23 -0
  103. package/dist/react/TimelinePanel.d.ts +20 -7
  104. package/dist/react/TimelinePanel.js +21 -15
  105. package/dist/react/TimelineTrackRow.d.ts +13 -3
  106. package/dist/react/TimelineTrackRow.js +6 -6
  107. package/dist/react/TransportBar.d.ts +13 -4
  108. package/dist/react/TransportBar.js +3 -3
  109. package/dist/react/ValueField.d.ts +32 -5
  110. package/dist/react/ValueField.js +22 -11
  111. package/dist/react/behaviorModules.d.ts +19 -6
  112. package/dist/react/behaviorModules.js +68 -17
  113. package/dist/react/fields.d.ts +9 -0
  114. package/dist/react/fields.js +6 -3
  115. package/dist/react/host/BottomDocks.d.ts +36 -0
  116. package/dist/react/host/BottomDocks.js +63 -0
  117. package/dist/react/host/HostDialogs.d.ts +50 -0
  118. package/dist/react/host/HostDialogs.js +113 -0
  119. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  120. package/dist/react/host/InspectorSidebar.js +59 -0
  121. package/dist/react/host/SceneSidebar.d.ts +53 -0
  122. package/dist/react/host/SceneSidebar.js +62 -0
  123. package/dist/react/host/ViewportPane.d.ts +56 -0
  124. package/dist/react/host/ViewportPane.js +91 -0
  125. package/dist/react/host/hostHooks.d.ts +44 -0
  126. package/dist/react/host/hostHooks.js +164 -0
  127. package/dist/react/host/hostStyles.d.ts +21 -0
  128. package/dist/react/host/hostStyles.js +57 -0
  129. package/dist/react/index.d.ts +42 -38
  130. package/dist/react/index.js +25 -23
  131. package/dist/react/inspectorHints.js +31 -0
  132. package/dist/react/modules.d.ts +19 -6
  133. package/dist/react/modules.js +134 -117
  134. package/dist/react/theme.d.ts +15 -0
  135. package/dist/react/theme.js +15 -0
  136. package/dist/session.d.ts +9 -0
  137. package/dist/session.js +8 -0
  138. package/package.json +18 -11
  139. package/src/core/assetDragSession.ts +1 -1
  140. package/src/core/assetTree.ts +15 -2
  141. package/src/core/assets.ts +25 -10
  142. package/src/core/binding.ts +12 -3
  143. package/src/core/collision.ts +4 -0
  144. package/src/core/colors.ts +21 -1
  145. package/src/core/defaultEffect.ts +8 -7
  146. package/src/core/editorScene.ts +1 -1
  147. package/src/core/editorSession.ts +1299 -0
  148. package/src/core/effectTree.ts +18 -101
  149. package/src/core/emitterShapeWireframe.ts +48 -16
  150. package/src/core/galleryDropMarker.ts +8 -8
  151. package/src/core/galleryLayout.ts +4 -4
  152. package/src/core/galleryPlayback.ts +3 -2
  153. package/src/core/galleryScene.ts +5 -5
  154. package/src/core/gallerySelectionMarker.ts +7 -7
  155. package/src/core/groups.ts +1 -1
  156. package/src/core/history.ts +7 -1
  157. package/src/core/indexedDbProjectStore.ts +14 -1
  158. package/src/core/lineShaders.ts +2 -2
  159. package/src/core/loadEffect.ts +4 -4
  160. package/src/core/material.ts +14 -4
  161. package/src/core/pickAssetFiles.ts +2 -2
  162. package/src/core/playback.ts +6 -0
  163. package/src/core/projectStore.ts +26 -0
  164. package/src/core/sceneEntries.ts +8 -5
  165. package/src/core/scrub.ts +3 -3
  166. package/src/core/shapes.ts +29 -2
  167. package/src/core/systems.ts +11 -4
  168. package/src/core/textureExport.ts +1 -1
  169. package/src/core/timeline.ts +43 -18
  170. package/src/core/transport.ts +4 -4
  171. package/src/core/values.ts +20 -4
  172. package/src/core/viewportPicker.ts +12 -11
  173. package/src/index.ts +25 -21
  174. package/src/react/AssetsPanel.tsx +13 -6
  175. package/src/react/ChoiceDialog.tsx +8 -1
  176. package/src/react/ContextMenu.tsx +11 -2
  177. package/src/react/CurveEditor.tsx +2 -2
  178. package/src/react/EditorWorkspace.tsx +16 -10
  179. package/src/react/EffectEditor.tsx +17 -9
  180. package/src/react/EffectEditorHost.tsx +415 -2354
  181. package/src/react/EffectTreeView.tsx +7 -4
  182. package/src/react/GradientEditor.tsx +3 -3
  183. package/src/react/InspectorInfoIcon.tsx +3 -3
  184. package/src/react/InspectorLabel.tsx +3 -3
  185. package/src/react/MessageDialog.tsx +6 -1
  186. package/src/react/ModuleSection.tsx +15 -4
  187. package/src/react/ObjectModule.tsx +5 -5
  188. package/src/react/ProjectsPage.tsx +44 -17
  189. package/src/react/PromptDialog.tsx +8 -1
  190. package/src/react/ScenePanel.tsx +21 -12
  191. package/src/react/SubEmittersModule.tsx +6 -6
  192. package/src/react/TextureSelect.tsx +118 -0
  193. package/src/react/TextureUVField.tsx +62 -0
  194. package/src/react/TimelinePanel.tsx +40 -21
  195. package/src/react/TimelineTrackRow.tsx +19 -9
  196. package/src/react/TransportBar.tsx +16 -7
  197. package/src/react/ValueField.tsx +60 -16
  198. package/src/react/behaviorModules.tsx +240 -55
  199. package/src/react/fields.tsx +22 -4
  200. package/src/react/host/BottomDocks.tsx +178 -0
  201. package/src/react/host/HostDialogs.tsx +228 -0
  202. package/src/react/host/InspectorSidebar.tsx +147 -0
  203. package/src/react/host/SceneSidebar.tsx +227 -0
  204. package/src/react/host/ViewportPane.tsx +300 -0
  205. package/src/react/host/hostHooks.ts +198 -0
  206. package/src/react/host/hostStyles.ts +65 -0
  207. package/src/react/index.ts +43 -38
  208. package/src/react/inspectorHints.ts +40 -0
  209. package/src/react/modules.tsx +440 -170
  210. package/src/react/theme.ts +15 -0
  211. package/src/session.ts +17 -0
@@ -12,8 +12,8 @@ import {
12
12
  EulerGenerator,
13
13
  ForceOverLife,
14
14
  FrameOverLife,
15
- getPhysicsResolver,
16
15
  Gradient,
16
+ GravityForce,
17
17
  InheritVelocity,
18
18
  IntervalValue,
19
19
  LimitSpeedOverLife,
@@ -33,21 +33,17 @@ import {
33
33
  WidthOverLength,
34
34
  } from 'babylon.quarks';
35
35
  import React from 'react';
36
- import {EffectBinding} from '../core/binding';
37
- import {buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops} from '../core/colors';
38
- import {buildCurve, buildScalar, readPieces, readScalar} from '../core/values';
39
- import {CurveEditor} from './CurveEditor';
40
- import {CheckboxField, NumberField, Row, SelectField} from './fields';
41
- import {GradientEditor} from './GradientEditor';
42
- import {iconStyle} from './icons';
43
- import type {TextureOption} from './modules';
44
- import {ModuleSection} from './ModuleSection';
45
- import {theme} from './theme';
46
- import {ValueField} from './ValueField';
47
-
48
- interface ModuleProps {
49
- binding: EffectBinding;
50
- }
36
+ import {EffectBinding} from '../core/binding.js';
37
+ 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';
40
+ import {CheckboxField, NumberField, Row, SelectField} from './fields.js';
41
+ import {GradientEditor} from './GradientEditor.js';
42
+ import {iconStyle} from './icons.js';
43
+ import type {TextureOption} from './modules.js';
44
+ import {ModuleSection, type ModuleProps} from './ModuleSection.js';
45
+ import {theme} from './theme.js';
46
+ import {ValueField} from './ValueField.js';
51
47
 
52
48
  function removeBehaviorOfType(behaviors: Behavior[], type: string): void {
53
49
  for (let i = behaviors.length - 1; i >= 0; i--) {
@@ -85,6 +81,7 @@ function BehaviorModule<T extends Behavior>(props: {
85
81
  );
86
82
  }
87
83
 
84
+ /** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
88
85
  export function SpeedOverLifeModule({binding}: ModuleProps) {
89
86
  return (
90
87
  <BehaviorModule<SpeedOverLife>
@@ -92,9 +89,7 @@ export function SpeedOverLifeModule({binding}: ModuleProps) {
92
89
  title="Speed over Lifetime"
93
90
  type="SpeedOverLife"
94
91
  create={() =>
95
- new SpeedOverLife(
96
- buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0]}) as never
97
- )
92
+ new SpeedOverLife(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0]}))
98
93
  }
99
94
  >
100
95
  {(behavior) => (
@@ -102,7 +97,7 @@ export function SpeedOverLifeModule({binding}: ModuleProps) {
102
97
  <CurveEditor
103
98
  pieces={readPieces(behavior.speed)}
104
99
  maxValue={2}
105
- onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces) as never))}
100
+ onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces)))}
106
101
  />
107
102
  </div>
108
103
  )}
@@ -110,6 +105,7 @@ export function SpeedOverLifeModule({binding}: ModuleProps) {
110
105
  );
111
106
  }
112
107
 
108
+ /** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
113
109
  export function LimitSpeedOverLifeModule({binding}: ModuleProps) {
114
110
  return (
115
111
  <BehaviorModule<LimitSpeedOverLife>
@@ -117,10 +113,7 @@ export function LimitSpeedOverLifeModule({binding}: ModuleProps) {
117
113
  title="Limit Speed over Lifetime"
118
114
  type="LimitSpeedOverLife"
119
115
  create={() =>
120
- new LimitSpeedOverLife(
121
- buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1]}) as never,
122
- 0.5
123
- )
116
+ new LimitSpeedOverLife(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1]}), 0.5)
124
117
  }
125
118
  >
126
119
  {(behavior) => (
@@ -129,7 +122,7 @@ export function LimitSpeedOverLifeModule({binding}: ModuleProps) {
129
122
  <CurveEditor
130
123
  pieces={readPieces(behavior.speed)}
131
124
  maxValue={8}
132
- onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces) as never))}
125
+ onChange={(pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces)))}
133
126
  />
134
127
  </div>
135
128
  <Row label="Dampen">
@@ -146,6 +139,7 @@ export function LimitSpeedOverLifeModule({binding}: ModuleProps) {
146
139
  );
147
140
  }
148
141
 
142
+ /** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
149
143
  export function ForceOverLifeModule({binding}: ModuleProps) {
150
144
  return (
151
145
  <BehaviorModule<ForceOverLife>
@@ -156,6 +150,16 @@ export function ForceOverLifeModule({binding}: ModuleProps) {
156
150
  >
157
151
  {(behavior) => (
158
152
  <>
153
+ <Row label="Space">
154
+ <SelectField
155
+ value={behavior.space}
156
+ options={[
157
+ {value: 'world', label: 'World'},
158
+ {value: 'local', label: 'Local'},
159
+ ]}
160
+ onChange={(v) => binding.apply(() => (behavior.space = v))}
161
+ />
162
+ </Row>
159
163
  <ValueField
160
164
  hintKey="force.x"
161
165
  label="X"
@@ -288,6 +292,43 @@ export function InheritVelocityModule({binding}: ModuleProps) {
288
292
  );
289
293
  }
290
294
 
295
+ /** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
296
+ export function AttractorModule({binding}: ModuleProps) {
297
+ return (
298
+ <BehaviorModule<GravityForce>
299
+ binding={binding}
300
+ title="Attractor"
301
+ type="GravityForce"
302
+ create={() => new GravityForce(new Vector3(0, 0, 0), 1)}
303
+ >
304
+ {(behavior) => (
305
+ <>
306
+ <Row label="Center" hintKey="attractor.center">
307
+ <div style={{display: 'flex', gap: 6}}>
308
+ {(['x', 'y', 'z'] as const).map((axis) => (
309
+ <NumberField
310
+ key={axis}
311
+ value={behavior.center[axis]}
312
+ step={0.1}
313
+ onChange={(v) => binding.apply(() => (behavior.center[axis] = v))}
314
+ />
315
+ ))}
316
+ </div>
317
+ </Row>
318
+ <Row label="Strength" hintKey="attractor.magnitude">
319
+ <NumberField
320
+ value={behavior.magnitude}
321
+ step={0.1}
322
+ onChange={(v) => binding.apply(() => (behavior.magnitude = v))}
323
+ />
324
+ </Row>
325
+ </>
326
+ )}
327
+ </BehaviorModule>
328
+ );
329
+ }
330
+
331
+ /** The Gravity module: pulls particles towards a point. */
291
332
  export function GravityModule({binding}: ModuleProps) {
292
333
  return (
293
334
  <BehaviorModule<ApplyForce>
@@ -297,18 +338,31 @@ export function GravityModule({binding}: ModuleProps) {
297
338
  create={() => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81))}
298
339
  >
299
340
  {(behavior) => (
300
- <Row label="Strength" hintKey="gravity.strength">
301
- <NumberField
302
- value={readScalar(behavior.magnitude).value}
303
- step={0.5}
304
- onChange={(v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v)))}
305
- />
306
- </Row>
341
+ <>
342
+ <Row label="Strength" hintKey="gravity.strength">
343
+ <NumberField
344
+ value={readScalar(behavior.magnitude).value}
345
+ step={0.5}
346
+ onChange={(v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v)))}
347
+ />
348
+ </Row>
349
+ <Row label="Space">
350
+ <SelectField
351
+ value={behavior.space}
352
+ options={[
353
+ {value: 'world', label: 'World'},
354
+ {value: 'local', label: 'Local'},
355
+ ]}
356
+ onChange={(v) => binding.apply(() => (behavior.space = v))}
357
+ />
358
+ </Row>
359
+ </>
307
360
  )}
308
361
  </BehaviorModule>
309
362
  );
310
363
  }
311
364
 
365
+ /** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
312
366
  export function RotationOverLifeModule({binding}: ModuleProps) {
313
367
  return (
314
368
  <BehaviorModule<RotationOverLife>
@@ -595,6 +649,7 @@ export function SequencesModule({binding, textureOptions}: ModuleProps & {textur
595
649
  );
596
650
  }
597
651
 
652
+ /** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
598
653
  export function NoiseModule({binding}: ModuleProps) {
599
654
  return (
600
655
  <BehaviorModule<Noise>
@@ -695,6 +750,7 @@ function Vector3Row(props: {
695
750
  );
696
751
  }
697
752
 
753
+ /** The Turbulence module: a curl noise force field. */
698
754
  export function TurbulenceModule({binding}: ModuleProps) {
699
755
  return (
700
756
  <BehaviorModule<TurbulenceField>
@@ -735,12 +791,36 @@ export function TurbulenceModule({binding}: ModuleProps) {
735
791
  step={0.1}
736
792
  onChange={(v) => binding.apply(() => (behavior.timeScale = v))}
737
793
  />
794
+ {/* The field is built from the seed, so a different seed is a different swirl. */}
795
+ <Row label="Seed">
796
+ <NumberField
797
+ value={behavior.seed}
798
+ step={1}
799
+ onChange={(v) =>
800
+ binding.apply(() => {
801
+ const seed = Math.round(v);
802
+ const next = new TurbulenceField(
803
+ behavior.scale,
804
+ behavior.octaves,
805
+ behavior.velocityMultiplier,
806
+ behavior.timeScale,
807
+ seed
808
+ );
809
+ behavior.seed = seed;
810
+ behavior.generator = next.generator;
811
+ behavior.initialOffset.copy(next.initialOffset);
812
+ behavior.timeOffset.copy(next.initialOffset);
813
+ })
814
+ }
815
+ />
816
+ </Row>
738
817
  </>
739
818
  )}
740
819
  </BehaviorModule>
741
820
  );
742
821
  }
743
822
 
823
+ /** The Randomize Direction module: tilts each particle's start direction by a random angle. */
744
824
  export function EmitDirectionModule({binding}: ModuleProps) {
745
825
  return (
746
826
  <BehaviorModule<ChangeEmitDirection>
@@ -752,25 +832,26 @@ export function EmitDirectionModule({binding}: ModuleProps) {
752
832
  {(behavior) => (
753
833
  <ValueField
754
834
  label="Angle (rad)"
755
- generator={behavior.angle as never}
835
+ generator={behavior.angle}
836
+ curve={false}
756
837
  min={0}
757
- curveMax={Math.PI}
758
- onChange={(g) => binding.apply(() => (behavior.angle = g as never))}
838
+ onChange={(g) => binding.apply(() => (behavior.angle = g))}
759
839
  />
760
840
  )}
761
841
  </BehaviorModule>
762
842
  );
763
843
  }
764
844
 
845
+ /** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
765
846
  export function CollisionModule({binding}: ModuleProps) {
766
847
  // Like Unity: Collision stores bounce only. Colliders are host-provided via
767
- // setPhysicsResolver — the editor never invents a ground plane.
848
+ // setPhysicsResolver, asked when a particle is tested — the editor never invents a ground plane.
768
849
  return (
769
850
  <BehaviorModule<ApplyCollision>
770
851
  binding={binding}
771
852
  title="Collision"
772
853
  type="ApplyCollision"
773
- create={() => new ApplyCollision(getPhysicsResolver(), 0.5)}
854
+ create={() => new ApplyCollision(undefined, 0.5)}
774
855
  >
775
856
  {(behavior) => (
776
857
  <>
@@ -816,6 +897,7 @@ function SpeedRangeRows({binding, behavior}: {binding: EffectBinding; behavior:
816
897
  );
817
898
  }
818
899
 
900
+ /** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
819
901
  export function ColorBySpeedModule({binding}: ModuleProps) {
820
902
  return (
821
903
  <BehaviorModule<ColorBySpeed>
@@ -843,6 +925,7 @@ export function ColorBySpeedModule({binding}: ModuleProps) {
843
925
  );
844
926
  }
845
927
 
928
+ /** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
846
929
  export function SizeBySpeedModule({binding}: ModuleProps) {
847
930
  return (
848
931
  <BehaviorModule<SizeBySpeed>
@@ -851,7 +934,7 @@ export function SizeBySpeedModule({binding}: ModuleProps) {
851
934
  type="SizeBySpeed"
852
935
  create={() =>
853
936
  new SizeBySpeed(
854
- buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [0.5, 0.8, 1.2, 1.5]}) as never,
937
+ buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [0.5, 0.8, 1.2, 1.5]}),
855
938
  new IntervalValue(0, 5)
856
939
  )
857
940
  }
@@ -860,9 +943,9 @@ export function SizeBySpeedModule({binding}: ModuleProps) {
860
943
  <>
861
944
  <div style={{marginTop: 6}}>
862
945
  <CurveEditor
863
- pieces={readPieces(behavior.size as never)}
946
+ pieces={readPieces(behavior.size)}
864
947
  maxValue={2}
865
- onChange={(pieces) => binding.apply(() => (behavior.size = buildCurve(pieces) as never))}
948
+ onChange={(pieces) => binding.apply(() => (behavior.size = buildCurve(pieces)))}
866
949
  />
867
950
  </div>
868
951
  <SpeedRangeRows binding={binding} behavior={behavior} />
@@ -872,6 +955,7 @@ export function SizeBySpeedModule({binding}: ModuleProps) {
872
955
  );
873
956
  }
874
957
 
958
+ /** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
875
959
  export function RotationBySpeedModule({binding}: ModuleProps) {
876
960
  return (
877
961
  <BehaviorModule<RotationBySpeed>
@@ -896,6 +980,7 @@ export function RotationBySpeedModule({binding}: ModuleProps) {
896
980
  );
897
981
  }
898
982
 
983
+ /** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
899
984
  export function OrbitOverLifeModule({binding}: ModuleProps) {
900
985
  return (
901
986
  <BehaviorModule<OrbitOverLife>
@@ -916,6 +1001,7 @@ export function OrbitOverLifeModule({binding}: ModuleProps) {
916
1001
  );
917
1002
  }
918
1003
 
1004
+ /** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
919
1005
  export function WidthOverTrailModule({binding}: ModuleProps) {
920
1006
  return (
921
1007
  <BehaviorModule<WidthOverLength>
@@ -923,18 +1009,16 @@ export function WidthOverTrailModule({binding}: ModuleProps) {
923
1009
  title="Width over Trail"
924
1010
  type="WidthOverLength"
925
1011
  create={() =>
926
- new WidthOverLength(
927
- buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0]}) as never
928
- )
1012
+ new WidthOverLength(buildScalar({mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0]}))
929
1013
  }
930
1014
  >
931
1015
  {(behavior) => (
932
1016
  <>
933
1017
  <div style={{marginTop: 6}}>
934
1018
  <CurveEditor
935
- pieces={readPieces(behavior.width as never)}
1019
+ pieces={readPieces(behavior.width)}
936
1020
  maxValue={2}
937
- onChange={(pieces) => binding.apply(() => (behavior.width = buildCurve(pieces) as never))}
1021
+ onChange={(pieces) => binding.apply(() => (behavior.width = buildCurve(pieces)))}
938
1022
  />
939
1023
  </div>
940
1024
  {/* Off: the curve multiplies the particle size. On: it is the width in world units (files from 0.21 and earlier). */}
@@ -950,6 +1034,13 @@ export function WidthOverTrailModule({binding}: ModuleProps) {
950
1034
  );
951
1035
  }
952
1036
 
1037
+ /** How many of the first frames a start-frame generator picks from: its exclusive upper bound. */
1038
+ function startFrameCount(startFrame: FrameOverLife['startFrame']): number {
1039
+ const state = readScalar(startFrame);
1040
+ return Math.round(state.mode === 'random' ? state.max : state.value);
1041
+ }
1042
+
1043
+ /** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
953
1044
  export function TextureSheetModule({binding}: ModuleProps) {
954
1045
  const system = binding.system;
955
1046
  const frameBehavior = findBehavior<FrameOverLife>(system.behaviors, 'FrameOverLife');
@@ -958,7 +1049,7 @@ export function TextureSheetModule({binding}: ModuleProps) {
958
1049
  const u = Math.max(1, system.uTileCount);
959
1050
  const v = Math.max(1, system.vTileCount);
960
1051
  // Start tile → cell (row-major, top-left origin — matches the shader's tile UV mapping).
961
- const startIdx = Math.min(Math.max(0, Math.round(readScalar(system.startTileIndex as never).value)), tiles - 1);
1052
+ const startIdx = Math.min(Math.max(0, Math.round(readScalar(system.startTileIndex).value)), tiles - 1);
962
1053
  const startCol = startIdx % u;
963
1054
  const startRow = Math.floor(startIdx / u);
964
1055
  return (
@@ -1047,7 +1138,7 @@ export function TextureSheetModule({binding}: ModuleProps) {
1047
1138
  </Row>
1048
1139
  <Row label="Start tile">
1049
1140
  <NumberField
1050
- value={readScalar(system.startTileIndex as never).value}
1141
+ value={readScalar(system.startTileIndex).value}
1051
1142
  min={0}
1052
1143
  step={1}
1053
1144
  onChange={(v) => binding.apply((s) => (s.startTileIndex = new ConstantValue(Math.round(v))))}
@@ -1071,7 +1162,7 @@ export function TextureSheetModule({binding}: ModuleProps) {
1071
1162
  min: 0,
1072
1163
  max: tiles - 1,
1073
1164
  curve: [0, (tiles - 1) / 3, (2 * (tiles - 1)) / 3, tiles - 1],
1074
- }) as never
1165
+ })
1075
1166
  )
1076
1167
  );
1077
1168
  }
@@ -1080,13 +1171,107 @@ export function TextureSheetModule({binding}: ModuleProps) {
1080
1171
  />
1081
1172
  </Row>
1082
1173
  {frameBehavior && (
1083
- <div style={{marginTop: 6}}>
1084
- <CurveEditor
1085
- pieces={readPieces(frameBehavior.frame)}
1086
- maxValue={Math.max(1, tiles - 1)}
1087
- onChange={(pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces) as never))}
1088
- />
1089
- </div>
1174
+ <>
1175
+ <Row label="Time mode">
1176
+ <SelectField<'life' | 'fps'>
1177
+ value={frameBehavior.fps > 0 ? 'fps' : 'life'}
1178
+ options={[
1179
+ {value: 'life', label: 'Lifetime (curve over life)'},
1180
+ {value: 'fps', label: 'FPS (frame from curve, then by age)'},
1181
+ ]}
1182
+ onChange={(mode) =>
1183
+ binding.apply(() => {
1184
+ frameBehavior.fps = mode === 'fps' ? 12 : 0;
1185
+ if (mode === 'fps' && frameBehavior.frameCount <= 0) {
1186
+ frameBehavior.frameCount = tiles;
1187
+ }
1188
+ })
1189
+ }
1190
+ />
1191
+ </Row>
1192
+ {frameBehavior.fps > 0 ? (
1193
+ <>
1194
+ <Row label="FPS">
1195
+ <NumberField
1196
+ value={frameBehavior.fps}
1197
+ min={0.01}
1198
+ step={1}
1199
+ onChange={(v) => binding.apply(() => (frameBehavior.fps = v))}
1200
+ />
1201
+ </Row>
1202
+ <Row label="Loop">
1203
+ <CheckboxField
1204
+ value={frameBehavior.loop}
1205
+ onChange={(v) => binding.apply(() => (frameBehavior.loop = v))}
1206
+ />
1207
+ </Row>
1208
+ <Row label="Random frame">
1209
+ <CheckboxField
1210
+ value={frameBehavior.random}
1211
+ onChange={(v) => binding.apply(() => (frameBehavior.random = v))}
1212
+ />
1213
+ </Row>
1214
+ </>
1215
+ ) : (
1216
+ <Row label="Cycles">
1217
+ <NumberField
1218
+ value={frameBehavior.cycles}
1219
+ min={0.01}
1220
+ step={1}
1221
+ onChange={(v) => binding.apply(() => (frameBehavior.cycles = v))}
1222
+ />
1223
+ </Row>
1224
+ )}
1225
+ <Row label="Frame count">
1226
+ <NumberField
1227
+ value={frameBehavior.frameCount}
1228
+ min={frameBehavior.fps > 0 || frameBehavior.startFrame ? 1 : 0}
1229
+ max={tiles}
1230
+ step={1}
1231
+ onChange={(v) =>
1232
+ binding.apply(() => (frameBehavior.frameCount = Math.max(0, Math.round(v))))
1233
+ }
1234
+ />
1235
+ </Row>
1236
+ <Row label="Start frame">
1237
+ <CheckboxField
1238
+ value={!!frameBehavior.startFrame}
1239
+ onChange={(v) =>
1240
+ binding.apply(() => {
1241
+ frameBehavior.startFrame = v ? new IntervalValue(0, tiles) : undefined;
1242
+ // The offset only wraps into a segment; without one it runs off the sheet.
1243
+ if (v && frameBehavior.frameCount <= 0) {
1244
+ frameBehavior.frameCount = tiles;
1245
+ }
1246
+ })
1247
+ }
1248
+ />
1249
+ {frameBehavior.startFrame && (
1250
+ <NumberField
1251
+ value={startFrameCount(frameBehavior.startFrame)}
1252
+ min={1}
1253
+ max={tiles}
1254
+ step={1}
1255
+ onChange={(v) =>
1256
+ binding.apply(
1257
+ () =>
1258
+ (frameBehavior.startFrame = new IntervalValue(
1259
+ 0,
1260
+ Math.max(1, Math.round(v))
1261
+ ))
1262
+ )
1263
+ }
1264
+ />
1265
+ )}
1266
+ </Row>
1267
+ <div style={{marginTop: 6}}>
1268
+ <CurveEditor
1269
+ pieces={readPieces(frameBehavior.frame)}
1270
+ maxValue={Math.max(1, tiles - 1)}
1271
+ onChange={(pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces)))}
1272
+ />
1273
+ </div>
1274
+ </>
1090
1275
  )}
1091
1276
  </ModuleSection>
1092
1277
  );
@@ -1,7 +1,8 @@
1
1
  import React, {useEffect, useState} from 'react';
2
- import {InspectorLabel} from './InspectorLabel';
3
- import {inputStyle, rowStyle} from './theme';
2
+ import {InspectorLabel} from './InspectorLabel.js';
3
+ import {inputStyle, rowStyle} from './theme.js';
4
4
 
5
+ /** An inspector row: a label, with an optional hint, and its controls. */
5
6
  export function Row(props: {label: string; hint?: string; hintKey?: string; children: React.ReactNode}) {
6
7
  return (
7
8
  <div style={rowStyle}>
@@ -12,7 +13,13 @@ export function Row(props: {label: string; hint?: string; hintKey?: string; chil
12
13
  }
13
14
 
14
15
  /** Number input that keeps focus-friendly local text state and commits parsed values. */
15
- export function NumberField(props: {value: number; onChange: (value: number) => void; step?: number; min?: number}) {
16
+ export function NumberField(props: {
17
+ value: number;
18
+ onChange: (value: number) => void;
19
+ step?: number;
20
+ min?: number;
21
+ max?: number;
22
+ }) {
16
23
  const [text, setText] = useState(String(round(props.value)));
17
24
  const focusedRef = React.useRef(false);
18
25
  useEffect(() => {
@@ -28,6 +35,7 @@ export function NumberField(props: {value: number; onChange: (value: number) =>
28
35
  value={text}
29
36
  step={props.step ?? 0.1}
30
37
  min={props.min}
38
+ max={props.max}
31
39
  onFocus={() => {
32
40
  focusedRef.current = true;
33
41
  }}
@@ -46,6 +54,7 @@ export function NumberField(props: {value: number; onChange: (value: number) =>
46
54
  );
47
55
  }
48
56
 
57
+ /** A checkbox bound to a boolean. */
49
58
  export function CheckboxField(props: {value: boolean; onChange: (value: boolean) => void}) {
50
59
  return (
51
60
  <input
@@ -58,9 +67,18 @@ export function CheckboxField(props: {value: boolean; onChange: (value: boolean)
58
67
  );
59
68
  }
60
69
 
70
+ /** A select bound to a value, from a list of options. */
61
71
  export function SelectField<T extends string | number>(props: {
72
+ /** The selected value. */
62
73
  value: T;
63
- options: Array<{value: T; label: string}>;
74
+ /** The options, in menu order. */
75
+ options: Array<{
76
+ /** The option's value. */
77
+ value: T;
78
+ /** Its text. */
79
+ label: string;
80
+ }>;
81
+ /** Receives the picked value, a number when the values are numbers. */
64
82
  onChange: (value: T) => void;
65
83
  }) {
66
84
  const numeric = typeof props.value === 'number';