babylon.quarks-editor 0.27.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +31 -4
  3. package/dist/core/assetTree.d.ts +5 -0
  4. package/dist/core/assets.d.ts +7 -0
  5. package/dist/core/assets.js +11 -7
  6. package/dist/core/binding.d.ts +9 -0
  7. package/dist/core/binding.js +8 -0
  8. package/dist/core/collision.d.ts +4 -0
  9. package/dist/core/collision.js +4 -0
  10. package/dist/core/colors.d.ts +16 -0
  11. package/dist/core/colors.js +8 -0
  12. package/dist/core/defaultEffect.d.ts +3 -2
  13. package/dist/core/defaultEffect.js +3 -1
  14. package/dist/core/editorSession.d.ts +229 -0
  15. package/dist/core/editorSession.js +983 -0
  16. package/dist/core/effectTree.d.ts +5 -0
  17. package/dist/core/emitterShapeWireframe.js +11 -4
  18. package/dist/core/galleryPlayback.js +2 -1
  19. package/dist/core/history.d.ts +6 -0
  20. package/dist/core/history.js +4 -0
  21. package/dist/core/indexedDbProjectStore.d.ts +13 -0
  22. package/dist/core/indexedDbProjectStore.js +13 -0
  23. package/dist/core/loadEffect.js +2 -0
  24. package/dist/core/material.d.ts +12 -2
  25. package/dist/core/playback.d.ts +6 -0
  26. package/dist/core/projectStore.d.ts +26 -0
  27. package/dist/core/projectStore.js +1 -0
  28. package/dist/core/sceneEntries.d.ts +3 -0
  29. package/dist/core/scrub.d.ts +1 -1
  30. package/dist/core/scrub.js +2 -2
  31. package/dist/core/shapes.d.ts +31 -1
  32. package/dist/core/shapes.js +18 -3
  33. package/dist/core/systems.d.ts +2 -0
  34. package/dist/core/systems.js +5 -1
  35. package/dist/core/timeline.d.ts +23 -3
  36. package/dist/core/values.d.ts +42 -8
  37. package/dist/core/values.js +52 -9
  38. package/dist/index.d.ts +5 -1
  39. package/dist/react/AssetsPanel.d.ts +7 -0
  40. package/dist/react/AxesCurveField.d.ts +21 -0
  41. package/dist/react/AxesCurveField.js +25 -0
  42. package/dist/react/ChoiceDialog.d.ts +7 -0
  43. package/dist/react/ContextMenu.d.ts +10 -1
  44. package/dist/react/EditorWorkspace.d.ts +7 -0
  45. package/dist/react/EditorWorkspace.js +6 -3
  46. package/dist/react/EffectEditor.d.ts +8 -2
  47. package/dist/react/EffectEditorHost.d.ts +32 -28
  48. package/dist/react/EffectEditorHost.js +241 -1665
  49. package/dist/react/EffectTreeView.d.ts +3 -0
  50. package/dist/react/LifeCurveField.d.ts +18 -0
  51. package/dist/react/LifeCurveField.js +26 -0
  52. package/dist/react/MessageDialog.d.ts +5 -0
  53. package/dist/react/ModuleSection.d.ts +10 -0
  54. package/dist/react/ModuleSection.js +4 -1
  55. package/dist/react/ProjectsPage.d.ts +6 -0
  56. package/dist/react/ProjectsPage.js +27 -11
  57. package/dist/react/PromptDialog.d.ts +7 -0
  58. package/dist/react/ScenePanel.d.ts +7 -0
  59. package/dist/react/ScenePanel.js +7 -5
  60. package/dist/react/TextureSelect.d.ts +5 -5
  61. package/dist/react/TimelinePanel.d.ts +13 -0
  62. package/dist/react/TimelinePanel.js +10 -6
  63. package/dist/react/TimelineTrackRow.d.ts +10 -0
  64. package/dist/react/TransportBar.d.ts +9 -0
  65. package/dist/react/ValueField.d.ts +33 -5
  66. package/dist/react/ValueField.js +34 -9
  67. package/dist/react/behaviorModules.d.ts +16 -5
  68. package/dist/react/behaviorModules.js +28 -9
  69. package/dist/react/fields.d.ts +8 -0
  70. package/dist/react/fields.js +3 -0
  71. package/dist/react/host/BottomDocks.d.ts +36 -0
  72. package/dist/react/host/BottomDocks.js +63 -0
  73. package/dist/react/host/HostDialogs.d.ts +50 -0
  74. package/dist/react/host/HostDialogs.js +113 -0
  75. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  76. package/dist/react/host/InspectorSidebar.js +59 -0
  77. package/dist/react/host/SceneSidebar.d.ts +53 -0
  78. package/dist/react/host/SceneSidebar.js +62 -0
  79. package/dist/react/host/ViewportPane.d.ts +56 -0
  80. package/dist/react/host/ViewportPane.js +91 -0
  81. package/dist/react/host/hostHooks.d.ts +44 -0
  82. package/dist/react/host/hostHooks.js +164 -0
  83. package/dist/react/host/hostStyles.d.ts +21 -0
  84. package/dist/react/host/hostStyles.js +57 -0
  85. package/dist/react/index.d.ts +3 -1
  86. package/dist/react/inspectorHints.js +1 -1
  87. package/dist/react/modules.d.ts +17 -6
  88. package/dist/react/modules.js +77 -34
  89. package/dist/react/theme.d.ts +15 -0
  90. package/dist/react/theme.js +15 -0
  91. package/dist/session.d.ts +9 -0
  92. package/dist/session.js +8 -0
  93. package/package.json +17 -11
  94. package/src/core/assetTree.ts +14 -1
  95. package/src/core/assets.ts +25 -10
  96. package/src/core/binding.ts +9 -0
  97. package/src/core/collision.ts +4 -0
  98. package/src/core/colors.ts +21 -1
  99. package/src/core/defaultEffect.ts +6 -3
  100. package/src/core/editorSession.ts +1299 -0
  101. package/src/core/effectTree.ts +5 -0
  102. package/src/core/emitterShapeWireframe.ts +12 -4
  103. package/src/core/galleryPlayback.ts +2 -1
  104. package/src/core/history.ts +6 -0
  105. package/src/core/indexedDbProjectStore.ts +13 -0
  106. package/src/core/loadEffect.ts +3 -1
  107. package/src/core/material.ts +14 -4
  108. package/src/core/playback.ts +6 -0
  109. package/src/core/projectStore.ts +26 -0
  110. package/src/core/sceneEntries.ts +3 -0
  111. package/src/core/scrub.ts +2 -2
  112. package/src/core/shapes.ts +48 -3
  113. package/src/core/systems.ts +11 -2
  114. package/src/core/timeline.ts +40 -15
  115. package/src/core/values.ts +91 -14
  116. package/src/index.ts +5 -1
  117. package/src/react/AssetsPanel.tsx +7 -0
  118. package/src/react/AxesCurveField.tsx +72 -0
  119. package/src/react/ChoiceDialog.tsx +7 -0
  120. package/src/react/ContextMenu.tsx +10 -1
  121. package/src/react/EditorWorkspace.tsx +11 -5
  122. package/src/react/EffectEditor.tsx +7 -1
  123. package/src/react/EffectEditorHost.tsx +406 -2340
  124. package/src/react/EffectTreeView.tsx +3 -0
  125. package/src/react/LifeCurveField.tsx +57 -0
  126. package/src/react/MessageDialog.tsx +5 -0
  127. package/src/react/ModuleSection.tsx +11 -0
  128. package/src/react/ProjectsPage.tsx +38 -11
  129. package/src/react/PromptDialog.tsx +7 -0
  130. package/src/react/ScenePanel.tsx +14 -5
  131. package/src/react/TextureSelect.tsx +6 -6
  132. package/src/react/TimelinePanel.tsx +23 -6
  133. package/src/react/TimelineTrackRow.tsx +10 -0
  134. package/src/react/TransportBar.tsx +9 -0
  135. package/src/react/ValueField.tsx +99 -15
  136. package/src/react/behaviorModules.tsx +58 -42
  137. package/src/react/fields.tsx +12 -1
  138. package/src/react/host/BottomDocks.tsx +178 -0
  139. package/src/react/host/HostDialogs.tsx +228 -0
  140. package/src/react/host/InspectorSidebar.tsx +147 -0
  141. package/src/react/host/SceneSidebar.tsx +227 -0
  142. package/src/react/host/ViewportPane.tsx +300 -0
  143. package/src/react/host/hostHooks.ts +198 -0
  144. package/src/react/host/hostStyles.ts +65 -0
  145. package/src/react/index.ts +3 -1
  146. package/src/react/inspectorHints.ts +1 -1
  147. package/src/react/modules.tsx +192 -71
  148. package/src/react/theme.ts +15 -0
  149. package/src/session.ts +17 -0
