babylon.quarks-editor 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +69 -9
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +6 -1
  5. package/dist/core/assets.d.ts +7 -0
  6. package/dist/core/assets.js +11 -7
  7. package/dist/core/binding.d.ts +11 -2
  8. package/dist/core/binding.js +9 -1
  9. package/dist/core/collision.d.ts +4 -0
  10. package/dist/core/collision.js +4 -0
  11. package/dist/core/colors.d.ts +16 -0
  12. package/dist/core/colors.js +8 -0
  13. package/dist/core/defaultEffect.d.ts +5 -4
  14. package/dist/core/defaultEffect.js +4 -4
  15. package/dist/core/editorScene.d.ts +1 -1
  16. package/dist/core/editorSession.d.ts +229 -0
  17. package/dist/core/editorSession.js +983 -0
  18. package/dist/core/effectTree.d.ts +6 -1
  19. package/dist/core/effectTree.js +13 -88
  20. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  21. package/dist/core/emitterShapeWireframe.js +45 -13
  22. package/dist/core/galleryDropMarker.d.ts +2 -2
  23. package/dist/core/galleryDropMarker.js +6 -6
  24. package/dist/core/galleryLayout.d.ts +3 -3
  25. package/dist/core/galleryLayout.js +1 -1
  26. package/dist/core/galleryPlayback.d.ts +1 -1
  27. package/dist/core/galleryPlayback.js +2 -1
  28. package/dist/core/galleryScene.d.ts +4 -4
  29. package/dist/core/galleryScene.js +3 -3
  30. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  31. package/dist/core/gallerySelectionMarker.js +5 -5
  32. package/dist/core/groups.d.ts +1 -1
  33. package/dist/core/groups.js +1 -1
  34. package/dist/core/history.d.ts +7 -1
  35. package/dist/core/history.js +4 -0
  36. package/dist/core/indexedDbProjectStore.d.ts +14 -1
  37. package/dist/core/indexedDbProjectStore.js +13 -0
  38. package/dist/core/lineShaders.d.ts +2 -2
  39. package/dist/core/lineShaders.js +2 -2
  40. package/dist/core/loadEffect.d.ts +2 -2
  41. package/dist/core/loadEffect.js +1 -1
  42. package/dist/core/material.d.ts +12 -2
  43. package/dist/core/pickAssetFiles.js +1 -1
  44. package/dist/core/playback.d.ts +6 -0
  45. package/dist/core/projectStore.d.ts +26 -0
  46. package/dist/core/projectStore.js +1 -0
  47. package/dist/core/sceneEntries.d.ts +7 -4
  48. package/dist/core/sceneEntries.js +2 -2
  49. package/dist/core/scrub.d.ts +2 -2
  50. package/dist/core/scrub.js +2 -2
  51. package/dist/core/shapes.d.ts +25 -1
  52. package/dist/core/shapes.js +5 -0
  53. package/dist/core/systems.d.ts +4 -2
  54. package/dist/core/systems.js +3 -1
  55. package/dist/core/textureExport.d.ts +1 -1
  56. package/dist/core/timeline.d.ts +25 -5
  57. package/dist/core/transport.d.ts +3 -3
  58. package/dist/core/transport.js +2 -2
  59. package/dist/core/values.d.ts +21 -4
  60. package/dist/core/values.js +5 -0
  61. package/dist/core/viewportPicker.d.ts +4 -4
  62. package/dist/core/viewportPicker.js +8 -7
  63. package/dist/index.d.ts +25 -21
  64. package/dist/index.js +12 -12
  65. package/dist/react/AssetsPanel.d.ts +9 -2
  66. package/dist/react/AssetsPanel.js +6 -6
  67. package/dist/react/ChoiceDialog.d.ts +7 -0
  68. package/dist/react/ChoiceDialog.js +1 -1
  69. package/dist/react/ContextMenu.d.ts +10 -1
  70. package/dist/react/ContextMenu.js +1 -1
  71. package/dist/react/CurveEditor.d.ts +1 -1
  72. package/dist/react/CurveEditor.js +2 -2
  73. package/dist/react/EditorWorkspace.d.ts +9 -2
  74. package/dist/react/EditorWorkspace.js +10 -7
  75. package/dist/react/EffectEditor.d.ts +11 -5
  76. package/dist/react/EffectEditor.js +7 -7
  77. package/dist/react/EffectEditorHost.d.ts +33 -29
  78. package/dist/react/EffectEditorHost.js +247 -1674
  79. package/dist/react/EffectTreeView.d.ts +4 -1
  80. package/dist/react/EffectTreeView.js +3 -3
  81. package/dist/react/GradientEditor.d.ts +1 -1
  82. package/dist/react/GradientEditor.js +3 -3
  83. package/dist/react/InspectorInfoIcon.js +3 -3
  84. package/dist/react/InspectorLabel.js +3 -3
  85. package/dist/react/MessageDialog.d.ts +5 -0
  86. package/dist/react/MessageDialog.js +1 -1
  87. package/dist/react/ModuleSection.d.ts +10 -0
  88. package/dist/react/ModuleSection.js +8 -5
  89. package/dist/react/ObjectModule.d.ts +1 -1
  90. package/dist/react/ObjectModule.js +4 -4
  91. package/dist/react/ProjectsPage.d.ts +7 -1
  92. package/dist/react/ProjectsPage.js +33 -17
  93. package/dist/react/PromptDialog.d.ts +7 -0
  94. package/dist/react/PromptDialog.js +1 -1
  95. package/dist/react/ScenePanel.d.ts +9 -2
  96. package/dist/react/ScenePanel.js +12 -10
  97. package/dist/react/SubEmittersModule.d.ts +1 -1
  98. package/dist/react/SubEmittersModule.js +5 -5
  99. package/dist/react/TextureSelect.d.ts +24 -0
  100. package/dist/react/TextureSelect.js +74 -0
  101. package/dist/react/TextureUVField.d.ts +14 -0
  102. package/dist/react/TextureUVField.js +23 -0
  103. package/dist/react/TimelinePanel.d.ts +20 -7
  104. package/dist/react/TimelinePanel.js +21 -15
  105. package/dist/react/TimelineTrackRow.d.ts +13 -3
  106. package/dist/react/TimelineTrackRow.js +6 -6
  107. package/dist/react/TransportBar.d.ts +13 -4
  108. package/dist/react/TransportBar.js +3 -3
  109. package/dist/react/ValueField.d.ts +32 -5
  110. package/dist/react/ValueField.js +22 -11
  111. package/dist/react/behaviorModules.d.ts +19 -6
  112. package/dist/react/behaviorModules.js +68 -17
  113. package/dist/react/fields.d.ts +9 -0
  114. package/dist/react/fields.js +6 -3
  115. package/dist/react/host/BottomDocks.d.ts +36 -0
  116. package/dist/react/host/BottomDocks.js +63 -0
  117. package/dist/react/host/HostDialogs.d.ts +50 -0
  118. package/dist/react/host/HostDialogs.js +113 -0
  119. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  120. package/dist/react/host/InspectorSidebar.js +59 -0
  121. package/dist/react/host/SceneSidebar.d.ts +53 -0
  122. package/dist/react/host/SceneSidebar.js +62 -0
  123. package/dist/react/host/ViewportPane.d.ts +56 -0
  124. package/dist/react/host/ViewportPane.js +91 -0
  125. package/dist/react/host/hostHooks.d.ts +44 -0
  126. package/dist/react/host/hostHooks.js +164 -0
  127. package/dist/react/host/hostStyles.d.ts +21 -0
  128. package/dist/react/host/hostStyles.js +57 -0
  129. package/dist/react/index.d.ts +42 -38
  130. package/dist/react/index.js +25 -23
  131. package/dist/react/inspectorHints.js +31 -0
  132. package/dist/react/modules.d.ts +19 -6
  133. package/dist/react/modules.js +134 -117
  134. package/dist/react/theme.d.ts +15 -0
  135. package/dist/react/theme.js +15 -0
  136. package/dist/session.d.ts +9 -0
  137. package/dist/session.js +8 -0
  138. package/package.json +18 -11
  139. package/src/core/assetDragSession.ts +1 -1
  140. package/src/core/assetTree.ts +15 -2
  141. package/src/core/assets.ts +25 -10
  142. package/src/core/binding.ts +12 -3
  143. package/src/core/collision.ts +4 -0
  144. package/src/core/colors.ts +21 -1
  145. package/src/core/defaultEffect.ts +8 -7
  146. package/src/core/editorScene.ts +1 -1
  147. package/src/core/editorSession.ts +1299 -0
  148. package/src/core/effectTree.ts +18 -101
  149. package/src/core/emitterShapeWireframe.ts +48 -16
  150. package/src/core/galleryDropMarker.ts +8 -8
  151. package/src/core/galleryLayout.ts +4 -4
  152. package/src/core/galleryPlayback.ts +3 -2
  153. package/src/core/galleryScene.ts +5 -5
  154. package/src/core/gallerySelectionMarker.ts +7 -7
  155. package/src/core/groups.ts +1 -1
  156. package/src/core/history.ts +7 -1
  157. package/src/core/indexedDbProjectStore.ts +14 -1
  158. package/src/core/lineShaders.ts +2 -2
  159. package/src/core/loadEffect.ts +4 -4
  160. package/src/core/material.ts +14 -4
  161. package/src/core/pickAssetFiles.ts +2 -2
  162. package/src/core/playback.ts +6 -0
  163. package/src/core/projectStore.ts +26 -0
  164. package/src/core/sceneEntries.ts +8 -5
  165. package/src/core/scrub.ts +3 -3
  166. package/src/core/shapes.ts +29 -2
  167. package/src/core/systems.ts +11 -4
  168. package/src/core/textureExport.ts +1 -1
  169. package/src/core/timeline.ts +43 -18
  170. package/src/core/transport.ts +4 -4
  171. package/src/core/values.ts +20 -4
  172. package/src/core/viewportPicker.ts +12 -11
  173. package/src/index.ts +25 -21
  174. package/src/react/AssetsPanel.tsx +13 -6
  175. package/src/react/ChoiceDialog.tsx +8 -1
  176. package/src/react/ContextMenu.tsx +11 -2
  177. package/src/react/CurveEditor.tsx +2 -2
  178. package/src/react/EditorWorkspace.tsx +16 -10
  179. package/src/react/EffectEditor.tsx +17 -9
  180. package/src/react/EffectEditorHost.tsx +415 -2354
  181. package/src/react/EffectTreeView.tsx +7 -4
  182. package/src/react/GradientEditor.tsx +3 -3
  183. package/src/react/InspectorInfoIcon.tsx +3 -3
  184. package/src/react/InspectorLabel.tsx +3 -3
  185. package/src/react/MessageDialog.tsx +6 -1
  186. package/src/react/ModuleSection.tsx +15 -4
  187. package/src/react/ObjectModule.tsx +5 -5
  188. package/src/react/ProjectsPage.tsx +44 -17
  189. package/src/react/PromptDialog.tsx +8 -1
  190. package/src/react/ScenePanel.tsx +21 -12
  191. package/src/react/SubEmittersModule.tsx +6 -6
  192. package/src/react/TextureSelect.tsx +118 -0
  193. package/src/react/TextureUVField.tsx +62 -0
  194. package/src/react/TimelinePanel.tsx +40 -21
  195. package/src/react/TimelineTrackRow.tsx +19 -9
  196. package/src/react/TransportBar.tsx +16 -7
  197. package/src/react/ValueField.tsx +60 -16
  198. package/src/react/behaviorModules.tsx +240 -55
  199. package/src/react/fields.tsx +22 -4
  200. package/src/react/host/BottomDocks.tsx +178 -0
  201. package/src/react/host/HostDialogs.tsx +228 -0
  202. package/src/react/host/InspectorSidebar.tsx +147 -0
  203. package/src/react/host/SceneSidebar.tsx +227 -0
  204. package/src/react/host/ViewportPane.tsx +300 -0
  205. package/src/react/host/hostHooks.ts +198 -0
  206. package/src/react/host/hostStyles.ts +65 -0
  207. package/src/react/index.ts +43 -38
  208. package/src/react/inspectorHints.ts +40 -0
  209. package/src/react/modules.tsx +440 -170
  210. package/src/react/theme.ts +15 -0
  211. package/src/session.ts +17 -0
