babylon.quarks-editor 0.26.0 → 0.27.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 (151) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +38 -5
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +1 -1
  5. package/dist/core/binding.d.ts +2 -2
  6. package/dist/core/binding.js +1 -1
  7. package/dist/core/defaultEffect.d.ts +2 -2
  8. package/dist/core/defaultEffect.js +3 -3
  9. package/dist/core/editorScene.d.ts +1 -1
  10. package/dist/core/effectTree.d.ts +1 -1
  11. package/dist/core/effectTree.js +13 -88
  12. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  13. package/dist/core/emitterShapeWireframe.js +45 -13
  14. package/dist/core/galleryDropMarker.d.ts +2 -2
  15. package/dist/core/galleryDropMarker.js +6 -6
  16. package/dist/core/galleryLayout.d.ts +3 -3
  17. package/dist/core/galleryLayout.js +1 -1
  18. package/dist/core/galleryPlayback.d.ts +1 -1
  19. package/dist/core/galleryScene.d.ts +4 -4
  20. package/dist/core/galleryScene.js +3 -3
  21. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  22. package/dist/core/gallerySelectionMarker.js +5 -5
  23. package/dist/core/groups.d.ts +1 -1
  24. package/dist/core/groups.js +1 -1
  25. package/dist/core/history.d.ts +1 -1
  26. package/dist/core/indexedDbProjectStore.d.ts +1 -1
  27. package/dist/core/lineShaders.d.ts +2 -2
  28. package/dist/core/lineShaders.js +2 -2
  29. package/dist/core/loadEffect.d.ts +2 -2
  30. package/dist/core/loadEffect.js +1 -1
  31. package/dist/core/pickAssetFiles.js +1 -1
  32. package/dist/core/sceneEntries.d.ts +4 -4
  33. package/dist/core/sceneEntries.js +2 -2
  34. package/dist/core/scrub.d.ts +1 -1
  35. package/dist/core/shapes.d.ts +1 -1
  36. package/dist/core/systems.d.ts +2 -2
  37. package/dist/core/textureExport.d.ts +1 -1
  38. package/dist/core/timeline.d.ts +2 -2
  39. package/dist/core/transport.d.ts +3 -3
  40. package/dist/core/transport.js +2 -2
  41. package/dist/core/viewportPicker.d.ts +4 -4
  42. package/dist/core/viewportPicker.js +8 -7
  43. package/dist/index.d.ts +21 -21
  44. package/dist/index.js +12 -12
  45. package/dist/react/AssetsPanel.d.ts +2 -2
  46. package/dist/react/AssetsPanel.js +6 -6
  47. package/dist/react/ChoiceDialog.js +1 -1
  48. package/dist/react/ContextMenu.js +1 -1
  49. package/dist/react/CurveEditor.d.ts +1 -1
  50. package/dist/react/CurveEditor.js +2 -2
  51. package/dist/react/EditorWorkspace.d.ts +2 -2
  52. package/dist/react/EditorWorkspace.js +4 -4
  53. package/dist/react/EffectEditor.d.ts +3 -3
  54. package/dist/react/EffectEditor.js +7 -7
  55. package/dist/react/EffectEditorHost.d.ts +6 -6
  56. package/dist/react/EffectEditorHost.js +59 -62
  57. package/dist/react/EffectTreeView.d.ts +1 -1
  58. package/dist/react/EffectTreeView.js +3 -3
  59. package/dist/react/GradientEditor.d.ts +1 -1
  60. package/dist/react/GradientEditor.js +3 -3
  61. package/dist/react/InspectorInfoIcon.js +3 -3
  62. package/dist/react/InspectorLabel.js +3 -3
  63. package/dist/react/MessageDialog.js +1 -1
  64. package/dist/react/ModuleSection.js +4 -4
  65. package/dist/react/ObjectModule.d.ts +1 -1
  66. package/dist/react/ObjectModule.js +4 -4
  67. package/dist/react/ProjectsPage.d.ts +1 -1
  68. package/dist/react/ProjectsPage.js +6 -6
  69. package/dist/react/PromptDialog.js +1 -1
  70. package/dist/react/ScenePanel.d.ts +2 -2
  71. package/dist/react/ScenePanel.js +5 -5
  72. package/dist/react/SubEmittersModule.d.ts +1 -1
  73. package/dist/react/SubEmittersModule.js +5 -5
  74. package/dist/react/TextureSelect.d.ts +24 -0
  75. package/dist/react/TextureSelect.js +74 -0
  76. package/dist/react/TextureUVField.d.ts +14 -0
  77. package/dist/react/TextureUVField.js +23 -0
  78. package/dist/react/TimelinePanel.d.ts +7 -7
  79. package/dist/react/TimelinePanel.js +11 -9
  80. package/dist/react/TimelineTrackRow.d.ts +3 -3
  81. package/dist/react/TimelineTrackRow.js +6 -6
  82. package/dist/react/TransportBar.d.ts +4 -4
  83. package/dist/react/TransportBar.js +3 -3
  84. package/dist/react/ValueField.d.ts +1 -1
  85. package/dist/react/ValueField.js +3 -3
  86. package/dist/react/behaviorModules.d.ts +4 -2
  87. package/dist/react/behaviorModules.js +52 -16
  88. package/dist/react/fields.d.ts +1 -0
  89. package/dist/react/fields.js +3 -3
  90. package/dist/react/index.d.ts +40 -38
  91. package/dist/react/index.js +25 -23
  92. package/dist/react/inspectorHints.js +31 -0
  93. package/dist/react/modules.d.ts +3 -1
  94. package/dist/react/modules.js +99 -100
  95. package/package.json +3 -2
  96. package/src/core/assetDragSession.ts +1 -1
  97. package/src/core/assetTree.ts +1 -1
  98. package/src/core/binding.ts +3 -3
  99. package/src/core/defaultEffect.ts +4 -4
  100. package/src/core/editorScene.ts +1 -1
  101. package/src/core/effectTree.ts +13 -101
  102. package/src/core/emitterShapeWireframe.ts +48 -16
  103. package/src/core/galleryDropMarker.ts +8 -8
  104. package/src/core/galleryLayout.ts +4 -4
  105. package/src/core/galleryPlayback.ts +1 -1
  106. package/src/core/galleryScene.ts +5 -5
  107. package/src/core/gallerySelectionMarker.ts +7 -7
  108. package/src/core/groups.ts +1 -1
  109. package/src/core/history.ts +1 -1
  110. package/src/core/indexedDbProjectStore.ts +1 -1
  111. package/src/core/lineShaders.ts +2 -2
  112. package/src/core/loadEffect.ts +3 -3
  113. package/src/core/pickAssetFiles.ts +2 -2
  114. package/src/core/sceneEntries.ts +5 -5
  115. package/src/core/scrub.ts +1 -1
  116. package/src/core/shapes.ts +1 -1
  117. package/src/core/systems.ts +2 -2
  118. package/src/core/textureExport.ts +1 -1
  119. package/src/core/timeline.ts +3 -3
  120. package/src/core/transport.ts +4 -4
  121. package/src/core/viewportPicker.ts +12 -11
  122. package/src/index.ts +21 -21
  123. package/src/react/AssetsPanel.tsx +6 -6
  124. package/src/react/ChoiceDialog.tsx +1 -1
  125. package/src/react/ContextMenu.tsx +1 -1
  126. package/src/react/CurveEditor.tsx +2 -2
  127. package/src/react/EditorWorkspace.tsx +5 -5
  128. package/src/react/EffectEditor.tsx +10 -8
  129. package/src/react/EffectEditorHost.tsx +64 -69
  130. package/src/react/EffectTreeView.tsx +4 -4
  131. package/src/react/GradientEditor.tsx +3 -3
  132. package/src/react/InspectorInfoIcon.tsx +3 -3
  133. package/src/react/InspectorLabel.tsx +3 -3
  134. package/src/react/MessageDialog.tsx +1 -1
  135. package/src/react/ModuleSection.tsx +4 -4
  136. package/src/react/ObjectModule.tsx +5 -5
  137. package/src/react/ProjectsPage.tsx +6 -6
  138. package/src/react/PromptDialog.tsx +1 -1
  139. package/src/react/ScenePanel.tsx +7 -7
  140. package/src/react/SubEmittersModule.tsx +6 -6
  141. package/src/react/TextureSelect.tsx +118 -0
  142. package/src/react/TextureUVField.tsx +62 -0
  143. package/src/react/TimelinePanel.tsx +17 -15
  144. package/src/react/TimelineTrackRow.tsx +9 -9
  145. package/src/react/TransportBar.tsx +7 -7
  146. package/src/react/ValueField.tsx +3 -3
  147. package/src/react/behaviorModules.tsx +211 -28
  148. package/src/react/fields.tsx +10 -3
  149. package/src/react/index.ts +41 -38
  150. package/src/react/inspectorHints.ts +40 -0
  151. package/src/react/modules.tsx +375 -131
