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
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
3
3
  import { useState } from 'react';
4
- import { iconStyle } from './icons';
5
- import { getInspectorHint } from './inspectorHints';
6
- import { InspectorInfoIcon } from './InspectorInfoIcon';
7
- import { theme } from './theme';
4
+ import { iconStyle } from './icons.js';
5
+ import { getInspectorHint } from './inspectorHints.js';
6
+ import { InspectorInfoIcon } from './InspectorInfoIcon.js';
7
+ import { theme } from './theme.js';
8
8
  /** Unity-style module foldout with an optional enable checkbox. */
9
9
  export function ModuleSection(props) {
10
10
  const [open, setOpen] = useState(props.defaultOpen ?? true);
@@ -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
  export declare function ObjectModule(props: {
3
3
  node: TransformNode;
4
4
  label: string;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Quaternion } from '@babylonjs/core/Maths/math.vector';
2
+ import { Quaternion } from '@babylonjs/core/Maths/math.vector.js';
3
3
  import { useSyncExternalStore } from 'react';
4
- import { getTransformRevision, notifyTransformChanged, subscribeTransform } from '../core/transformStore';
5
- import { ModuleSection } from './ModuleSection';
6
- import { NumberField, Row } from './fields';
4
+ import { getTransformRevision, notifyTransformChanged, subscribeTransform } from '../core/transformStore.js';
5
+ import { ModuleSection } from './ModuleSection.js';
6
+ import { NumberField, Row } from './fields.js';
7
7
  const RAD_TO_DEG = 180 / Math.PI;
8
8
  const DEG_TO_RAD = Math.PI / 180;
9
9
  function getEulerDegrees(node) {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type ProjectMeta, type ProjectStore } from '../core/projectStore';
2
+ import { type ProjectMeta, type ProjectStore } from '../core/projectStore.js';
3
3
  export interface ProjectsPageProps {
4
4
  store: ProjectStore;
5
5
  onOpen: (project: ProjectMeta) => void;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowLeftIcon, FolderIcon, PlusIcon } from '@heroicons/react/24/solid';
3
3
  import { useCallback, useEffect, useState } from 'react';
4
- import { formatBytes } from '../core/projectStore';
5
- import { ChoiceDialog } from './ChoiceDialog';
6
- import { ContextMenu } from './ContextMenu';
7
- import { iconStyle } from './icons';
8
- import { PromptDialog } from './PromptDialog';
9
- import { buttonStyle, globalEditorStyles, theme } from './theme';
4
+ import { formatBytes } from '../core/projectStore.js';
5
+ import { ChoiceDialog } from './ChoiceDialog.js';
6
+ import { ContextMenu } from './ContextMenu.js';
7
+ import { iconStyle } from './icons.js';
8
+ import { PromptDialog } from './PromptDialog.js';
9
+ import { buttonStyle, globalEditorStyles, theme } from './theme.js';
10
10
  function formatDate(time) {
11
11
  return new Date(time).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
12
12
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import { buttonStyle, inputStyle, theme } from './theme';
3
+ import { buttonStyle, inputStyle, theme } from './theme.js';
4
4
  /** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
5
5
  * Enter, cancels on Escape or backdrop click. */
6
6
  export function PromptDialog(props) {
@@ -1,6 +1,6 @@
1
- import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
- import type { SceneEntry } from '../core/sceneEntries';
3
+ import type { SceneEntry } from '../core/sceneEntries.js';
4
4
  export interface ScenePanelProps {
5
5
  entries: SceneEntry[];
6
6
  /** Effect shown in the inspector and timeline. */
@@ -1,11 +1,11 @@
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
3
  import React, { useEffect, useState } from 'react';
4
- import { buildEffectTree } from '../core/effectTree';
5
- import { ContextMenu } from './ContextMenu';
6
- import { EffectTreeView } from './EffectTreeView';
7
- import { iconStyle } from './icons';
8
- import { buttonStyle, theme } from './theme';
4
+ import { buildEffectTree } from '../core/effectTree.js';
5
+ import { ContextMenu } from './ContextMenu.js';
6
+ import { EffectTreeView } from './EffectTreeView.js';
7
+ import { iconStyle } from './icons.js';
8
+ import { buttonStyle, theme } from './theme.js';
9
9
  const ROW_HEIGHT = 34;
10
10
  export const rowIconButtonStyle = {
11
11
  flexShrink: 0,
@@ -1,4 +1,4 @@
1
- import { EffectBinding } from '../core/binding';
1
+ import { EffectBinding } from '../core/binding.js';
2
2
  /**
3
3
  * Sub Emitters module for the currently selected system. The spawned target lives as a
4
4
  * child system in the effect hierarchy — edit it by selecting it there (Unity pattern).
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { EmitSubParticleSystem, SubParticleEmitMode } from 'babylon.quarks';
3
- import { findBehavior } from '../core/colors';
4
- import { createChildSystem } from '../core/systems';
5
- import { ModuleSection } from './ModuleSection';
6
- import { CheckboxField, NumberField, Row, SelectField } from './fields';
7
- import { theme } from './theme';
3
+ import { findBehavior } from '../core/colors.js';
4
+ import { createChildSystem } from '../core/systems.js';
5
+ import { ModuleSection } from './ModuleSection.js';
6
+ import { CheckboxField, NumberField, Row, SelectField } from './fields.js';
7
+ import { theme } from './theme.js';
8
8
  /**
9
9
  * Sub Emitters module for the currently selected system. The spawned target lives as a
10
10
  * child system in the effect hierarchy — edit it by selecting it there (Unity pattern).
@@ -0,0 +1,24 @@
1
+ import type { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture.js';
2
+ import type { TextureOption } from './modules.js';
3
+ /** The url a texture-like object was loaded from, or '' for none. */
4
+ export declare function textureUrlOf(texture: unknown): string;
5
+ /**
6
+ * Picks a texture for one slot: a host preset, a custom URL, a file from disk, or none. The
7
+ * current texture stays selectable when it is not among the presets. Used by the Renderer
8
+ * module for the map and for the ramp, mask and noise of the texture effects.
9
+ */
10
+ export declare function TextureSelect(props: {
11
+ /** The slot's current texture (anything with a `url`), or null for none. */
12
+ value: unknown;
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;
18
+ /** Label of the "none" entry. Default "(none)". */
19
+ noneLabel?: string;
20
+ /** Shows a thumbnail of the current texture under the select. Default false. */
21
+ preview?: boolean;
22
+ /** Title of the custom URL prompt. Default "Texture URL". */
23
+ promptTitle?: string;
24
+ }): import("react").JSX.Element;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { SelectField } from './fields.js';
4
+ import { PromptDialog } from './PromptDialog.js';
5
+ /** The url a texture-like object was loaded from, or '' for none. */
6
+ export function textureUrlOf(texture) {
7
+ return texture?.url ?? '';
8
+ }
9
+ /**
10
+ * Picks a texture for one slot: a host preset, a custom URL, a file from disk, or none. The
11
+ * current texture stays selectable when it is not among the presets. Used by the Renderer
12
+ * module for the map and for the ramp, mask and noise of the texture effects.
13
+ */
14
+ export function TextureSelect(props) {
15
+ const currentUrl = textureUrlOf(props.value);
16
+ const options = props.textureOptions ?? [];
17
+ const isPreset = options.some((o) => o.url === currentUrl);
18
+ const [prompting, setPrompting] = useState(false);
19
+ const apply = (url) => {
20
+ const texture = props.loadTexture(url);
21
+ if (texture) {
22
+ props.onChange(texture);
23
+ }
24
+ };
25
+ return (_jsxs(_Fragment, { children: [_jsx(SelectField, { value: !currentUrl ? '__none' : isPreset ? currentUrl : '__current', options: [
26
+ { value: '__none', label: props.noneLabel ?? '(none)' },
27
+ ...(currentUrl && !isPreset
28
+ ? [{ value: '__current', label: `(current) ${currentUrl.slice(-24)}` }]
29
+ : []),
30
+ ...options.map((o) => ({ value: o.url, label: o.label })),
31
+ { value: '__url', label: 'Custom URL…' },
32
+ { value: '__file', label: 'Load from file…' },
33
+ ], onChange: (url) => {
34
+ if (url === '__current') {
35
+ return;
36
+ }
37
+ if (url === '__none') {
38
+ props.onChange(null);
39
+ return;
40
+ }
41
+ if (url === '__url') {
42
+ setPrompting(true);
43
+ return;
44
+ }
45
+ if (url === '__file') {
46
+ const input = document.createElement('input');
47
+ input.type = 'file';
48
+ input.accept = 'image/*';
49
+ input.onchange = () => {
50
+ const file = input.files?.[0];
51
+ if (!file) {
52
+ return;
53
+ }
54
+ const reader = new FileReader();
55
+ reader.onload = () => apply(reader.result);
56
+ reader.readAsDataURL(file);
57
+ };
58
+ input.click();
59
+ return;
60
+ }
61
+ apply(url);
62
+ } }), prompting && (_jsx(PromptDialog, { title: props.promptTitle ?? 'Texture URL', defaultValue: currentUrl, placeholder: "https://\u2026", onSubmit: (url) => {
63
+ apply(url);
64
+ setPrompting(false);
65
+ }, onCancel: () => setPrompting(false) })), props.preview && currentUrl && (_jsx("img", { src: currentUrl, alt: "texture preview", style: {
66
+ width: 64,
67
+ height: 64,
68
+ marginTop: 6,
69
+ objectFit: 'contain',
70
+ borderRadius: 6,
71
+ border: '1px solid #2b3761',
72
+ background: 'repeating-conic-gradient(#2a3252 0% 25%, #171d33 0% 50%) 0 0 / 12px 12px',
73
+ } }))] }));
74
+ }
@@ -0,0 +1,14 @@
1
+ import type { TextureUV } from 'babylon.quarks';
2
+ /**
3
+ * Rows for how a texture is laid over the particle (`TextureUV`): its UV channel, then
4
+ * tiling and offset as Unity's material Tiling / Offset, then a scroll as Unreal's Panner.
5
+ * One field for the map, the erosion mask and the distortion noise alike.
6
+ */
7
+ export declare function TextureUVField(props: {
8
+ /** The complete layout as the system holds it. */
9
+ value: TextureUV;
10
+ /** Called with the whole layout, one field changed. */
11
+ onChange: (next: TextureUV) => void;
12
+ /** Prefix of each row's label, so several layouts in one module stay apart. Default none. */
13
+ labelPrefix?: string;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { NumberField, Row, SelectField } from './fields.js';
3
+ /** A `[u, v]` pair of number inputs. */
4
+ function PairField(props) {
5
+ return (_jsx("div", { style: { display: 'flex', gap: 6 }, children: [0, 1].map((i) => (_jsx(NumberField, { value: props.value[i], step: props.step, onChange: (v) => {
6
+ const next = [props.value[0], props.value[1]];
7
+ next[i] = v;
8
+ props.onChange(next);
9
+ } }, i))) }));
10
+ }
11
+ /**
12
+ * Rows for how a texture is laid over the particle (`TextureUV`): its UV channel, then
13
+ * tiling and offset as Unity's material Tiling / Offset, then a scroll as Unreal's Panner.
14
+ * One field for the map, the erosion mask and the distortion noise alike.
15
+ */
16
+ export function TextureUVField(props) {
17
+ const prefix = props.labelPrefix ?? '';
18
+ const patch = (fields) => props.onChange({ ...props.value, ...fields });
19
+ return (_jsxs(_Fragment, { children: [_jsx(Row, { label: `${prefix}UV channel`, hintKey: "UV channel", children: _jsx(SelectField, { value: props.value.channel, options: [
20
+ { value: 0, label: 'UV0 (sprite / mesh UV)' },
21
+ { value: 1, label: 'UV1 (mesh second set, along a trail)' },
22
+ ], onChange: (channel) => patch({ channel }) }) }), _jsx(Row, { label: `${prefix}Tiling`, hintKey: "Tiling", children: _jsx(PairField, { value: props.value.tiling, step: 0.5, onChange: (tiling) => patch({ tiling }) }) }), _jsx(Row, { label: `${prefix}Offset`, hintKey: "Offset", children: _jsx(PairField, { value: props.value.offset, step: 0.05, onChange: (offset) => patch({ offset }) }) }), _jsx(Row, { label: `${prefix}Scroll`, hintKey: "Scroll", children: _jsx(PairField, { value: props.value.scroll, step: 0.05, onChange: (scroll) => patch({ scroll }) }) })] }));
23
+ }
@@ -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 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
- export type { PlaybackState } from '../core/playback';
5
+ import type { EffectBinding } from '../core/binding.js';
6
+ import { type PlaybackState } from '../core/playback.js';
7
+ export type { PlaybackState } from '../core/playback.js';
8
8
  export interface TimelinePanelProps {
9
9
  binding: EffectBinding;
10
10
  selectedNode: TransformNode | null;
@@ -12,8 +12,8 @@ export interface TimelinePanelProps {
12
12
  scene: Scene;
13
13
  renderer: BatchedRenderer;
14
14
  playbackRef: React.MutableRefObject<PlaybackState>;
15
- playheadRef: React.RefObject<HTMLDivElement>;
16
- counterRef: React.RefObject<HTMLSpanElement>;
15
+ playheadRef: React.RefObject<HTMLDivElement | null>;
16
+ counterRef: React.RefObject<HTMLSpanElement | null>;
17
17
  stagePlaying: boolean;
18
18
  setStagePlaying: (playing: boolean) => void;
19
19
  speed: number;
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon, RectangleGroupIcon } from '@heroicons/react/24/solid';
3
3
  import React, { useMemo, useRef, useState } from 'react';
4
- import { groupNodes } from '../core/groups';
5
- import { clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, } from '../core/playback';
6
- import { pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled } from '../core/scrub';
7
- import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
8
- import { PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback } from '../core/transport';
9
- import { TimelineTrackRow } from './TimelineTrackRow';
10
- import { iconStyle } from './icons';
11
- import { buttonStyle, theme } from './theme';
4
+ import { groupNodes } from '../core/groups.js';
5
+ import { clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, } from '../core/playback.js';
6
+ import { pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled } from '../core/scrub.js';
7
+ import { computeTimelineRows, computeTimelineSpan } from '../core/timeline.js';
8
+ import { PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback } from '../core/transport.js';
9
+ import { TimelineTrackRow } from './TimelineTrackRow.js';
10
+ import { iconStyle } from './icons.js';
11
+ import { buttonStyle, theme } from './theme.js';
12
12
  const LABEL_WIDTH = 200;
13
13
  const RULER_HEIGHT = 22;
14
14
  const PANEL_HEIGHT = 220;
@@ -90,7 +90,9 @@ export function TimelinePanel(props) {
90
90
  observer.observe(el);
91
91
  return () => observer.disconnect();
92
92
  }, []);
93
- const rows = useMemo(() => computeTimelineRows(binding), [binding, binding.getRevision()]);
93
+ const revision = binding.getRevision();
94
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the binding changes in place; its revision says when to recompute
95
+ const rows = useMemo(() => computeTimelineRows(binding), [binding, revision]);
94
96
  const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
95
97
  const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET - RIGHT_PADDING) / timelineSpan;
96
98
  const anyLooping = useMemo(() => rows.some((r) => r.kind === 'track' && r.looping), [rows]);
@@ -1,7 +1,7 @@
1
- import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import React from 'react';
3
- import type { EffectBinding } from '../core/binding';
4
- import type { TimelineRow } from '../core/timeline';
3
+ import type { EffectBinding } from '../core/binding.js';
4
+ import type { TimelineRow } from '../core/timeline.js';
5
5
  export interface TimelineTrackRowProps {
6
6
  row: TimelineRow;
7
7
  binding: EffectBinding;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { CheckIcon, ChevronDownIcon, ChevronRightIcon, EyeIcon, EyeSlashIcon, PencilSquareIcon, PlusIcon, SparklesIcon, SquaresPlusIcon, XMarkIcon, } from '@heroicons/react/24/solid';
3
3
  import { useState } from 'react';
4
- import { buildEffectTree, collectSystems } from '../core/effectTree';
5
- import { ungroupNode } from '../core/groups';
6
- import { createChildSystem, removeSystems } from '../core/systems';
7
- import { PromptDialog } from './PromptDialog';
8
- import { iconStyle } from './icons';
9
- import { theme } from './theme';
4
+ import { buildEffectTree, collectSystems } from '../core/effectTree.js';
5
+ import { ungroupNode } from '../core/groups.js';
6
+ import { createChildSystem, removeSystems } from '../core/systems.js';
7
+ import { PromptDialog } from './PromptDialog.js';
8
+ import { iconStyle } from './icons.js';
9
+ import { theme } from './theme.js';
10
10
  const iconButton = {
11
11
  background: 'none',
12
12
  border: 'none',
@@ -1,15 +1,15 @@
1
- import type { Scene } from '@babylonjs/core/scene';
1
+ import type { Scene } from '@babylonjs/core/scene.js';
2
2
  import type { BatchedRenderer } from 'babylon.quarks';
3
3
  import React from 'react';
4
- import type { EffectBinding } from '../core/binding';
5
- import type { PlaybackState } from '../core/playback';
4
+ import type { EffectBinding } from '../core/binding.js';
5
+ import type { PlaybackState } from '../core/playback.js';
6
6
  export interface TransportBarProps {
7
7
  binding: EffectBinding | null;
8
8
  scene: Scene;
9
9
  renderer: BatchedRenderer;
10
10
  playbackRef: React.MutableRefObject<PlaybackState>;
11
11
  /** The host's render loop writes time and particle count here. */
12
- counterRef: React.RefObject<HTMLSpanElement>;
12
+ counterRef: React.RefObject<HTMLSpanElement | null>;
13
13
  stagePlaying: boolean;
14
14
  setStagePlaying: (playing: boolean) => void;
15
15
  speed: number;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon } from '@heroicons/react/24/solid';
3
- import { PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback } from '../core/transport';
4
- import { iconStyle } from './icons';
5
- import { buttonStyle, theme } from './theme';
3
+ import { PLAYBACK_SPEEDS, restartFocusPlayback, toggleStagePlayback } from '../core/transport.js';
4
+ import { iconStyle } from './icons.js';
5
+ import { buttonStyle, theme } from './theme.js';
6
6
  const barButton = {
7
7
  ...buttonStyle,
8
8
  width: 28,
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ScalarGenerator } from '../core/values';
2
+ import { ScalarGenerator } from '../core/values.js';
3
3
  export declare function ValueField(props: {
4
4
  label: string;
5
5
  hint?: string;
@@ -1,7 +1,7 @@
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';
3
- import { CurveEditor } from './CurveEditor';
4
- import { NumberField, Row, SelectField } from './fields';
2
+ import { 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
  * Unity-like value field: Constant | Random Between Two Constants | Curve,
7
7
  * bound to a quarks value generator.
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
- import { EffectBinding } from '../core/binding';
3
- import type { TextureOption } from './modules';
2
+ import { EffectBinding } from '../core/binding.js';
3
+ import type { TextureOption } from './modules.js';
4
4
  interface ModuleProps {
5
5
  binding: EffectBinding;
6
6
  }
@@ -9,6 +9,8 @@ export declare function LimitSpeedOverLifeModule({ binding }: ModuleProps): Reac
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
+ /** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
13
+ export declare function AttractorModule({ binding }: ModuleProps): React.JSX.Element;
12
14
  export declare function GravityModule({ binding }: ModuleProps): React.JSX.Element;
13
15
  export declare function RotationOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
14
16
  /** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
@@ -1,16 +1,16 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
3
- import { ApplyCollision, ApplyForce, ApplySequences, AxisAngleGenerator, Bezier, ChangeEmitDirection, ColorBySpeed, ConstantValue, EulerGenerator, ForceOverLife, FrameOverLife, getPhysicsResolver, Gradient, InheritVelocity, IntervalValue, LimitSpeedOverLife, Noise, OrbitOverLife, PiecewiseBezier, RandomQuatGenerator, Rotation3DOverLife, RotationBySpeed, RotationOverLife, SizeBySpeed, SpeedOverLife, TextureSequencer, TurbulenceField, Vector3, VelocityOverLife, WidthOverLength, } from 'babylon.quarks';
3
+ import { ApplyCollision, ApplyForce, ApplySequences, AxisAngleGenerator, Bezier, ChangeEmitDirection, ColorBySpeed, ConstantValue, EulerGenerator, ForceOverLife, FrameOverLife, Gradient, GravityForce, InheritVelocity, IntervalValue, LimitSpeedOverLife, Noise, OrbitOverLife, PiecewiseBezier, RandomQuatGenerator, Rotation3DOverLife, RotationBySpeed, RotationOverLife, SizeBySpeed, SpeedOverLife, TextureSequencer, TurbulenceField, Vector3, VelocityOverLife, WidthOverLength, } from 'babylon.quarks';
4
4
  import React from 'react';
5
- import { buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops } from '../core/colors';
6
- import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values';
7
- import { CurveEditor } from './CurveEditor';
8
- import { CheckboxField, NumberField, Row, SelectField } from './fields';
9
- import { GradientEditor } from './GradientEditor';
10
- import { iconStyle } from './icons';
11
- import { ModuleSection } from './ModuleSection';
12
- import { theme } from './theme';
13
- import { ValueField } from './ValueField';
5
+ import { buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops } from '../core/colors.js';
6
+ import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values.js';
7
+ import { CurveEditor } from './CurveEditor.js';
8
+ import { CheckboxField, NumberField, Row, SelectField } from './fields.js';
9
+ import { GradientEditor } from './GradientEditor.js';
10
+ import { iconStyle } from './icons.js';
11
+ import { ModuleSection } from './ModuleSection.js';
12
+ import { theme } from './theme.js';
13
+ import { ValueField } from './ValueField.js';
14
14
  function removeBehaviorOfType(behaviors, type) {
15
15
  for (let i = behaviors.length - 1; i >= 0; i--) {
16
16
  if (behaviors[i].type === type) {
@@ -35,7 +35,10 @@ export function LimitSpeedOverLifeModule({ binding }) {
35
35
  return (_jsx(BehaviorModule, { binding: binding, title: "Limit Speed over Lifetime", type: "LimitSpeedOverLife", create: () => new LimitSpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1] }), 0.5), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 8, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) }), _jsx(Row, { label: "Dampen", children: _jsx(NumberField, { value: behavior.dampen, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.dampen = Math.min(1, v))) }) })] })) }));
36
36
  }
37
37
  export function ForceOverLifeModule({ binding }) {
38
- return (_jsx(BehaviorModule, { binding: binding, title: "Force over Lifetime", type: "ForceOverLife", create: () => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "force.x", label: "X", generator: behavior.x, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.x = g)) }), _jsx(ValueField, { hintKey: "force.y", label: "Y", generator: behavior.y, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.y = g)) }), _jsx(ValueField, { hintKey: "force.z", label: "Z", generator: behavior.z, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.z = g)) })] })) }));
38
+ return (_jsx(BehaviorModule, { binding: binding, title: "Force over Lifetime", type: "ForceOverLife", create: () => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
39
+ { value: 'world', label: 'World' },
40
+ { value: 'local', label: 'Local' },
41
+ ], onChange: (v) => binding.apply(() => (behavior.space = v)) }) }), _jsx(ValueField, { hintKey: "force.x", label: "X", generator: behavior.x, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.x = g)) }), _jsx(ValueField, { hintKey: "force.y", label: "Y", generator: behavior.y, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.y = g)) }), _jsx(ValueField, { hintKey: "force.z", label: "Z", generator: behavior.z, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.z = g)) })] })) }));
39
42
  }
40
43
  export function VelocityOverLifeModule({ binding }) {
41
44
  return (_jsx(BehaviorModule, { binding: binding, title: "Velocity over Lifetime", type: "VelocityOverLife", create: () => new VelocityOverLife(new ConstantValue(0), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
@@ -49,8 +52,15 @@ export function InheritVelocityModule({ binding }) {
49
52
  { value: 'current', label: 'Current' },
50
53
  ], onChange: (v) => binding.apply(() => (behavior.mode = v)) }) }), _jsx(ValueField, { label: "Multiplier", generator: behavior.multiplier, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.multiplier = g)) })] })) }));
51
54
  }
55
+ /** A point attractor (`GravityForce`): particles fall towards a point, harder the nearer they are. */
56
+ export function AttractorModule({ binding }) {
57
+ return (_jsx(BehaviorModule, { binding: binding, title: "Attractor", type: "GravityForce", create: () => new GravityForce(new Vector3(0, 0, 0), 1), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Center", hintKey: "attractor.center", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: ['x', 'y', 'z'].map((axis) => (_jsx(NumberField, { value: behavior.center[axis], step: 0.1, onChange: (v) => binding.apply(() => (behavior.center[axis] = v)) }, axis))) }) }), _jsx(Row, { label: "Strength", hintKey: "attractor.magnitude", children: _jsx(NumberField, { value: behavior.magnitude, step: 0.1, onChange: (v) => binding.apply(() => (behavior.magnitude = v)) }) })] })) }));
58
+ }
52
59
  export function GravityModule({ binding }) {
53
- return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsx(Row, { label: "Strength", hintKey: "gravity.strength", children: _jsx(NumberField, { value: readScalar(behavior.magnitude).value, step: 0.5, onChange: (v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v))) }) })) }));
60
+ return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Strength", hintKey: "gravity.strength", children: _jsx(NumberField, { value: readScalar(behavior.magnitude).value, step: 0.5, onChange: (v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v))) }) }), _jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
61
+ { value: 'world', label: 'World' },
62
+ { value: 'local', label: 'Local' },
63
+ ], onChange: (v) => binding.apply(() => (behavior.space = v)) }) })] })) }));
54
64
  }