@@ -1,9 +1,12 @@
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
+ /** 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;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ChevronDownIcon, ChevronRightIcon, FolderIcon, SparklesIcon } from '@heroicons/react/24/solid';
3
3
  import { useState } from 'react';
4
- import { buildEffectTree } from '../core/effectTree';
5
- import { iconStyle } from './icons';
6
- import { theme } from './theme';
4
+ import { buildEffectTree } from '../core/effectTree.js';
5
+ import { iconStyle } from './icons.js';
6
+ import { theme } from './theme.js';
7
7
  const ROW_HEIGHT = 26;
8
8
  function TreeRow(props) {
9
9
  const { item, level, indent, selectedNode, collapsed, onToggle, onSelect } = props;
@@ -1,4 +1,4 @@
1
- import { GradientStop } from '../core/colors';
1
+ import { GradientStop } from '../core/colors.js';
2
2
  /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
3
3
  export declare function GradientEditor(props: {
4
4
  stops: GradientStop[];
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from 'react';
3
- import { hexToRgb, rgbToHex, stopsToCssGradient } from '../core/colors';
4
- import { NumberField, Row } from './fields';
5
- import { buttonStyle, theme } from './theme';
3
+ import { hexToRgb, rgbToHex, stopsToCssGradient } from '../core/colors.js';
4
+ import { NumberField, Row } from './fields.js';
5
+ import { buttonStyle, theme } from './theme.js';
6
6
  const BAR_WIDTH = 218;
7
7
  /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
8
8
  export function GradientEditor(props) {
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { InformationCircleIcon } from '@heroicons/react/24/solid';
3
- import { iconStyle } from './icons';
4
- import { theme } from './theme';
5
- import { Tooltip } from './Tooltip';
3
+ import { iconStyle } from './icons.js';
4
+ import { theme } from './theme.js';
5
+ import { Tooltip } from './Tooltip.js';
6
6
  /** Small info icon with a themed inspector tooltip. */