@@ -1,10 +1,10 @@
1
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
2
- import type {Scene} from '@babylonjs/core/scene';
1
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
2
+ import type {Scene} from '@babylonjs/core/scene.js';
3
3
  import {ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon, RectangleGroupIcon} from '@heroicons/react/24/solid';
4
4
  import type {BatchedRenderer, ParticleSystem} from 'babylon.quarks';
5
5
  import React, {useMemo, useRef, useState} from 'react';
6
- import type {EffectBinding} from '../core/binding';
7
- import {groupNodes} from '../core/groups';
6
+ import type {EffectBinding} from '../core/binding.js';
7
+ import {groupNodes} from '../core/groups.js';
8
8
  import {
9
9
  clearFocusFinished,
10
10
  getFocusElapsed,
@@ -12,15 +12,15 @@ import {
12
12
  markFocusFinished,
13
13
  setFocusElapsed,
14
14
  type PlaybackState,
15
- } from '../core/playback';
16
- import {pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled} from '../core/scrub';
17
- import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
18
- import {PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback} from '../core/transport';
19
- import {TimelineTrackRow} from './TimelineTrackRow';
20
- import {iconStyle} from './icons';
21
- import {buttonStyle, theme} from './theme';
15
+ } from '../core/playback.js';
16
+ import {pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled} from '../core/scrub.js';
17
+ import {computeTimelineRows, computeTimelineSpan} from '../core/timeline.js';
18
+ import {PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback} from '../core/transport.js';
19
+ import {TimelineTrackRow} from './TimelineTrackRow.js';
20
+ import {iconStyle} from './icons.js';
21
+ import {buttonStyle, theme} from './theme.js';
22
22
 