55
65
  export function RotationOverLifeModule({ binding }) {
56
66
  return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime", type: "RotationOverLife", create: () => new RotationOverLife(new ConstantValue(Math.PI)), children: (behavior) => (_jsx(ValueField, { hintKey: "rotationOverLife.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) })) }));
@@ -142,15 +152,22 @@ function Vector3Row(props) {
142
152
  return (_jsx(Row, { label: props.label, hintKey: props.hintKey, children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: props.value.x, min: props.min, step: props.step, onChange: (n) => set('x', n) }), _jsx(NumberField, { value: props.value.y, min: props.min, step: props.step, onChange: (n) => set('y', n) }), _jsx(NumberField, { value: props.value.z, min: props.min, step: props.step, onChange: (n) => set('z', n) })] }) }));
143
153
  }
144
154
  export function TurbulenceModule({ binding }) {
145
- return (_jsx(BehaviorModule, { binding: binding, title: "Turbulence", type: "TurbulenceField", create: () => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { hintKey: "turbulence.scale", label: "Scale", value: behavior.scale, min: 0.01, step: 0.1, onChange: (v) => binding.apply(() => (behavior.scale = v)) }), _jsx(Row, { label: "Octaves", children: _jsx(NumberField, { value: behavior.octaves, min: 1, step: 1, onChange: (v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v)))) }) }), _jsx(Vector3Row, { hintKey: "turbulence.strength", label: "Strength", value: behavior.velocityMultiplier, step: 0.5, onChange: (v) => binding.apply(() => (behavior.velocityMultiplier = v)) }), _jsx(Vector3Row, { label: "Time scale", value: behavior.timeScale, step: 0.1, onChange: (v) => binding.apply(() => (behavior.timeScale = v)) })] })) }));
155
+ return (_jsx(BehaviorModule, { binding: binding, title: "Turbulence", type: "TurbulenceField", create: () => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { hintKey: "turbulence.scale", label: "Scale", value: behavior.scale, min: 0.01, step: 0.1, onChange: (v) => binding.apply(() => (behavior.scale = v)) }), _jsx(Row, { label: "Octaves", children: _jsx(NumberField, { value: behavior.octaves, min: 1, step: 1, onChange: (v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v)))) }) }), _jsx(Vector3Row, { hintKey: "turbulence.strength", label: "Strength", value: behavior.velocityMultiplier, step: 0.5, onChange: (v) => binding.apply(() => (behavior.velocityMultiplier = v)) }), _jsx(Vector3Row, { label: "Time scale", value: behavior.timeScale, step: 0.1, onChange: (v) => binding.apply(() => (behavior.timeScale = v)) }), _jsx(Row, { label: "Seed", children: _jsx(NumberField, { value: behavior.seed, step: 1, onChange: (v) => binding.apply(() => {
156
+ const seed = Math.round(v);
157
+ const next = new TurbulenceField(behavior.scale, behavior.octaves, behavior.velocityMultiplier, behavior.timeScale, seed);
158
+ behavior.seed = seed;
159
+ behavior.generator = next.generator;
160
+ behavior.initialOffset.copy(next.initialOffset);
161
+ behavior.timeOffset.copy(next.initialOffset);
162
+ }) }) })] })) }));
146
163
  }