7
7
  export function InspectorInfoIcon(props) {
8
8
  const size = props.size ?? 13;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getInspectorHint } from './inspectorHints';
3
- import { InspectorInfoIcon } from './InspectorInfoIcon';
4
- import { labelStyle } from './theme';
2
+ import { getInspectorHint } from './inspectorHints.js';
3
+ import { InspectorInfoIcon } from './InspectorInfoIcon.js';
4
+ import { labelStyle } from './theme.js';
5
5
  /** Inspector row label with an optional info icon tooltip. */
6
6
  export function InspectorLabel(props) {
7
7
  if (!props.label) {
@@ -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,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { buttonStyle, theme } from './theme';
2
+ import { buttonStyle, theme } from './theme.js';
3
3
  /** Modal for info and error messages — same chrome as PromptDialog, no text input. */
4
4
  export function MessageDialog(props) {
5
5
  const { title, message, confirmLabel = 'OK', onClose } = props;
@@ -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;
@@ -1,11 +1,14 @@
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';
8
- /** Unity-style module foldout with an optional enable checkbox. */
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
+ /**
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,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,12 +1,18 @@
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
+ /** 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
  }
@@ -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
  }
@@ -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,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,20 +1,27 @@
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
+ /** 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,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
- 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';
3
+ import React, { useState } from 'react';
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,
@@ -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 { 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 { Texture } from '@babylonjs/core/Materials/Textures/texture.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 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
+ /** 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,22 +1,35 @@
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
+ /** 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>;
15
- playheadRef: React.RefObject<HTMLDivElement>;
16
- counterRef: React.RefObject<HTMLSpanElement>;
22
+ /** The playhead line, moved every frame without a re-render. */
23
+ playheadRef: React.RefObject<HTMLDivElement | null>;
24
+ /** The time and particle counter in the header, written by the host's render loop. */
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;
@@ -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;
@@ -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);
@@ -90,7 +93,9 @@ export function TimelinePanel(props) {
90
93
  observer.observe(el);
91
94
  return () => observer.disconnect();
92
95
  }, []);
93
- const rows = useMemo(() => computeTimelineRows(binding), [binding, binding.getRevision()]);
96
+ const revision = binding.getRevision();
97
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- the binding changes in place; its revision says when to recompute
98
+ const rows = useMemo(() => computeTimelineRows(binding), [binding, revision]);
94
99
  const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
95
100
  const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET - RIGHT_PADDING) / timelineSpan;
96
101
  const anyLooping = useMemo(() => rows.some((r) => r.kind === 'track' && r.looping), [rows]);
@@ -143,18 +148,19 @@ export function TimelinePanel(props) {
143
148
  renderer,
144
149
  ]);
145
150
  const visibleRows = useMemo(() => {
151
+ const visible = [];
146
152
  let hideUntilDepth = null;
147
- const visible = rows.filter((row) => {
153
+ for (const row of rows) {
148
154
  if (hideUntilDepth !== null) {
149
155
  if (row.depth > hideUntilDepth)
150
- return false;
156
+ continue;
151
157
  hideUntilDepth = null;
152
158
  }
153
159
  if (row.kind === 'group' && collapsedGroups.has(row.node)) {
154
160
  hideUntilDepth = row.depth;
155
161
  }
156
- return true;
157
- });
162
+ visible.push(row);
163
+ }
158
164
  return visible;
159
165
  }, [rows, collapsedGroups]);
160
166
  const toggleCollapse = (node) => {