23
- export type {PlaybackState} from '../core/playback';
23
+ export type {PlaybackState} from '../core/playback.js';
24
24
 
25
25
  export interface TimelinePanelProps {
26
26
  binding: EffectBinding;
@@ -29,8 +29,8 @@ export interface TimelinePanelProps {
29
29
  scene: Scene;
30
30
  renderer: BatchedRenderer;
31
31
  playbackRef: React.MutableRefObject<PlaybackState>;
32
- playheadRef: React.RefObject<HTMLDivElement>;
33
- counterRef: React.RefObject<HTMLSpanElement>;
32
+ playheadRef: React.RefObject<HTMLDivElement | null>;
33
+ counterRef: React.RefObject<HTMLSpanElement | null>;
34
34
  stagePlaying: boolean;
35
35
  setStagePlaying: (playing: boolean) => void;
36
36
  speed: number;
@@ -146,7 +146,9 @@ export function TimelinePanel(props: TimelinePanelProps): React.ReactElement {
146
146
  return () => observer.disconnect();
147
147
  }, []);
148
148
 
149
- const rows = useMemo(() => computeTimelineRows(binding), [binding, binding.getRevision()]);
149
+ const revision = binding.getRevision();
150
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the binding changes in place; its revision says when to recompute
151
+ const rows = useMemo(() => computeTimelineRows(binding), [binding, revision]);
150
152
  const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
151
153
  const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET - RIGHT_PADDING) / timelineSpan;
152
154
  const anyLooping = useMemo(() => rows.some((r) => r.kind === 'track' && r.looping), [rows]);