147
164
  export function EmitDirectionModule({ binding }) {
148
165
  return (_jsx(BehaviorModule, { binding: binding, title: "Randomize Direction", type: "ChangeEmitDirection", create: () => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6)), children: (behavior) => (_jsx(ValueField, { label: "Angle (rad)", generator: behavior.angle, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.angle = g)) })) }));
149
166
  }
150
167
  export function CollisionModule({ binding }) {
151
168
  // Like Unity: Collision stores bounce only. Colliders are host-provided via
152
- // setPhysicsResolver — the editor never invents a ground plane.
153
- return (_jsx(BehaviorModule, { binding: binding, title: "Collision", type: "ApplyCollision", create: () => new ApplyCollision(getPhysicsResolver(), 0.5), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Bounce", children: _jsx(NumberField, { value: behavior.bounce, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.bounce = v)) }) }), _jsx("div", { style: { fontSize: 11, opacity: 0.6, marginTop: 4 }, children: "Bounce is saved with the effect. Colliders come from the host (setPhysicsResolver) \u2014 without one, Collision is inert." })] })) }));
169
+ // setPhysicsResolver, asked when a particle is tested — the editor never invents a ground plane.
170
+ return (_jsx(BehaviorModule, { binding: binding, title: "Collision", type: "ApplyCollision", create: () => new ApplyCollision(undefined, 0.5), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Bounce", children: _jsx(NumberField, { value: behavior.bounce, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.bounce = v)) }) }), _jsx("div", { style: { fontSize: 11, opacity: 0.6, marginTop: 4 }, children: "Bounce is saved with the effect. Colliders come from the host (setPhysicsResolver) \u2014 without one, Collision is inert." })] })) }));
154
171
  }
155
172
  /** Shared speed-range rows for the "by Speed" trio. */
156
173
  function SpeedRangeRows({ binding, behavior }) {
@@ -173,6 +190,11 @@ export function OrbitOverLifeModule({ binding }) {
173
190
  export function WidthOverTrailModule({ binding }) {
174
191
  return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.width), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.width = buildCurve(pieces))) }) }), _jsx(Row, { label: "Absolute width", children: _jsx(CheckboxField, { value: behavior.absolute, onChange: (v) => binding.apply(() => (behavior.absolute = v)) }) })] })) }));