@@ -1,9 +1,12 @@
1
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
+ /** Props of {@link EffectTreeView}. */
3
4
  export interface EffectTreeViewProps {
5
+ /** The effect's root. */
4
6
  root: TransformNode;
5
7
  /** Node shown in the inspector (and moved by the gizmo). */
6
8
  selectedNode: TransformNode | null;
9
+ /** Selects a group or emitter. */
7
10
  onSelect: (node: TransformNode) => void;
8
11
  /** Show the root itself as the first row (default: only its descendants). */
9
12
  includeRoot?: boolean;
@@ -0,0 +1,18 @@
1
+ import { PiecewiseBezier, RandomBetweenCurves } from 'babylon.quarks';
2
+ import React from 'react';
3
+ import { AnyGenerator } from '../core/values.js';
4
+ /** Props of a {@link LifeCurveField}. */
5
+ export interface LifeCurveFieldProps {
6
+ /** The generator shown: one curve, two curves, or anything else, read as one curve. */
7
+ generator: AnyGenerator | undefined;
8
+ /** Top of the curve editors' value range. */
9
+ maxValue?: number;
10
+ /** Receives the edited curve or curves. */
11
+ onChange: (generator: PiecewiseBezier | RandomBetweenCurves) => void;
12
+ }
13
+ /**
14
+ * A setting that is a curve over the particle's life, or two with a "Two curves" switch (Unity's
15
+ * Random Between Two Curves): each particle then follows its own curve between the lower and the
16
+ * upper one. Every segment of both curves is kept through edits and switches.
17
+ */
18
+ export declare function LifeCurveField(props: LifeCurveFieldProps): React.ReactElement;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { IntervalValue, RandomBetweenCurves } from 'babylon.quarks';
3
+ import { buildCurve, readPieces, withCurves } from '../core/values.js';
4
+ import { CurveEditor } from './CurveEditor.js';
5
+ import { CheckboxField, Row } from './fields.js';
6
+ const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
7
+ /**
8
+ * A setting that is a curve over the particle's life, or two with a "Two curves" switch (Unity's
9
+ * Random Between Two Curves): each particle then follows its own curve between the lower and the
10
+ * upper one. Every segment of both curves is kept through edits and switches.
11
+ */
12
+ export function LifeCurveField(props) {
13
+ const { generator } = props;
14
+ // A random range shows as two flat curves, and an edit turns it into them.
15
+ const two = generator instanceof RandomBetweenCurves || generator instanceof IntervalValue;
16
+ const edit = (pieces, upper) => {
17
+ if (!two) {
18
+ props.onChange(buildCurve(pieces));
19
+ return;
20
+ }
21
+ const lower = upper ? buildCurve(readPieces(generator)) : buildCurve(pieces);
22
+ const higher = upper ? buildCurve(pieces) : buildCurve(readPieces(generator, true));
23
+ props.onChange(new RandomBetweenCurves(lower, higher));
24
+ };
25
+ return (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Two curves", hint: "Random between two curves: each particle follows its own curve between them.", children: _jsx(CheckboxField, { value: two, onChange: (on) => props.onChange(withCurves(generator, on ? 'curves' : 'curve')) }) }), (two ? ['lower', 'upper'] : ['curve']).map((bound) => (_jsxs("div", { style: { marginTop: 6 }, children: [two && _jsx("span", { style: captionStyle, children: bound }), _jsx(CurveEditor, { pieces: readPieces(generator, bound === 'upper'), maxValue: props.maxValue, onChange: (pieces) => edit(pieces, bound === 'upper') })] }, bound)))] }));
26
+ }
@@ -1,8 +1,13 @@
1
1
  import React from 'react';
2
+ /** Props of {@link MessageDialog}. */
2
3
  export interface MessageDialogProps {
4
+ /** The dialog's title. */
3
5
  title: string;
6
+ /** The message; line breaks are kept. */
4
7
  message: string;
8
+ /** The button's text. Default `OK`. */
5
9
  confirmLabel?: string;
10
+ /** Closes the dialog: the button, Enter, Escape or the backdrop. */
6
11
  onClose: () => void;
7
12
  }
8
13
  /** Modal for info and error messages — same chrome as PromptDialog, no text input. */
@@ -1,5 +1,15 @@
1
1
  import React from 'react';
2
+ import type { EffectBinding } from '../core/binding.js';
2
3
  /** Unity-style module foldout with an optional enable checkbox. */
4
+ /** Props of every inspector module: the effect binding it edits, through `binding.apply`. */
5
+ export interface ModuleProps {
6
+ /** The focused effect; the module edits `binding.system`. */
7
+ binding: EffectBinding;
8
+ }
9
+ /**
10
+ * An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
11
+ * given. Unity's Particle System inspector modules look the same way.
12
+ */
3
13
  export declare function ModuleSection(props: {
4
14
  title: string;
5
15
  hint?: string;
@@ -5,7 +5,10 @@ import { iconStyle } from './icons.js';
5
5
  import { getInspectorHint } from './inspectorHints.js';
6
6
  import { InspectorInfoIcon } from './InspectorInfoIcon.js';
7
7
  import { theme } from './theme.js';
8
- /** Unity-style module foldout with an optional enable checkbox. */
8
+ /**
9
+ * An inspector module: a foldout with a title, a hint, and an enable checkbox when `onToggle` is
10
+ * given. Unity's Particle System inspector modules look the same way.
11
+ */
9
12
  export function ModuleSection(props) {
10
13
  const [open, setOpen] = useState(props.defaultOpen ?? true);
11
14
  const toggleable = props.onToggle !== undefined;
@@ -1,12 +1,18 @@
1
1
  import React from 'react';
2
2
  import { type ProjectMeta, type ProjectStore } from '../core/projectStore.js';
3
+ /** Props of {@link ProjectsPage}. */
3
4
  export interface ProjectsPageProps {
5
+ /** Where the projects live. */
4
6
  store: ProjectStore;
7
+ /** Opens a project in the editor. */
5
8
  onOpen: (project: ProjectMeta) => void;
9
+ /** The page's heading. */
6
10
  title?: string;
7
11
  /** Optional link back to the page that hosts the editor (e.g. a gallery), shown above the title. */
8
12
  backLink?: {
13
+ /** Where the link goes. */
9
14
  href: string;
15
+ /** The link's text. */
10
16
  label: string;
11
17
  };
12
18
  }
@@ -29,6 +29,15 @@ function StorageBar(props) {
29
29
  background: theme.accent,
30
30
  } }) }), _jsx("div", { style: { fontSize: 11.5, color: theme.textDim, lineHeight: 1.5 }, children: "Projects are saved in this browser only (IndexedDB). Clearing site data removes them, and Safari deletes data of sites not opened for 7 days \u2014 export projects you care about." })] }));