@@ -1,4 +1,4 @@
1
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
1
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import {
3
3
  CheckIcon,
4
4
  ChevronDownIcon,
@@ -13,14 +13,14 @@ import {
13
13
  } from '@heroicons/react/24/solid';
14
14
  import type {ParticleSystem} from 'babylon.quarks';
15
15
  import React, {useState} from 'react';
16
- import type {EffectBinding} from '../core/binding';
17
- import {buildEffectTree, collectSystems} from '../core/effectTree';
18
- import {ungroupNode} from '../core/groups';
19
- import {createChildSystem, removeSystems} from '../core/systems';
20
- import type {TimelineRow} from '../core/timeline';
21
- import {PromptDialog} from './PromptDialog';
22
- import {iconStyle} from './icons';
23
- import {theme} from './theme';
16
+ import type {EffectBinding} from '../core/binding.js';
17
+ import {buildEffectTree, collectSystems} from '../core/effectTree.js';
18
+ import {ungroupNode} from '../core/groups.js';
19
+ import {createChildSystem, removeSystems} from '../core/systems.js';
20
+ import type {TimelineRow} from '../core/timeline.js';
21
+ import {PromptDialog} from './PromptDialog.js';
22
+ import {iconStyle} from './icons.js';
23
+ import {theme} from './theme.js';
24
24
 
25
25
  const iconButton: React.CSSProperties = {
26
26
  background: 'none',
@@ -1,12 +1,12 @@
1
- import type {Scene} from '@babylonjs/core/scene';
1
+ import type {Scene} from '@babylonjs/core/scene.js';
2
2
  import {ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon} from '@heroicons/react/24/solid';
3
3
  import type {BatchedRenderer} from 'babylon.quarks';
4
4
  import React from 'react';
5
- import type {EffectBinding} from '../core/binding';
6
- import type {PlaybackState} from '../core/playback';
7
- import {PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback} from '../core/transport';
8
- import {iconStyle} from './icons';
9
- import {buttonStyle, theme} from './theme';
5
+ import type {EffectBinding} from '../core/binding.js';
6
+ import type {PlaybackState} from '../core/playback.js';
7
+ import {PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback} from '../core/transport.js';
8
+ import {iconStyle} from './icons.js';
9
+ import {buttonStyle, theme} from './theme.js';
10
10
 
11
11
  export interface TransportBarProps {
12
12
  binding: EffectBinding | null;
@@ -14,7 +14,7 @@ export interface TransportBarProps {
14
14
  renderer: BatchedRenderer;
15
15
  playbackRef: React.MutableRefObject<PlaybackState>;
16
16
  /** The host's render loop writes time and particle count here. */
17
- counterRef: React.RefObject<HTMLSpanElement>;
17
+ counterRef: React.RefObject<HTMLSpanElement | null>;
18
18
  stagePlaying: boolean;
19
19
  setStagePlaying: (playing: boolean) => void;
20
20
  speed: number;
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
- import {ScalarGenerator, ScalarValueState, buildCurve, buildScalar, readPieces, readScalar} from '../core/values';
3
- import {CurveEditor} from './CurveEditor';
4
- import {NumberField, Row, SelectField} from './fields';
2
+ import {ScalarGenerator, ScalarValueState, buildCurve, buildScalar, readPieces, readScalar} from '../core/values.js';
3
+ import {CurveEditor} from './CurveEditor.js';
4
+ import {NumberField, Row, SelectField} from './fields.js';
5
5
 
6
6
  /**
7
7
  * Unity-like value field: Constant | Random Between Two Constants | Curve,
@@ -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,17 +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';
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} from './ModuleSection.js';
45
+ import {theme} from './theme.js';
46
+ import {ValueField} from './ValueField.js';
47
47
 
48
48
  interface ModuleProps {
49
49
  binding: EffectBinding;
@@ -156,6 +156,16 @@ export function ForceOverLifeModule({binding}: ModuleProps) {
156
156
  >
157
157
  {(behavior) => (
158
158
  <>
159
+ <Row label="Space">
160
+ <SelectField
161
+ value={behavior.space}
162
+ options={[
163
+ {value: 'world', label: 'World'},
164
+ {value: 'local', label: 'Local'},
165
+ ]}
166
+ onChange={(v) => binding.apply(() => (behavior.space = v))}
167
+ />
168
+ </Row>
159
169
  <ValueField
160
170
  hintKey="force.x"
161
171
  label="X"
@@ -288,6 +298,42 @@ export function InheritVelocityModule({binding}: ModuleProps) {
288
298
  );
289
299
  }
290
300
 
301
+ /** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
302
+ export function AttractorModule({binding}: ModuleProps) {
303
+ return (
304
+ <BehaviorModule<GravityForce>
305
+ binding={binding}
306
+ title="Attractor"
307
+ type="GravityForce"
308
+ create={() => new GravityForce(new Vector3(0, 0, 0), 1)}
309
+ >
310
+ {(behavior) => (
311
+ <>
312
+ <Row label="Center" hintKey="attractor.center">
313
+ <div style={{display: 'flex', gap: 6}}>
314
+ {(['x', 'y', 'z'] as const).map((axis) => (
315
+ <NumberField
316
+ key={axis}
317
+ value={behavior.center[axis]}
318
+ step={0.1}
319
+ onChange={(v) => binding.apply(() => (behavior.center[axis] = v))}
320
+ />
321
+ ))}
322
+ </div>
323
+ </Row>
324
+ <Row label="Strength" hintKey="attractor.magnitude">
325
+ <NumberField
326
+ value={behavior.magnitude}
327
+ step={0.1}
328
+ onChange={(v) => binding.apply(() => (behavior.magnitude = v))}
329
+ />
330
+ </Row>
331
+ </>
332
+ )}
333
+ </BehaviorModule>
334
+ );
335
+ }
336
+
291
337
  export function GravityModule({binding}: ModuleProps) {
292
338
  return (
293
339
  <BehaviorModule<ApplyForce>
@@ -297,13 +343,25 @@ export function GravityModule({binding}: ModuleProps) {
297
343
  create={() => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81))}
298
344
  >
299
345
  {(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>
346
+ <>
347
+ <Row label="Strength" hintKey="gravity.strength">
348
+ <NumberField
349
+ value={readScalar(behavior.magnitude).value}
350
+ step={0.5}
351
+ onChange={(v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v)))}
352
+ />
353
+ </Row>
354
+ <Row label="Space">
355
+ <SelectField
356
+ value={behavior.space}
357
+ options={[
358
+ {value: 'world', label: 'World'},
359
+ {value: 'local', label: 'Local'},
360
+ ]}
361
+ onChange={(v) => binding.apply(() => (behavior.space = v))}
362
+ />
363
+ </Row>
364
+ </>
307
365
  )}
308
366
  </BehaviorModule>
309
367
  );
@@ -735,6 +793,29 @@ export function TurbulenceModule({binding}: ModuleProps) {
735
793
  step={0.1}
736
794
  onChange={(v) => binding.apply(() => (behavior.timeScale = v))}
737
795
  />
796
+ {/* The field is built from the seed, so a different seed is a different swirl. */}
797
+ <Row label="Seed">
798
+ <NumberField
799
+ value={behavior.seed}
800
+ step={1}
801
+ onChange={(v) =>
802
+ binding.apply(() => {
803
+ const seed = Math.round(v);
804
+ const next = new TurbulenceField(
805
+ behavior.scale,
806
+ behavior.octaves,
807
+ behavior.velocityMultiplier,
808
+ behavior.timeScale,
809
+ seed
810
+ );
811
+ behavior.seed = seed;
812
+ behavior.generator = next.generator;
813
+ behavior.initialOffset.copy(next.initialOffset);
814
+ behavior.timeOffset.copy(next.initialOffset);
815
+ })
816
+ }
817
+ />
818
+ </Row>
738
819
  </>
739
820
  )}
740
821
  </BehaviorModule>
@@ -764,13 +845,13 @@ export function EmitDirectionModule({binding}: ModuleProps) {
764
845
 
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
  <>
@@ -950,6 +1031,12 @@ export function WidthOverTrailModule({binding}: ModuleProps) {
950
1031
  );
951
1032
  }
952
1033
 
1034
+ /** How many of the first frames a start-frame generator picks from: its exclusive upper bound. */
1035
+ function startFrameCount(startFrame: FrameOverLife['startFrame']): number {
1036
+ const state = readScalar(startFrame as never);
1037
+ return Math.round(state.mode === 'random' ? state.max : state.value);
1038
+ }
1039
+
953
1040
  export function TextureSheetModule({binding}: ModuleProps) {
954
1041
  const system = binding.system;
955
1042
  const frameBehavior = findBehavior<FrameOverLife>(system.behaviors, 'FrameOverLife');
@@ -1080,13 +1167,109 @@ export function TextureSheetModule({binding}: ModuleProps) {
1080
1167
  />
1081
1168
  </Row>
1082
1169
  {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>
1170
+ <>
1171
+ <Row label="Time mode">
1172
+ <SelectField<'life' | 'fps'>
1173
+ value={frameBehavior.fps > 0 ? 'fps' : 'life'}
1174
+ options={[
1175
+ {value: 'life', label: 'Lifetime (curve over life)'},
1176
+ {value: 'fps', label: 'FPS (frame from curve, then by age)'},
1177
+ ]}
1178
+ onChange={(mode) =>
1179
+ binding.apply(() => {
1180
+ frameBehavior.fps = mode === 'fps' ? 12 : 0;
1181
+ if (mode === 'fps' && frameBehavior.frameCount <= 0) {
1182
+ frameBehavior.frameCount = tiles;
1183
+ }
1184
+ })
1185
+ }
1186
+ />
1187
+ </Row>
1188
+ {frameBehavior.fps > 0 ? (
1189
+ <>
1190
+ <Row label="FPS">
1191
+ <NumberField
1192
+ value={frameBehavior.fps}
1193
+ min={0.01}
1194
+ step={1}
1195
+ onChange={(v) => binding.apply(() => (frameBehavior.fps = v))}
1196
+ />
1197
+ </Row>
1198
+ <Row label="Loop">
1199
+ <CheckboxField
1200
+ value={frameBehavior.loop}
1201
+ onChange={(v) => binding.apply(() => (frameBehavior.loop = v))}
1202
+ />
1203
+ </Row>
1204
+ <Row label="Random frame">
1205
+ <CheckboxField
1206
+ value={frameBehavior.random}
1207
+ onChange={(v) => binding.apply(() => (frameBehavior.random = v))}
1208
+ />
1209
+ </Row>
1210
+ </>
1211
+ ) : (
1212
+ <Row label="Cycles">
1213
+ <NumberField
1214
+ value={frameBehavior.cycles}
1215
+ min={0.01}
1216
+ step={1}
1217
+ onChange={(v) => binding.apply(() => (frameBehavior.cycles = v))}
1218
+ />
1219
+ </Row>
1220
+ )}
1221
+ <Row label="Frame count">
1222
+ <NumberField
1223
+ value={frameBehavior.frameCount}
1224
+ min={frameBehavior.fps > 0 || frameBehavior.startFrame ? 1 : 0}
1225
+ max={tiles}
1226
+ step={1}
1227
+ onChange={(v) =>
1228
+ binding.apply(() => (frameBehavior.frameCount = Math.max(0, Math.round(v))))
1229
+ }
1230
+ />
1231
+ </Row>
1232
+ <Row label="Start frame">
1233
+ <CheckboxField
1234
+ value={!!frameBehavior.startFrame}
1235
+ onChange={(v) =>
1236
+ binding.apply(() => {
1237
+ frameBehavior.startFrame = v ? new IntervalValue(0, tiles) : undefined;
1238
+ // The offset only wraps into a segment; without one it runs off the sheet.
1239
+ if (v && frameBehavior.frameCount <= 0) {
1240
+ frameBehavior.frameCount = tiles;
1241
+ }
1242
+ })
1243
+ }
1244
+ />
1245
+ {frameBehavior.startFrame && (
1246
+ <NumberField
1247
+ value={startFrameCount(frameBehavior.startFrame)}
1248
+ min={1}
1249
+ max={tiles}
1250
+ step={1}
1251
+ onChange={(v) =>
1252
+ binding.apply(
1253
+ () =>
1254
+ (frameBehavior.startFrame = new IntervalValue(
1255
+ 0,
1256
+ Math.max(1, Math.round(v))
1257
+ ))
1258
+ )
1259
+ }
1260
+ />
1261
+ )}
1262
+ </Row>
1263
+ <div style={{marginTop: 6}}>
1264
+ <CurveEditor
1265
+ pieces={readPieces(frameBehavior.frame)}
1266
+ maxValue={Math.max(1, tiles - 1)}
1267
+ onChange={(pieces) =>
1268
+ binding.apply(() => (frameBehavior.frame = buildCurve(pieces) as never))
1269
+ }
1270
+ />
1271
+ </div>
1272
+ </>
1090
1273
  )}
1091
1274
  </ModuleSection>
1092
1275
  );
@@ -1,6 +1,6 @@
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
5
  export function Row(props: {label: string; hint?: string; hintKey?: string; children: React.ReactNode}) {
6
6
  return (
@@ -12,7 +12,13 @@ export function Row(props: {label: string; hint?: string; hintKey?: string; chil
12
12
  }
13
13
 
14
14
  /** 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}) {
15
+ export function NumberField(props: {
16
+ value: number;
17
+ onChange: (value: number) => void;
18
+ step?: number;
19
+ min?: number;
20
+ max?: number;
21
+ }) {
16
22
  const [text, setText] = useState(String(round(props.value)));
17
23
  const focusedRef = React.useRef(false);
18
24
  useEffect(() => {
@@ -28,6 +34,7 @@ export function NumberField(props: {value: number; onChange: (value: number) =>
28
34
  value={text}
29
35
  step={props.step ?? 0.1}
30
36
  min={props.min}
37
+ max={props.max}
31
38
  onFocus={() => {
32
39
  focusedRef.current = true;
33
40
  }}
@@ -1,6 +1,7 @@
1
- export {AssetsPanel} from './AssetsPanel';
2
- export type {AssetsPanelProps} from './AssetsPanel';
1
+ export {AssetsPanel} from './AssetsPanel.js';
2
+ export type {AssetsPanelProps} from './AssetsPanel.js';
3
3
  export {
4
+ AttractorModule,
4
5
  CollisionModule,
5
6
  ColorBySpeedModule,
6
7
  EmitDirectionModule,
@@ -16,24 +17,24 @@ export {
16
17
  TextureSheetModule,
17
18
  TurbulenceModule,
18
19
  WidthOverTrailModule,
19
- } from './behaviorModules';
20
- export {ChoiceDialog} from './ChoiceDialog';
21
- export type {ChoiceDialogAction, ChoiceDialogProps} from './ChoiceDialog';
22
- export {ContextMenu} from './ContextMenu';
23
- export type {ContextMenuItem, ContextMenuProps} from './ContextMenu';
24
- export {CurveEditor} from './CurveEditor';
25
- export {EditorWorkspace, QuarksEditorWorkspace} from './EditorWorkspace';
26
- export type {EditorWorkspaceProps} from './EditorWorkspace';
27
- export {EffectEditor} from './EffectEditor';
28
- export type {EffectEditorProps} from './EffectEditor';
29
- export {EffectEditorHost, QuarksEffectEditor} from './EffectEditorHost';
30
- export type {EffectEditorHostHandle, EffectEditorHostProps} from './EffectEditorHost';
31
- export {EffectTreeView} from './EffectTreeView';
32
- export type {EffectTreeViewProps} from './EffectTreeView';
33
- export {CheckboxField, NumberField, Row, SelectField} from './fields';
34
- export {GradientEditor} from './GradientEditor';
35
- export {MessageDialog} from './MessageDialog';
36
- export type {MessageDialogProps} from './MessageDialog';
20
+ } from './behaviorModules.js';
21
+ export {ChoiceDialog} from './ChoiceDialog.js';
22
+ export type {ChoiceDialogAction, ChoiceDialogProps} from './ChoiceDialog.js';
23
+ export {ContextMenu} from './ContextMenu.js';
24
+ export type {ContextMenuItem, ContextMenuProps} from './ContextMenu.js';
25
+ export {CurveEditor} from './CurveEditor.js';
26
+ export {EditorWorkspace, QuarksEditorWorkspace} from './EditorWorkspace.js';
27
+ export type {EditorWorkspaceProps} from './EditorWorkspace.js';
28
+ export {EffectEditor} from './EffectEditor.js';
29
+ export type {EffectEditorProps} from './EffectEditor.js';
30
+ export {EffectEditorHost, QuarksEffectEditor} from './EffectEditorHost.js';
31
+ export type {EffectEditorHostHandle, EffectEditorHostProps} from './EffectEditorHost.js';
32
+ export {EffectTreeView} from './EffectTreeView.js';
33
+ export type {EffectTreeViewProps} from './EffectTreeView.js';
34
+ export {CheckboxField, NumberField, Row, SelectField} from './fields.js';
35
+ export {GradientEditor} from './GradientEditor.js';
36
+ export {MessageDialog} from './MessageDialog.js';
37
+ export type {MessageDialogProps} from './MessageDialog.js';
37
38
  export {
38
39
  ColorOverLifeModule,
39
40
  EmissionModule,
@@ -41,21 +42,23 @@ export {
41
42
  RendererModule,
42
43
  ShapeModule,
43
44
  SizeOverLifeModule,
44
- } from './modules';
45
- export type {GeometryData, GeometryOption, TextureOption} from './modules';
46
- export {ModuleSection} from './ModuleSection';
47
- export {ProjectsPage} from './ProjectsPage';
48
- export type {ProjectsPageProps} from './ProjectsPage';
49
- export {PromptDialog} from './PromptDialog';
50
- export type {PromptDialogProps} from './PromptDialog';
51
- export {ScenePanel} from './ScenePanel';
52
- export type {ScenePanelProps} from './ScenePanel';
53
- export {SubEmittersModule} from './SubEmittersModule';
54
- export {buttonStyle, inputStyle, labelStyle, rowStyle, theme} from './theme';
55
- export {TimelinePanel} from './TimelinePanel';
56
- export type {PlaybackState, TimelinePanelProps} from './TimelinePanel';
57
- export {TimelineTrackRow} from './TimelineTrackRow';
58
- export type {TimelineTrackRowProps} from './TimelineTrackRow';
59
- export {TransportBar} from './TransportBar';
60
- export type {TransportBarProps} from './TransportBar';
61
- export {ValueField} from './ValueField';
45
+ } from './modules.js';
46
+ export type {GeometryData, GeometryOption, TextureOption} from './modules.js';
47
+ export {ModuleSection} from './ModuleSection.js';
48
+ export {ProjectsPage} from './ProjectsPage.js';
49
+ export type {ProjectsPageProps} from './ProjectsPage.js';
50
+ export {PromptDialog} from './PromptDialog.js';
51
+ export type {PromptDialogProps} from './PromptDialog.js';
52
+ export {ScenePanel} from './ScenePanel.js';
53
+ export type {ScenePanelProps} from './ScenePanel.js';
54
+ export {SubEmittersModule} from './SubEmittersModule.js';
55
+ export {TextureSelect, textureUrlOf} from './TextureSelect.js';
56
+ export {TextureUVField} from './TextureUVField.js';
57
+ export {buttonStyle, inputStyle, labelStyle, rowStyle, theme} from './theme.js';
58
+ export {TimelinePanel} from './TimelinePanel.js';
59
+ export type {PlaybackState, TimelinePanelProps} from './TimelinePanel.js';
60
+ export {TimelineTrackRow} from './TimelineTrackRow.js';
61
+ export type {TimelineTrackRowProps} from './TimelineTrackRow.js';
62
+ export {TransportBar} from './TransportBar.js';
63
+ export type {TransportBarProps} from './TransportBar.js';
64
+ export {ValueField} from './ValueField.js';
@@ -13,6 +13,7 @@ const INSPECTOR_HINTS: Record<string, string> = {
13
13
  'Velocity over Lifetime': 'Adds linear and orbital velocity each frame in local or world space.',
14
14
  'Inherit Velocity': 'Copies the emitter’s movement into new particle velocity.',
15
15
  Gravity: 'Applies a constant downward force (Earth-like gravity by default).',
16
+ Attractor: 'Pulls particles towards a point, harder the nearer they are (a point gravity).',
16
17
  'Rotation over Lifetime': 'Spins particles by setting angular velocity over lifetime.',
17
18
  Noise: 'Procedural noise that jitters particle position and rotation.',
18
19
  Turbulence: '3D turbulent velocity field that swirls particles through space.',
@@ -31,7 +32,11 @@ const INSPECTOR_HINTS: Record<string, string> = {
31
32
  Duration: 'How long the particle system runs before stopping (non-looping) or one loop cycle.',
32
33
  Looping: 'When enabled, the effect restarts from the beginning after duration ends.',
33
34
  'World space': 'Simulates in world coordinates so moving the emitter does not drag existing particles.',
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).',
34
37
  Prewarm: 'Simulates the effect once at start so it already looks mid-playback when play begins.',
38
+ 'Warm-up time':
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.',
35
40
  'Start delay': 'Wait time before emission starts after play or restart.',
36
41
  'Start lifetime': 'How long each spawned particle lives before dying.',
37
42
  'Start speed': 'Initial speed along the emission direction when a particle is born.',
@@ -70,12 +75,37 @@ const INSPECTOR_HINTS: Record<string, string> = {
70
75
  'Render mode': 'Orientation and geometry used to draw each particle.',
71
76
  'Trail length': 'Maximum number of trail segments per particle (trail mode).',
72
77
  'Follow origin': 'Keeps the trail anchored to the emitter origin instead of the particle head.',
78
+ 'Texture mode':
79
+ 'How the texture runs along the ribbon: once by distance, once with each segment an equal share, repeated per world unit, or once per segment.',
80
+ 'Texture scale': 'Repeats of the texture: per world unit (tile), per segment (repeat), or over the ribbon.',
81
+ 'Min vertex distance':
82
+ 'The particle has to move this far before a new point is fixed; the last point follows it meanwhile. 0: a point every step.',
83
+ 'Trail time':
84
+ 'How long a point is kept, in seconds, within the trail length. 0: until the trail length is reached.',
73
85
  'Speed factor': 'How much particle speed stretches the billboard (stretched billboard mode).',
74
86
  'Length factor': 'Base stretch multiplier for stretched billboards.',
75
87
  Source: 'Material preset loaded from the effect JSON (read-only reference).',
76
88
  'Blend mode': 'How particle color is composited over the background.',
77
89
  Transparent: 'Enables alpha blending for soft edges and fade-outs.',
78
90
  Texture: 'Sprite or flipbook image applied to each particle.',
91
+ 'UV channel':
92
+ 'Which UV set the texture reads: the sprite or mesh UV, or a mesh’s second set / the length of a trail.',
93
+ Tiling: 'Repeats of the texture across the UV range, U and V (Unity’s material Tiling).',
94
+ Offset: 'Shift of the texture in UV units after tiling, U and V (Unity’s material Offset).',
95
+ Scroll: 'UV units per second the texture moves on the renderer’s clock (Unreal’s Panner).',
96
+ 'Flip U/V': 'Fraction of particles mirrored horizontally and vertically, picked at birth (Unity’s renderer Flip).',
97
+ Ramp: 'Colour ramp read left to right; without one there is no gradient map.',
98
+ 'Ramp channel':
99
+ 'Which value of the map texel picks the colour along the ramp: one channel of a packed texture, or luminance.',
100
+ Erosion: 'Lets alpha erode the shape through a mask instead of fading it.',
101
+ 'Erosion mask': 'Texture whose red channel orders what goes first; none uses the map’s own alpha.',
102
+ Softness: 'Width of the soft transition at the erosion cut, in mask units.',
103
+ 'Edge width': 'Width of the glowing band above the erosion cut; 0 draws none.',
104
+ 'Edge color': 'Linear RGB added in the edge band; may exceed 1 for a glow.',
105
+ 'Noise texture': 'Texture whose red and green channels push the map’s UV about; it should repeat.',
106
+ 'Distortion strength': 'How far the map’s UV is pushed at most, in UV units.',
107
+ 'Swept trail': 'Spans the ribbon along a fixed axis of the emitter instead of facing the camera (a weapon trail).',
108
+ 'Side axis': 'Local axis the swept trail spans its width along; give a unit vector.',
79
109
  'Alpha test': 'Pixels below this alpha are discarded (cutout/hard edges).',
80
110
  'Depth write': 'Writes to the depth buffer — usually off for additive particles.',
81
111
  'Depth test': 'Whether particles are occluded by scene geometry.',
@@ -90,6 +120,16 @@ const INSPECTOR_HINTS: Record<string, string> = {
90
120
  'Blend tiles': 'Smoothly blends between adjacent atlas tiles during animation.',
91
121
  'Start tile': 'Atlas cell index where playback begins (0 = top-left).',
92
122
  'Animate frames': 'Enables frame-over-lifetime curve to cycle through atlas tiles.',
123
+ 'Time mode':
124
+ 'Lifetime samples the curve over each particle’s life; FPS plays frames by age from the curve’s frame.',
125
+ FPS: 'Frames per second in FPS mode.',
126
+ Loop: 'In FPS mode, wrap back to the first frame of the segment, or hold the last.',
127
+ 'Random frame': 'In FPS mode, show a random frame of the segment every tick instead of the next one.',
128
+ 'Frame count': 'Frames in the segment that plays, from the first frame; 0 means no segment.',
129
+ 'Start frame':
130
+ 'Starts each particle on a random one of the first N frames (Unity’s Start Frame); editing sets a 0…N range.',
131
+ 'attractor.center': 'Point in the system’s space particles are pulled towards.',
132
+ 'attractor.magnitude': 'Pull strength, divided by the squared distance to the center.',
93
133
 
94
134
  // Sub emitters
95
135
  Trigger: 'When the child system is spawned: on particle birth, death, or every frame.',