175
192
  }
193
+ /** How many of the first frames a start-frame generator picks from: its exclusive upper bound. */
194
+ function startFrameCount(startFrame) {
195
+ const state = readScalar(startFrame);
196
+ return Math.round(state.mode === 'random' ? state.max : state.value);
197
+ }
176
198
  export function TextureSheetModule({ binding }) {
177
199
  const system = binding.system;
178
200
  const frameBehavior = findBehavior(system.behaviors, 'FrameOverLife');
@@ -225,5 +247,19 @@ export function TextureSheetModule({ binding }) {
225
247
  curve: [0, (tiles - 1) / 3, (2 * (tiles - 1)) / 3, tiles - 1],
226
248
  })));
227
249
  }
228
- }) }) }), frameBehavior && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(frameBehavior.frame), maxValue: Math.max(1, tiles - 1), onChange: (pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces))) }) }))] }));
250
+ }) }) }), frameBehavior && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Time mode", children: _jsx(SelectField, { value: frameBehavior.fps > 0 ? 'fps' : 'life', options: [
251
+ { value: 'life', label: 'Lifetime (curve over life)' },
252
+ { value: 'fps', label: 'FPS (frame from curve, then by age)' },
253
+ ], onChange: (mode) => binding.apply(() => {
254
+ frameBehavior.fps = mode === 'fps' ? 12 : 0;
255
+ if (mode === 'fps' && frameBehavior.frameCount <= 0) {
256
+ frameBehavior.frameCount = tiles;
257
+ }
258
+ }) }) }), frameBehavior.fps > 0 ? (_jsxs(_Fragment, { children: [_jsx(Row, { label: "FPS", children: _jsx(NumberField, { value: frameBehavior.fps, min: 0.01, step: 1, onChange: (v) => binding.apply(() => (frameBehavior.fps = v)) }) }), _jsx(Row, { label: "Loop", children: _jsx(CheckboxField, { value: frameBehavior.loop, onChange: (v) => binding.apply(() => (frameBehavior.loop = v)) }) }), _jsx(Row, { label: "Random frame", children: _jsx(CheckboxField, { value: frameBehavior.random, onChange: (v) => binding.apply(() => (frameBehavior.random = v)) }) })] })) : (_jsx(Row, { label: "Cycles", children: _jsx(NumberField, { value: frameBehavior.cycles, min: 0.01, step: 1, onChange: (v) => binding.apply(() => (frameBehavior.cycles = v)) }) })), _jsx(Row, { label: "Frame count", children: _jsx(NumberField, { value: frameBehavior.frameCount, min: frameBehavior.fps > 0 || frameBehavior.startFrame ? 1 : 0, max: tiles, step: 1, onChange: (v) => binding.apply(() => (frameBehavior.frameCount = Math.max(0, Math.round(v)))) }) }), _jsxs(Row, { label: "Start frame", children: [_jsx(CheckboxField, { value: !!frameBehavior.startFrame, onChange: (v) => binding.apply(() => {
259
+ frameBehavior.startFrame = v ? new IntervalValue(0, tiles) : undefined;
260
+ // The offset only wraps into a segment; without one it runs off the sheet.
261
+ if (v && frameBehavior.frameCount <= 0) {
262
+ frameBehavior.frameCount = tiles;
263
+ }
264
+ }) }), frameBehavior.startFrame && (_jsx(NumberField, { value: startFrameCount(frameBehavior.startFrame), min: 1, max: tiles, step: 1, onChange: (v) => binding.apply(() => (frameBehavior.startFrame = new IntervalValue(0, Math.max(1, Math.round(v))))) }))] }), _jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(frameBehavior.frame), maxValue: Math.max(1, tiles - 1), onChange: (pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces))) }) })] }))] }));
229
265
  }