31
31
  }
32
+ async function listProjects(store) {
33
+ try {
34
+ const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
35
+ return { list, storage };
36
+ }
37
+ catch (err) {
38
+ return { error: err instanceof Error ? err.message : String(err) };
39
+ }
40
+ }
32
41
  /** Start page: the projects saved in the store, plus storage usage. */
33
42
  export function ProjectsPage(props) {
34
43
  const { store, onOpen, title = 'Effect editor', backLink } = props;
@@ -37,20 +46,27 @@ export function ProjectsPage(props) {
37
46
  const [dialog, setDialog] = useState(null);
38
47
  const [menu, setMenu] = useState(null);
39
48
  const [error, setError] = useState(null);
40
- const reload = useCallback(async () => {
41
- try {
42
- const [list, storage] = await Promise.all([store.listProjects(), store.estimate()]);
43
- setProjects(list);
44
- setEstimate(storage);
45
- }
46
- catch (err) {
47
- setError(err instanceof Error ? err.message : String(err));
49
+ const show = useCallback((listing) => {
50
+ if ('error' in listing) {
51
+ setError(listing.error);
48
52
  setProjects([]);
49
53
  }
50
- }, [store]);
54
+ else {
55
+ setProjects(listing.list);
56
+ setEstimate(listing.storage);
57
+ }
58
+ }, []);
59
+ const reload = useCallback(async () => show(await listProjects(store)), [store, show]);
51
60
  useEffect(() => {
52
- void reload();
53
- }, [reload]);
61
+ let cancelled = false;
62
+ void listProjects(store).then((listing) => {
63
+ if (!cancelled)
64
+ show(listing);
65
+ });
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [store, show]);
54
70
  const run = (action) => {
55
71
  setDialog(null);
56
72
  action()
@@ -1,12 +1,19 @@
1
1
  import React from 'react';
2
+ /** Props of {@link PromptDialog}. */
2
3
  export interface PromptDialogProps {
4
+ /** What to enter. */
3
5
  title: string;
6
+ /** The text the field starts with, selected. */
4
7
  defaultValue?: string;
8
+ /** Hint shown in the empty field. */
5
9
  placeholder?: string;
10
+ /** The confirm button's text. Default `OK`. */
6
11
  confirmLabel?: string;
7
12
  /** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
8
13
  allowEmpty?: boolean;
14
+ /** Receives the entered text, trimmed. */
9
15
  onSubmit: (value: string) => void;
16
+ /** Escape, the backdrop, Cancel, or an empty submit when empty is not allowed. */
10
17
  onCancel: () => void;
11
18
  }
12
19
  /** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
@@ -1,20 +1,27 @@
1
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
3
  import type { SceneEntry } from '../core/sceneEntries.js';
4
+ /** Props of {@link ScenePanel}. */
4
5
  export interface ScenePanelProps {
6
+ /** The effects in the scene, in list order. */
5
7
  entries: SceneEntry[];
6
8
  /** Effect shown in the inspector and timeline. */
7
9
  focusedRoot: TransformNode | null;
8
10
  /** Visible entries — they render and play with the stage transport. */
9
11
  visibleRootIds: ReadonlySet<number>;
12
+ /** Focuses an effect in the inspector and timeline. */
10
13
  onFocus: (entry: SceneEntry) => void;
14
+ /** Shows or hides an effect. */
11
15
  onToggleVisible: (entry: SceneEntry) => void;
12
16
  /** Root of the entry shown alone, or null. */
13
17
  soloRootId: number | null;
18
+ /** Shows an effect alone, or ends solo. */
14
19
  onToggleSolo: (entry: SceneEntry) => void;
20
+ /** Removes an effect from the scene. */
15
21
  onDelete: (entry: SceneEntry) => void;
16
22
  /** "Save as new asset…" in the row's right-click menu. */
17
23
  onSaveAsAsset?: (entry: SceneEntry) => void;
24
+ /** Adds a new default effect. */
18
25
  onCreateDefault: () => void;
19
26
  /** Node shown in the inspector; highlighted in the expanded effect trees. */
20
27
  selectedNode?: TransformNode | null;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDownIcon, ChevronRightIcon, EyeIcon, EyeSlashIcon, SparklesIcon, TrashIcon, ViewfinderCircleIcon, } from '@heroicons/react/24/solid';
3
- import React, { useEffect, useState } from 'react';
3
+ import React, { useState } from 'react';
4
4
  import { buildEffectTree } from '../core/effectTree.js';
5
5
  import { ContextMenu } from './ContextMenu.js';
6
6
  import { EffectTreeView } from './EffectTreeView.js';
@@ -83,11 +83,13 @@ export function ScenePanel(props) {
83
83
  const [menu, setMenu] = useState(null);
84
84
  const [expanded, setExpanded] = useState(() => new Set());
85
85
  // The focused effect opens its tree, so its sub-emitters are one click away.
86
- useEffect(() => {
87
- if (focusedRoot) {
88
- setExpanded((prev) => (prev.has(focusedRoot.uniqueId) ? prev : new Set(prev).add(focusedRoot.uniqueId)));
86
+ const [expandedFocus, setExpandedFocus] = useState(null);
87
+ if (focusedRoot !== expandedFocus) {
88
+ setExpandedFocus(focusedRoot);
89
+ if (focusedRoot && !expanded.has(focusedRoot.uniqueId)) {
90
+ setExpanded(new Set(expanded).add(focusedRoot.uniqueId));
89
91
  }
90
- }, [focusedRoot]);
92
+ }
91
93
  const toggleExpanded = (entry) => setExpanded((prev) => {
92
94
  const next = new Set(prev);
93
95
  if (next.has(entry.root.uniqueId)) {
@@ -1,4 +1,4 @@
1
- import type { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture.js';
1
+ import type { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
2
2
  import type { TextureOption } from './modules.js';
3
3
  /** The url a texture-like object was loaded from, or '' for none. */
4
4
  export declare function textureUrlOf(texture: unknown): string;
@@ -11,10 +11,10 @@ export declare function TextureSelect(props: {
11
11
  /** The slot's current texture (anything with a `url`), or null for none. */
12
12
  value: unknown;
13
13
  textureOptions?: TextureOption[];
14
- /** Turns a URL or `data:` URI into the host's texture object; null when it cannot. */
15
- loadTexture: (url: string) => unknown;
16
- /** The host's texture object, assumed to be a Babylon texture, or null for none. */
17
- onChange: (texture: BaseTexture | null) => void;
14
+ /** Turns a URL or `data:` URI into a texture; null or undefined when it cannot. */
15
+ loadTexture: (url: string) => Texture | null | undefined;
16
+ /** The picked texture, or null for none. */
17
+ onChange: (texture: Texture | null) => void;
18
18
  /** Label of the "none" entry. Default "(none)". */
19
19
  noneLabel?: string;
20
20
  /** Shows a thumbnail of the current texture under the select. Default false. */
@@ -5,18 +5,31 @@ import React from 'react';
5
5
  import type { EffectBinding } from '../core/binding.js';
6
6
  import { type PlaybackState } from '../core/playback.js';
7
7
  export type { PlaybackState } from '../core/playback.js';
8
+ /** Props of {@link TimelinePanel}. */
8
9
  export interface TimelinePanelProps {
10
+ /** The focused effect, whose systems are the tracks. */
9
11
  binding: EffectBinding;
12
+ /** The node shown in the inspector; its track is highlighted. */
10
13
  selectedNode: TransformNode | null;
14
+ /** Selects a track's or group's node. */
11
15
  onSelectNode: (node: TransformNode | null) => void;
16
+ /** The preview scene, redrawn after a scrub. */
12
17
  scene: Scene;
18
+ /** The renderer the effect draws through, refreshed after a scrub. */
13
19
  renderer: BatchedRenderer;
20
+ /** Playback state shared with the host's render loop. */
14
21
  playbackRef: React.MutableRefObject<PlaybackState>;
22
+ /** The playhead line, moved every frame without a re-render. */
15
23
  playheadRef: React.RefObject<HTMLDivElement | null>;
24
+ /** The time and particle counter in the header, written by the host's render loop. */
16
25
  counterRef: React.RefObject<HTMLSpanElement | null>;
26
+ /** The stage is playing. */
17
27
  stagePlaying: boolean;
28
+ /** Plays or pauses the stage. */
18
29
  setStagePlaying: (playing: boolean) => void;
30
+ /** Playback speed, 1 for real time. */
19
31
  speed: number;
32
+ /** Sets the playback speed. */
20
33
  setSpeed: (speed: number) => void;
21
34
  /** False when the focused effect is hidden — it has nothing to play. */
22
35
  playbackEnabled?: boolean;
@@ -38,10 +38,13 @@ export function TimelinePanel(props) {
38
38
  const bodyRef = useRef(null);
39
39
  const dragRef = useRef({ raf: null, pendingTime: null });
40
40
  const focusRootId = binding.root.uniqueId;
41
- React.useEffect(() => {
41
+ // Another effect starts with every system shown and nothing selected.
42
+ const [shownBinding, setShownBinding] = useState(binding);
43
+ if (binding !== shownBinding) {
44
+ setShownBinding(binding);
42
45
  setHiddenSystems(new Set());
43
46
  setGroupSelection(new Set());
44
- }, [binding]);
47
+ }
45
48
  const toggleGroupSelect = (node) => {
46
49
  setGroupSelection((prev) => {
47
50
  const next = new Set(prev);
@@ -145,18 +148,19 @@ export function TimelinePanel(props) {
145
148
  renderer,
146
149
  ]);
147
150
  const visibleRows = useMemo(() => {
151
+ const visible = [];
148
152
  let hideUntilDepth = null;
149
- const visible = rows.filter((row) => {
153
+ for (const row of rows) {
150
154
  if (hideUntilDepth !== null) {
151
155
  if (row.depth > hideUntilDepth)
152
- return false;
156
+ continue;
153
157
  hideUntilDepth = null;
154
158
  }
155
159
  if (row.kind === 'group' && collapsedGroups.has(row.node)) {
156
160
  hideUntilDepth = row.depth;
157
161
  }
158
- return true;
159
- });
162
+ visible.push(row);
163
+ }
160
164
  return visible;
161
165
  }, [rows, collapsedGroups]);
162
166
  const toggleCollapse = (node) => {
@@ -2,21 +2,31 @@ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
3
  import type { EffectBinding } from '../core/binding.js';
4
4
  import type { TimelineRow } from '../core/timeline.js';
5
+ /** Props of {@link TimelineTrackRow}. */
5
6
  export interface TimelineTrackRowProps {
7
+ /** The group or track the row shows. */
6
8
  row: TimelineRow;
9
+ /** The focused effect, edited by the row's actions. */
7
10
  binding: EffectBinding;
11
+ /** The row's node is the one in the inspector. */
8
12
  selected: boolean;
13
+ /** Selects the row's node. */
9
14
  onSelectNode: (node: TransformNode | null) => void;
15
+ /** The ruler's scale, pixels per second. */
10
16
  pxPerSecond: number;
11
17
  /** Matches the ruler's left gutter so bars line up under the correct tick. */
12
18
  offsetPx: number;
19
+ /** A group row's children are hidden. */
13
20
  collapsed: boolean;
21
+ /** Collapses or expands a group row. */
14
22
  onToggleCollapse?: () => void;
15
23
  /** Preview-only visibility (toggles `system.emitter.visible`); lets you isolate one track. */
16
24
  hidden?: boolean;
25
+ /** Shows or hides the track in the preview. */
17
26
  onToggleVisible?: () => void;
18
27
  /** Membership in the "group selected rows together" set (unrelated to inspector selection). */
19
28
  groupSelected: boolean;
29
+ /** Adds the row to the set, or takes it out. */
20
30
  onToggleGroupSelect: () => void;
21
31
  }
22
32
  /** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
@@ -3,16 +3,25 @@ import type { BatchedRenderer } from 'babylon.quarks';
3
3
  import React from 'react';
4
4
  import type { EffectBinding } from '../core/binding.js';
5
5
  import type { PlaybackState } from '../core/playback.js';
6
+ /** Props of {@link TransportBar}. */
6
7
  export interface TransportBarProps {
8
+ /** The focused effect, which Restart and Step act on. */
7
9
  binding: EffectBinding | null;
10
+ /** The preview scene, redrawn after a restart. */
8
11
  scene: Scene;
12
+ /** The renderer the effects draw through. */
9
13
  renderer: BatchedRenderer;
14
+ /** Playback state shared with the host's render loop. */
10
15
  playbackRef: React.MutableRefObject<PlaybackState>;
11
16
  /** The host's render loop writes time and particle count here. */
12
17
  counterRef: React.RefObject<HTMLSpanElement | null>;
18
+ /** The stage is playing. */
13
19
  stagePlaying: boolean;
20
+ /** Plays or pauses the stage. */
14
21
  setStagePlaying: (playing: boolean) => void;
22
+ /** Playback speed, 1 for real time. */
15
23
  speed: number;
24
+ /** Sets the playback speed. */
16
25
  setSpeed: (speed: number) => void;
17
26
  /** False when the focused effect is hidden. */
18
27
  enabled: boolean;
@@ -1,11 +1,39 @@
1
+ import type { ValueGenerator } from 'babylon.quarks';
1
2
  import React from 'react';
2
- import { ScalarGenerator } from '../core/values.js';
3
- export declare function ValueField(props: {
3
+ import { AnyGenerator, ScalarGenerator } from '../core/values.js';
4
+ /** Props every {@link ValueField} takes. */
5
+ interface ValueFieldBaseProps {
6
+ /** The row's label. */
4
7
  label: string;
8
+ /** Tooltip text of the label. */
5
9
  hint?: string;
10
+ /** Key into the inspector hints, used when `hint` is not given. */
6
11
  hintKey?: string;
7
- generator: ScalarGenerator | undefined;
8
- onChange: (generator: ScalarGenerator) => void;
12
+ /** The generator shown; one that is not a scalar reads as the constant 1. */
13
+ generator: AnyGenerator | undefined;
14
+ /** Smallest value the number fields accept. */
9
15
  min?: number;
16
+ /** Top of the curve editor's value range. */
10
17
  curveMax?: number;
11
- }): React.JSX.Element;
18
+ }
19
+ /** A value field for a setting that takes a curve over the particle's life. */
20
+ export interface CurveValueFieldProps extends ValueFieldBaseProps {
21
+ /** Offers Curve beside Constant and Random (the default). */
22
+ curve?: true;
23
+ /** Receives the edited generator. */
24
+ onChange: (generator: ScalarGenerator) => void;
25
+ }
26
+ /** A value field for a setting sampled once, without a curve: Constant or Random. */
27
+ export interface ConstantValueFieldProps extends ValueFieldBaseProps {
28
+ /** Hides Curve: the setting takes no curve. */
29
+ curve: false;
30
+ /** Receives the edited generator. */
31
+ onChange: (generator: ValueGenerator) => void;
32
+ }
33
+ /**
34
+ * Unity-like value field: Constant | Random Between Two Constants | Curve | Random Between Two
35
+ * Curves, bound to a quarks value generator. With `curve={false}` it offers only the first two,
36
+ * for settings sampled once.
37
+ */
38
+ export declare function ValueField(props: CurveValueFieldProps | ConstantValueFieldProps): React.ReactElement;
39
+ export {};
@@ -1,20 +1,45 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values.js';
2
+ import { RandomBetweenCurves } from 'babylon.quarks';
3
+ import { buildCurve, buildScalar, readPieces, readScalar, withCurves, } from '../core/values.js';
3
4
  import { CurveEditor } from './CurveEditor.js';
4
5
  import { NumberField, Row, SelectField } from './fields.js';
6
+ const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
7
+ function isValueGenerator(generator) {
8
+ return generator.type === 'value';
9
+ }
5
10
  /**
6
- * Unity-like value field: Constant | Random Between Two Constants | Curve,
7
- * bound to a quarks value generator.
11
+ * Unity-like value field: Constant | Random Between Two Constants | Curve | Random Between Two
12
+ * Curves, bound to a quarks value generator. With `curve={false}` it offers only the first two,
13
+ * for settings sampled once.
8
14
  */
9
- const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
10
15
  export function ValueField(props) {
11
- const state = readScalar(props.generator);
12
- const commit = (patch) => {
13
- props.onChange(buildScalar({ ...state, ...patch }));
16
+ const read = readScalar(props.generator);
17
+ // A setting without curves shows a curve it was loaded with as a constant, so an edit replaces it.
18
+ const curved = read.mode === 'curve' || read.mode === 'curves';
19
+ const state = props.curve === false && curved ? { ...read, mode: 'constant' } : read;
20
+ const emit = (generator) => {
21
+ if (props.curve !== false) {
22
+ props.onChange(generator);
23
+ }
24
+ else if (isValueGenerator(generator)) {
25
+ props.onChange(generator);
26
+ }
14
27
  };
28
+ const commit = (patch) => emit(buildScalar({ ...state, ...patch }));
15
29
  return (_jsxs(_Fragment, { children: [_jsx(Row, { label: props.label, hint: props.hint, hintKey: props.hintKey, children: _jsx(SelectField, { value: state.mode, options: [
16
30
  { value: 'constant', label: 'Constant' },
17
31
  { value: 'random', label: 'Random between two' },
18
- { value: 'curve', label: 'Curve' },
19
- ], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => props.onChange(buildCurve(pieces)) }) }))] }));
32
+ ...(props.curve !== false
33
+ ? [
34
+ { value: 'curve', label: 'Curve' },
35
+ { value: 'curves', label: 'Random between two curves' },
36
+ ]
37
+ : []),
38
+ ], onChange: (mode) =>
39
+ // Between one curve and two, every segment is kept; other switches start afresh.
40
+ (mode === 'curve' || mode === 'curves') && curved
41
+ ? emit(withCurves(props.generator, mode))
42
+ : commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => emit(buildCurve(pieces)) }) })), state.mode === 'curves' && (_jsx(_Fragment, { children: ['lower', 'upper'].map((bound, index) => (_jsxs("div", { style: { marginTop: 6 }, children: [_jsx("span", { style: captionStyle, children: bound }), _jsx(CurveEditor, { pieces: readPieces(props.generator, index === 1), maxValue: props.curveMax, onChange: (pieces) => emit(index === 0
43
+ ? new RandomBetweenCurves(buildCurve(pieces), buildCurve(readPieces(props.generator, true)))
44
+ : new RandomBetweenCurves(buildCurve(readPieces(props.generator)), buildCurve(pieces))) })] }, bound))) }))] }));
20
45
  }
@@ -1,17 +1,19 @@
1
1
  import React from 'react';
2
- import { EffectBinding } from '../core/binding.js';
3
2
  import type { TextureOption } from './modules.js';
4
- interface ModuleProps {
5
- binding: EffectBinding;
6
- }
3
+ import { type ModuleProps } from './ModuleSection.js';
4
+ /** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
7
5
  export declare function SpeedOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
6
+ /** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
8
7
  export declare function LimitSpeedOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
8
+ /** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
9
9
  export declare function ForceOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
10
10
  export declare function VelocityOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
11
11
  export declare function InheritVelocityModule({ binding }: ModuleProps): React.JSX.Element;
12
12
  /** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
13
13
  export declare function AttractorModule({ binding }: ModuleProps): React.JSX.Element;
14
+ /** The Gravity module: pulls particles towards a point. */
14
15
  export declare function GravityModule({ binding }: ModuleProps): React.JSX.Element;
16
+ /** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
15
17
  export declare function RotationOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
16
18
  /** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
17
19
  export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
@@ -23,14 +25,23 @@ export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): Reac
23
25
  export declare function SequencesModule({ binding, textureOptions }: ModuleProps & {
24
26
  textureOptions?: TextureOption[];
25
27
  }): React.JSX.Element;
28
+ /** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
26
29
  export declare function NoiseModule({ binding }: ModuleProps): React.JSX.Element;
30
+ /** The Turbulence module: a curl noise force field. */
27
31
  export declare function TurbulenceModule({ binding }: ModuleProps): React.JSX.Element;
32
+ /** The Randomize Direction module: tilts each particle's start direction by a random angle. */
28
33
  export declare function EmitDirectionModule({ binding }: ModuleProps): React.JSX.Element;
34
+ /** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
29
35
  export declare function CollisionModule({ binding }: ModuleProps): React.JSX.Element;
36
+ /** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
30
37
  export declare function ColorBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
38
+ /** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
31
39
  export declare function SizeBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
40
+ /** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
32
41
  export declare function RotationBySpeedModule({ binding }: ModuleProps): React.JSX.Element;
42
+ /** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
33
43
  export declare function OrbitOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
44
+ /** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
34
45
  export declare function WidthOverTrailModule({ binding }: ModuleProps): React.JSX.Element;
46
+ /** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
35
47
  export declare function TextureSheetModule({ binding }: ModuleProps): React.JSX.Element;
36
- export {};