@@ -11,6 +11,7 @@ export declare function NumberField(props: {
11
11
  onChange: (value: number) => void;
12
12
  step?: number;
13
13
  min?: number;
14
+ max?: number;
14
15
  }): React.JSX.Element;
15
16
  export declare function CheckboxField(props: {
16
17
  value: boolean;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState } from 'react';
3
- import { InspectorLabel } from './InspectorLabel';
4
- import { inputStyle, rowStyle } from './theme';
3
+ import { InspectorLabel } from './InspectorLabel.js';
4
+ import { inputStyle, rowStyle } from './theme.js';
5
5
  export function Row(props) {
6
6
  return (_jsxs("div", { style: rowStyle, children: [_jsx(InspectorLabel, { label: props.label, hint: props.hint, hintKey: props.hintKey }), props.children] }));
7
7
  }
@@ -14,7 +14,7 @@ export function NumberField(props) {
14
14
  setText(String(round(props.value)));
15
15
  }
16
16
  }, [props.value]);
17
- return (_jsx("input", { type: "number", className: "qe-hover", style: inputStyle, value: text, step: props.step ?? 0.1, min: props.min, onFocus: () => {
17
+ return (_jsx("input", { type: "number", className: "qe-hover", style: inputStyle, value: text, step: props.step ?? 0.1, min: props.min, max: props.max, onFocus: () => {
18
18
  focusedRef.current = true;
19
19
  }, onBlur: () => {
20
20
  focusedRef.current = false;