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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,140 @@ All notable changes to the `babylon.quarks-editor` package are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project follows the `quarks.core` 0.x version line.
6
6
 
7
+ ## [0.28.0] — 2026-10-23
8
+
9
+ ### Breaking
10
+
11
+ - **Texture callbacks are typed.** `resolveTexture` returns `Texture | null | undefined` instead
12
+ of `unknown` (on `EffectEditorHost`, `QuarksEffectEditor.Show`, `EffectEditor`,
13
+ `RendererModule` and `createDefaultEffect`). `TextureSelect`'s `loadTexture` and `onChange` use
14
+ `Texture`, and `RendererMaterialPatch` types `texture` as `Texture | null` and
15
+ `reflectionAtlas` as `BaseTexture | null`. A resolver that returns a Babylon `Texture` needs no
16
+ change; one typed to return something else must now return a `Texture`.
17
+ - **`ValueField` has two prop shapes.** `CurveValueFieldProps` (the default, `curve` unset or
18
+ `true`) calls `onChange` with a `ScalarGenerator`; `ConstantValueFieldProps` (`curve: false`)
19
+ calls it with a `ValueGenerator`. Code that passed `curve={false}` and expected a curve back no
20
+ longer type-checks.
21
+
22
+ ### Added
23
+
24
+ - A **Render face** field (Both, Front, Back) for mesh systems, editing the library's new
25
+ `renderFace`.
26
+ - **`createEditorSession`** and the `EditorSession` interface, in a new
27
+ `babylon.quarks-editor/session` entry: the scene, camera, gizmos, history, scene entries, assets,
28
+ prefab mode and project save/load without React. The host passes only the view callbacks it
29
+ needs; none runs after `dispose()`, which also drops a project load or an import still in
30
+ flight. `EffectEditorHost` is now a view over a session. The session has its own entry because
31
+ it builds the viewport (grid, gizmos, markers), which the UI-free `.` entry does not load.
32
+ - **`createEngine`** on `EffectEditorHostProps` (and `Show()`), to draw with a WebGPU or other
33
+ engine instead of the default WebGL `Engine`. It may return a promise, so a `WebGPUEngine` can
34
+ finish `initAsync()` first.
35
+ - Exported types that public signatures already used: `AssetLoadProgress`, `EffectAsset`,
36
+ `AssetDragPayload`, `SceneEntry`, `TimelineRow` (`TimelineGroupRow | TimelineSystemRow`),
37
+ `AnyGenerator`, `ModuleProps`, `ConstantValueFieldProps` and `CurveValueFieldProps`, and
38
+ `EditorProject` from `./react`.
39
+ - TSDoc on every public export. `npm run docs:check` now builds the editor's API docs and fails on
40
+ an undocumented export or a broken link.
41
+
42
+ ### Fixed
43
+
44
+ - A mesh from geometry without indices (a preset or a loaded GLB) no longer leaves the system
45
+ with no index buffer: it gets sequential indices, so it draws and exports. A mesh without UVs or
46
+ normals gets the library's fallback ones instead of keeping the previous mesh's.
47
+ - Randomize Direction's angle no longer offers a curve. The angle is sampled once per particle,
48
+ so a curve could not apply, and a curve loaded from JSON shows as its starting value.
49
+ - `EffectEditorHost` under React StrictMode, or remounted, no longer lets the first mount's project
50
+ load fill the live editor with entries from a disposed scene.
51
+ - The transport's play resumes what pause froze and no longer replays effects or bursts that had
52
+ already finished (it uses the library's new `resume()`, now that `play()` replays a finished
53
+ system).
54
+ - A node deleted by an undo while selected hands the selection back to its effect's root on the
55
+ next render. The host only checked when the selection or the session changed, so the inspector
56
+ and timeline could keep showing the deleted node.
57
+
58
+ ### Internal
59
+
60
+ - The asset import checks every field it reads from an effect file, with no `any`.
61
+ - The React panels follow the React Compiler's rules (eslint-plugin-react-hooks 7): no ref
62
+ written during render, and state that follows a prop is adjusted during render instead of in an
63
+ effect, which saves a render each time.
64
+ - `EffectEditorHost` is split into a session and panel components under `src/react/host/`, and
65
+ every `as never` cast is gone. The session and the host have their own tests, and the React
66
+ code has a coverage floor.
67
+
68
+ ## [0.27.1] — 2026-10-23 (not published)
69
+
70
+ ### Changed
71
+
72
+ - **Peer ranges:** `@babylonjs/core` and `@babylonjs/materials` are `^9.0.0` instead of
73
+ `>=9.0.0` (no Babylon 10 exists yet, so no install that worked stops working). `babylon.quarks` stays `~0.27.0`: the editor follows the library's minor line, and
74
+ the release check now enforces it.
75
+ - The `exports` map lists `types` first and exports `./package.json`. The README says the package
76
+ is ES modules only.
77
+
78
+ ## [0.27.0] — 2026-10-23
79
+
80
+ A 0.27.0 dated 2026-10-21 was prepared but never published; its entries are folded in here.
81
+
82
+ ### Added
83
+
84
+ - Main module: "Warm-up time", shown while Prewarm is on, the seconds of the cycle simulated
85
+ before the first frame (`prewarmTime`; 0 warms up a whole cycle).
86
+ - **The Renderer module reaches the 0.25–0.26 texture features.** Map layout (UV channel,
87
+ tiling, offset, scroll), Flip, a Gradient map section (ramp and channel), an Erosion section
88
+ (mask with its own layout, softness, edge width and colour) and a UV distortion section (noise
89
+ with its layout, strength). The ramp, mask and noise are picked the way the texture is: from
90
+ the host's presets, a URL or a file.
91
+ - Renderer: a swept trail's side axis. A host geometry
92
+ (`GeometryOption`, `resolveGeometry`) may carry a second UV set, `uvs2`, which mesh particles
93
+ read on UV channel 1; the demo's glTF loader passes `TEXCOORD_1` through.
94
+ - Main: Scaling mode for world-space systems (shape only, or the hierarchy's motion too).
95
+ - Texture Sheet Animation: time mode (lifetime or FPS), FPS, loop or hold, random frame, cycles,
96
+ frame count and a random start frame.
97
+ - Attractor module: a point gravity (`GravityForce`) with its centre and strength.
98
+ - Emitter shape wireframes for the Box and Edge shapes: the box outline with its +Z emit
99
+ direction, and the edge line with its +Y emit direction.
100
+ - `TextureSelect` and `TextureUVField` components, exported from `babylon.quarks-editor/react`.
101
+ - Renderer module, trails: "Tessellation" and "Curve tension".
102
+
103
+ ### Changed
104
+
105
+ - Renderer, trails: "Texture mode" (stretch, distribute per segment, tile, repeat per segment),
106
+ "Texture scale", "Min vertex distance" and "Trail time", each left unset at its default so the
107
+ JSON stays sparse. The "Head width scale" row is gone: it wrote a `headWidthScale` field no
108
+ runtime read (the head's width comes from the Width over Trail module).
109
+ - **`TimelinePanelProps` and `TransportBarProps` take `RefObject<HTMLElement | null>` refs**, the
110
+ type `useRef<HTMLElement>(null)` returns, now that the editor compiles with `strict`. Code that
111
+ passed the old type still compiles.
112
+
113
+ ### Fixed
114
+
115
+ - An imported effect whose images were inlined from the picked files is saved with those inlined
116
+ images; `serializeEffect` writes a texture's source path only while the texture still shows the
117
+ image it was loaded with, and an inlined image has no path. A test covers import → scene → save.
118
+ - **Saving lost mesh nodes and texture settings.** The editor now saves through `serializeEffect`:
119
+ mesh nodes keep their geometry (so mesh surface emitters still emit after a save and reload),
120
+ and textures keep their wrap, sampling, repeat, offset and rotation. Textures with no url a file
121
+ can point at are reported instead of written as an empty entry.
122
+ - The yellow hover ring stayed on the ground after the hovered effect was removed from the scene.
123
+ - **Node ESM and webpack 5 could not import the package.** `dist` was compiled with relative and
124
+ `@babylonjs` deep imports that had no file extension. Every import now ends in `.js`.
125
+ - The `babylon.quarks` peer range was `>=0.17.0`, but the editor needs current APIs (beam mode,
126
+ rim fade, culling and sort settings). It is now `~0.27.0`.
127
+
128
+ ### Internal
129
+
130
+ - Jest tests now cover the whole editor core (`src/core`): systems, material, timeline, scrub,
131
+ playback and transport, groups, collision, effect tree, assets and asset tree, scene entries,
132
+ gallery layout, playback, markers and picker, emitter shape wireframes, texture export and the
133
+ IndexedDB project store. The coverage threshold for the editor is raised to match.
134
+ - Editor tests resolve `babylon.quarks` to its sources, so they pick up library changes made in
135
+ the same branch without rebuilding it.
136
+ - React modules are tested in jsdom through the DOM, and the whole `EffectEditor` is mounted in
137
+ every render mode.
138
+ - The README leads with `QuarksEffectEditor.Show`, lists every peer and every module, and the
139
+ `EffectEditor` snippet carries all of its props.
140
+
7
141
  ## [0.26.0] — 2026-10-21
8
142
 
9
143
  ### Added
package/README.md CHANGED
@@ -7,11 +7,33 @@ Shuriken-style effect editor for [babylon.quarks](https://www.npmjs.com/package/
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install babylon.quarks-editor babylon.quarks @babylonjs/core react @heroicons/react @tanstack/react-virtual
10
+ npm install babylon.quarks-editor babylon.quarks @babylonjs/core @babylonjs/materials react react-dom @heroicons/react @tanstack/react-virtual
11
11
  ```
12
12
 
13
+ The editor is an ES module package (`import`, not `require`). Its version follows babylon.quarks:
14
+ editor 0.28.x needs babylon.quarks 0.28.x.
15
+
13
16
  ## Usage
14
17
 
18
+ The supported embed is the host: it owns the scene, the timeline, undo/redo, playback and the
19
+ module inspector, and mounts into any element.
20
+
21
+ ```ts
22
+ import {Texture} from '@babylonjs/core/Materials/Textures/texture';
23
+ import {QuarksEffectEditor} from 'babylon.quarks-editor/react';
24
+
25
+ const editor = QuarksEffectEditor.Show({
26
+ hostElement: document.getElementById('editor')!,
27
+ effectJson, // optional: a Quarks JSON export to open; a default effect otherwise
28
+ textureOptions: [{label: 'Glow', url: '/textures/glow.png'}],
29
+ resolveTexture: (url, scene) => new Texture(url, scene),
30
+ onSave: (json) => save(json), // the effect as Quarks JSON, round-trips through QuarksLoader
31
+ });
32
+ editor.dispose();
33
+ ```
34
+
35
+ The module inspector on its own, for a host that already has a scene and a selection:
36
+
15
37
  ```tsx
16
38
  import {EffectBinding, EffectHistory} from 'babylon.quarks-editor';
17
39
  import {EffectEditor} from 'babylon.quarks-editor/react';
@@ -20,13 +42,25 @@ const binding = new EffectBinding(particleSystem); // your babylon.quarks Partic
20
42
  const history = new EffectHistory();
21
43
  history.attach(binding);
22
44
 
23
- <EffectEditor binding={binding} />;
45
+ <EffectEditor
46
+ binding={binding}
47
+ selectedSystem={binding.system}
48
+ gizmoTargetNode={binding.system.emitter}
49
+ gizmoTargetLabel={binding.system.emitter.name}
50
+ />;
24
51
 
25
52
  // Serialize to Quarks JSON (round-trips through QuarksLoader):
26
53
  const json = binding.exportJSON('MyEffect');
27
54
  ```
28
55
 
29
- Modules: Main, Emission (bursts), Shape (7 emitter types), Speed/Limit Speed/Force/Rotation over Lifetime, Gravity, Color over Lifetime (gradient), Size over Lifetime (curve), Noise, Sub Emitters, Texture Sheet Animation, Renderer. Multi-system hierarchy with add/remove/rename, undo/redo via `EffectHistory`.
56
+ Modules, in Unity's order: Main, Emission (rate over time and distance, bursts), Shape (cone,
57
+ sphere, hemisphere, circle, donut, rectangle, grid, box, edge, mesh surface, point), Randomize
58
+ Direction, Velocity / Speed / Limit Speed / Force over Lifetime, Inherit Velocity, Gravity,
59
+ Attractor, Orbit over Lifetime, Color over Lifetime and by Speed, Size over Lifetime and by Speed,
60
+ Rotation over Lifetime (2D and 3D) and by Speed, Noise, Turbulence, Collision, Width over Trail,
61
+ Sub Emitters, Texture Sheet Animation, Sequences, Renderer (render modes incl. trail, mesh and
62
+ beam; material, texture layout, flip, gradient map, erosion, UV distortion, soft particles, sorting).
63
+ Multi-system hierarchy with add/remove/rename, undo/redo via `EffectHistory`.
30
64
 
31
65
  The `.` export is UI-free (edit model, serialization, value/shape/gradient helpers) — usable headless. The `./react` export ships the components; React is a peer dependency, so the same UI can mount in a standalone page or inside a [BabylonJS Editor](https://github.com/BabylonJS/Editor) plugin.
32
66
 
@@ -35,22 +69,25 @@ The `.` export is UI-free (edit model, serialization, value/shape/gradient helpe
35
69
  The editor never loads assets itself — it defers to host-supplied callbacks so it doesn't need
36
70
  an opinion on your CDN, asset pipeline, or loader (`@babylonjs/loaders`, your own backend, etc.):
37
71
 
38
- - `resolveTexture(url) => Texture-like` — turns a picked URL or `data:` URI (from "Custom URL…" /
39
- "Load from file…" in the Renderer module) into whatever object you assign as `system.texture`.
72
+ - `resolveTexture(url) => Texture | null | undefined` — turns a picked URL or `data:` URI (from
73
+ "Custom URL…" / "Load from file…" in the Renderer module) into the Babylon `Texture` assigned as
74
+ `system.texture`. Return `null` or `undefined` to leave the texture unset.
40
75
  - `resolveGeometry(file) => Promise<GeometryData>` — turns a picked file (from "Load from file…"
41
76
  in the Mesh render mode's geometry dropdown) into vertex buffers: `{positions, indices?, uvs?,
42
- normals?}` (typed arrays or plain number arrays).
77
+ uvs2?, normals?}` (typed arrays or plain number arrays; `uvs2` is the second UV set textures on
78
+ UV channel 1 read).
43
79
  - `textureOptions?: {label, url}[]` / `geometryOptions?: {label, positions, indices?, uvs?,
44
- normals?}[]` — preset lists shown alongside "Custom URL…" / "Load from file…".
80
+ uvs2?, normals?}[]` — preset lists shown alongside "Custom URL…" / "Load from file…". The texture
81
+ presets also serve the gradient map's ramp, the erosion mask and the distortion noise.
45
82
 
46
83
  **Signature differs by entry point** — a common source of "works in the demo, throws in my
47
84
  host" bugs:
48
85
 
49
86
  - `QuarksEffectEditor.Show(...)` / `<EffectEditorHost>` call your callbacks with the live
50
- `Scene` as a second argument: `resolveTexture: (url, scene) => unknown`,
87
+ `Scene` as a second argument: `resolveTexture: (url, scene) => Texture | null | undefined`,
51
88
  `resolveGeometry: (file, scene) => Promise<GeometryData>`.
52
89
  - The lower-level `<EffectEditor>` (no Host wrapper) does **not** pass a scene:
53
- `resolveTexture: (url) => unknown`, `resolveGeometry: (file) => Promise<GeometryData>`. If you
90
+ `resolveTexture: (url) => Texture | null | undefined`, `resolveGeometry: (file) => Promise<GeometryData>`. If you
54
91
  copy a `Show()`-style resolver that reads its `scene` parameter into a raw `<EffectEditor>`,
55
92
  `scene` is `undefined` and your resolver throws.
56
93
 
@@ -79,6 +116,29 @@ resolveGeometry: async (file, scene) => {
79
116
  },
80
117
  ```
81
118
 
119
+ ## Custom engines and headless use
120
+
121
+ `<EffectEditorHost>` creates a WebGL `Engine` on its canvas. Pass `createEngine` to build another
122
+ engine on that canvas instead, or a `NullEngine` in tests. It may return a promise:
123
+
124
+ ```ts
125
+ createEngine: async (canvas) => {
126
+ const engine = new WebGPUEngine(canvas);
127
+ await engine.initAsync();
128
+ return engine;
129
+ },
130
+ ```
131
+
132
+ Build a new engine on every call: the host disposes it when it unmounts, and React StrictMode
133
+ mounts the host twice.
134
+
135
+ Everything the host does besides drawing its panels lives in an `EditorSession`, from the
136
+ `babylon.quarks-editor/session` entry: `createEditorSession({engine, playback, view, ...})`. The
137
+ session owns the scene, camera, gizmos, history, scene entries, assets, prefab mode and project
138
+ save/load, and reports changes through the `view` callbacks you pass. Use it to drive the editor
139
+ from your own UI, or to script and test editing without React. It builds the viewport (grid,
140
+ gizmos, markers), which is why it is not part of the lighter UI-free `.` entry.
141
+
82
142
  ## License
83
143
 
84
144
  MIT
@@ -1,4 +1,4 @@
1
- import type { AssetDragPayload } from './assetTree';
1
+ import type { AssetDragPayload } from './assetTree.js';
2
2
  /** Stores the current asset drag payload for viewport preview (getData is drop-only in most browsers). */
3
3
  export declare function setAssetDragSession(payload: AssetDragPayload | null): void;
4
4
  export declare function getAssetDragSession(): AssetDragPayload | null;
@@ -1,4 +1,4 @@
1
- import type { EffectAsset } from './assets';
1
+ import type { EffectAsset } from './assets.js';
2
2
  export interface AssetTreeFolder {
3
3
  kind: 'folder';
4
4
  name: string;
@@ -19,11 +19,16 @@ export declare function folderChildren(tree: AssetTreeNode[], path: string): Ass
19
19
  /** Assets whose name or path contains `query`, across every folder, sorted by name. */
20
20
  export declare function searchAssets(assets: EffectAsset[], query: string): EffectAsset[];
21
21
  export declare const ASSET_DRAG_MIME = "application/x-quarks-asset-drop";
22
+ /** What is dragged out of the Assets panel: one asset, or every asset under a folder. */
22
23
  export type AssetDragPayload = {
24
+ /** One asset. */
23
25
  kind: 'asset';
26
+ /** The asset's id. */
24
27
  id: number;
25
28
  } | {
29
+ /** A folder. */
26
30
  kind: 'folder';
31
+ /** The folder's path; every asset under it is dragged. */
27
32
  path: string;
28
33
  };
29
34
  export declare function writeAssetDrag(dataTransfer: DataTransfer, payload: AssetDragPayload): void;
@@ -4,7 +4,9 @@
4
4
  * fresh, independent copy (see `instantiateSceneEntry`).
5
5
  */
6
6
  export interface EffectAsset {
7
+ /** The asset's id in this session; scene entries made from it keep it. */
7
8
  id: number;
9
+ /** The asset's display name, its file name without `.json`. */
8
10
  name: string;
9
11
  /** Relative path inside the imported folder (preserves subfolders). */
10
12
  path: string;
@@ -13,10 +15,15 @@ export interface EffectAsset {
13
15
  /** Texture/geometry files the effect references that were not among the imported files. */
14
16
  missingFiles: string[];
15
17
  }
18
+ /** How far an asset import got. */
16
19
  export interface AssetLoadProgress {
20
+ /** JSON files read so far. */
17
21
  done: number;
22
+ /** JSON files in the import. */
18
23
  total: number;
24
+ /** Effects found so far. */
19
25
  loaded: number;
26
+ /** Files that could not be read. */
20
27
  failed: number;
21
28
  }
22
29
  export declare function relativePathOf(file: File): string;
@@ -42,6 +42,9 @@ function readAsDataUrl(file) {
42
42
  reader.readAsDataURL(file);
43
43
  });
44
44
  }
45
+ function isRecord(value) {
46
+ return value !== null && typeof value === 'object';
47
+ }
45
48
  /** True for a Quarks effect envelope with at least one particle emitter. */
46
49
  export function isEffectJson(json) {
47
50
  const object = json?.object;
@@ -206,11 +209,11 @@ export function assetIconUrl(asset) {
206
209
  if (cached !== undefined) {
207
210
  return cached;
208
211
  }
209
- const json = asset.json;
212
+ const json = isRecord(asset.json) ? asset.json : {};
210
213
  const findMaterial = (node) => {
211
- if (!node || typeof node !== 'object')
214
+ if (!isRecord(node))
212
215
  return undefined;
213
- if (node.type === 'ParticleEmitter' && node.ps?.material)
216
+ if (node.type === 'ParticleEmitter' && isRecord(node.ps) && node.ps.material)
214
217
  return node.ps.material;
215
218
  for (const child of Array.isArray(node.children) ? node.children : []) {
216
219
  const found = findMaterial(child);
@@ -219,10 +222,11 @@ export function assetIconUrl(asset) {
219
222
  }
220
223
  return undefined;
221
224
  };
222
- const byUuid = (list, uuid) => (Array.isArray(list) ? list : []).find((item) => item?.uuid === uuid);
223
- const material = byUuid(json?.materials, findMaterial(json?.object));
224
- const texture = byUuid(json?.textures, material?.map ?? material?.texture);
225
- const image = byUuid(json?.images, texture?.image) ?? (Array.isArray(json?.images) ? json.images[0] : undefined);
225
+ const byUuid = (list, uuid) => (Array.isArray(list) ? list : []).find((item) => isRecord(item) && item.uuid === uuid);
226
+ const material = byUuid(json.materials, findMaterial(json.object));
227
+ const texture = byUuid(json.textures, material?.map ?? material?.texture);
228
+ const firstImage = Array.isArray(json.images) ? json.images[0] : undefined;
229
+ const image = byUuid(json.images, texture?.image) ?? (isRecord(firstImage) ? firstImage : undefined);
226
230
  const url = typeof image?.url === 'string' && image.url ? image.url : null;
227
231
  iconCache.set(asset, url);
228
232
  return url;
@@ -1,6 +1,7 @@
1
- import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import type { ParticleSystem } from 'babylon.quarks';
3
- import type { EffectTreeNode } from './effectTree';
3
+ import type { EffectTreeNode } from './effectTree.js';
4
+ /** Called after every change made through an {@link EffectBinding}. */
4
5
  export type EditorListener = () => void;
5
6
  /**
6
7
  * Headless edit-model over a live ParticleSystem: mutate through apply() so every
@@ -20,11 +21,18 @@ export declare class EffectBinding {
20
21
  constructor(system: ParticleSystem, subSystems?: ParticleSystem[], root?: TransformNode);
21
22
  /** Editor hierarchy tree (groups + emitters) rooted at this effect's scene root. */
22
23
  getTree(): EffectTreeNode;
24
+ /** Calls `listener` after every change; returns the call that stops it. */
23
25
  subscribe(listener: EditorListener): () => void;
24
26
  /** Monotonic change counter, usable as a React external-store snapshot. */
25
27
  getRevision(): number;
28
+ /**
29
+ * Changes the main system through `mutate`: takes an undo snapshot first, then notifies
30
+ * subscribers. Every inspector edit goes through here.
31
+ */
26
32
  apply(mutate: (system: ParticleSystem) => void): void;
33
+ /** Adds a system to the effect's sub systems. */
27
34
  addSubSystem(sub: ParticleSystem): void;
35
+ /** Removes a system from the effect's sub systems; a system it does not have is ignored. */
28
36
  removeSubSystem(sub: ParticleSystem): void;
29
37
  /**
30
38
  * Swaps which system is "main" — used when the current `system` is about to be removed
@@ -39,6 +47,7 @@ export declare class EffectBinding {
39
47
  */
40
48
  markChanged(): void;
41
49
  private notify;
50
+ /** Restarts every system of the effect from time 0. */
42
51
  restart(): void;
43
52
  /**
44
53
  * Serializes the effect into the Quarks JSON envelope understood by QuarksLoader,
@@ -1,4 +1,4 @@
1
- import { buildEffectTree, serializeEffectTree } from './effectTree';
1
+ import { buildEffectTree, serializeEffectTree } from './effectTree.js';
2
2
  /**
3
3
  * Headless edit-model over a live ParticleSystem: mutate through apply() so every
4
4
  * change notifies subscribers (React UI, save indicators, hosts like BabylonJS Editor).
@@ -17,6 +17,7 @@ export class EffectBinding {
17
17
  getTree() {
18
18
  return buildEffectTree(this.root);
19
19
  }
20
+ /** Calls `listener` after every change; returns the call that stops it. */
20
21
  subscribe(listener) {
21
22
  this.listeners.add(listener);
22
23
  return () => this.listeners.delete(listener);
@@ -25,15 +26,21 @@ export class EffectBinding {
25
26
  getRevision() {
26
27
  return this.revision;
27
28
  }
29
+ /**
30
+ * Changes the main system through `mutate`: takes an undo snapshot first, then notifies
31
+ * subscribers. Every inspector edit goes through here.
32
+ */
28
33
  apply(mutate) {
29
34
  this.onBeforeChange?.();
30
35
  mutate(this.system);
31
36
  this.notify();
32
37
  }
38
+ /** Adds a system to the effect's sub systems. */
33
39
  addSubSystem(sub) {
34
40
  this.subSystems.push(sub);
35
41
  this.notify();
36
42
  }
43
+ /** Removes a system from the effect's sub systems; a system it does not have is ignored. */
37
44
  removeSubSystem(sub) {
38
45
  const index = this.subSystems.indexOf(sub);
39
46
  if (index >= 0) {
@@ -69,6 +76,7 @@ export class EffectBinding {
69
76
  listener();
70
77
  }
71
78
  }
79
+ /** Restarts every system of the effect from time 0. */
72
80
  restart() {
73
81
  // onlyUsedByOther systems skip self-emission in update(); play() only unpauses sim.
74
82
  for (const system of [this.system, ...this.subSystems]) {
@@ -8,7 +8,11 @@ import { Vector3 } from 'babylon.quarks';
8
8
  */
9
9
  export declare class GroundPlaneResolver {
10
10
  y: number;
11
+ /** @param y - Height of the ground plane. */
11
12
  constructor(y?: number);
13
+ /**
14
+ * True when `position` is at or below the plane, with the plane's normal written to `normal`.
15
+ */
12
16
  resolve(position: Vector3, normal: Vector3): boolean;
13
17
  }
14
18
  /**
@@ -7,9 +7,13 @@ import { getPhysicsResolver, setPhysicsResolver } from 'babylon.quarks';
7
7
  * which is exactly what ApplyCollision.update reflects the velocity against.
8
8
  */
9
9
  export class GroundPlaneResolver {
10
+ /** @param y - Height of the ground plane. */
10
11
  constructor(y = 0) {
11
12
  this.y = y;
12
13
  }
14
+ /**
15
+ * True when `position` is at or below the plane, with the plane's normal written to `normal`.
16
+ */
13
17
  resolve(position, normal) {
14
18
  if (position.y <= this.y) {
15
19
  normal.set(0, 1, 0);
@@ -2,28 +2,44 @@ import type { Behavior } from 'babylon.quarks';
2
2
  import { ColorOverLife, Gradient, Vector4 } from 'babylon.quarks';
3
3
  /** One RGBA stop of the editable gradient (all channels 0..1, pos 0..1). */
4
4
  export interface GradientStop {
5
+ /** Where the stop sits over the particle's life, 0..1. */
5
6
  pos: number;
7
+ /** Red, 0..1. */
6
8
  r: number;
9
+ /** Green, 0..1. */
7
10
  g: number;
11
+ /** Blue, 0..1. */
8
12
  b: number;
13
+ /** Alpha, 0..1. */
9
14
  a: number;
10
15
  }
16
+ /** The gradient a new Color over Lifetime starts with: warm yellow fading out through red. */
11
17
  export declare const DEFAULT_GRADIENT_STOPS: GradientStop[];
18
+ /** A quarks gradient through `stops`, sorted by position; colour and alpha share the stops. */
12
19
  export declare function buildGradient(stops: GradientStop[]): Gradient;
20
+ /** A Color over Lifetime behavior through `stops`. */
13
21
  export declare function buildColorOverLife(stops: GradientStop[]): ColorOverLife;
22
+ /** An RGBA colour as a quarks `Vector4`. */
14
23
  export declare function rgbaToVector4(stop: {
15
24
  r: number;
16
25
  g: number;
17
26
  b: number;
18
27
  a: number;
19
28
  }): Vector4;
29
+ /** A stop's colour as CSS `rgba()`. */
20
30
  export declare function stopToCss(stop: GradientStop): string;
31
+ /** The stops as a left-to-right CSS `linear-gradient`, for a gradient preview. */
21
32
  export declare function stopsToCssGradient(stops: GradientStop[]): string;
33
+ /** A `#rrggbb` colour as channels 0..1. */
22
34
  export declare function hexToRgb(hex: string): {
35
+ /** Red, 0..1. */
23
36
  r: number;
37
+ /** Green, 0..1. */
24
38
  g: number;
39
+ /** Blue, 0..1. */
25
40
  b: number;
26
41
  };
42
+ /** Channels 0..1, clamped, as a `#rrggbb` colour. */
27
43
  export declare function rgbToHex(r: number, g: number, b: number): string;
28
44
  /** Reads RGBA stops back from a Gradient (union of color/alpha key positions). */
29
45
  export declare function readGradientStops(gradient: Gradient): GradientStop[];
@@ -1,27 +1,34 @@
1
1
  import { ColorOverLife, Gradient, Vector3, Vector4 } from 'babylon.quarks';
2
+ /** The gradient a new Color over Lifetime starts with: warm yellow fading out through red. */
2
3
  export const DEFAULT_GRADIENT_STOPS = [
3
4
  { pos: 0, r: 1, g: 0.85, b: 0.4, a: 1 },
4
5
  { pos: 1, r: 1, g: 0.25, b: 0.05, a: 0 },
5
6
  ];
7
+ /** A quarks gradient through `stops`, sorted by position; colour and alpha share the stops. */
6
8
  export function buildGradient(stops) {
7
9
  const sorted = [...stops].sort((a, b) => a.pos - b.pos);
8
10
  return new Gradient(sorted.map((s) => [new Vector3(s.r, s.g, s.b), s.pos]), sorted.map((s) => [s.a, s.pos]));
9
11
  }
12
+ /** A Color over Lifetime behavior through `stops`. */
10
13
  export function buildColorOverLife(stops) {
11
14
  return new ColorOverLife(buildGradient(stops));
12
15
  }
16
+ /** An RGBA colour as a quarks `Vector4`. */
13
17
  export function rgbaToVector4(stop) {
14
18
  return new Vector4(stop.r, stop.g, stop.b, stop.a);
15
19
  }
20
+ /** A stop's colour as CSS `rgba()`. */
16
21
  export function stopToCss(stop) {
17
22
  const c = (v) => Math.round(v * 255);
18
23
  return `rgba(${c(stop.r)}, ${c(stop.g)}, ${c(stop.b)}, ${stop.a.toFixed(2)})`;
19
24
  }
25
+ /** The stops as a left-to-right CSS `linear-gradient`, for a gradient preview. */
20
26
  export function stopsToCssGradient(stops) {
21
27
  const sorted = [...stops].sort((a, b) => a.pos - b.pos);
22
28
  const parts = sorted.map((s) => `${stopToCss(s)} ${(s.pos * 100).toFixed(1)}%`);
23
29
  return `linear-gradient(90deg, ${parts.join(', ')})`;
24
30
  }
31
+ /** A `#rrggbb` colour as channels 0..1. */
25
32
  export function hexToRgb(hex) {
26
33
  const value = hex.replace('#', '');
27
34
  return {
@@ -30,6 +37,7 @@ export function hexToRgb(hex) {
30
37
  b: parseInt(value.slice(4, 6), 16) / 255,
31
38
  };
32
39
  }
40
+ /** Channels 0..1, clamped, as a `#rrggbb` colour. */
33
41
  export function rgbToHex(r, g, b) {
34
42
  const c = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255)
35
43
  .toString(16)
@@ -1,11 +1,12 @@
1
- import type { Scene } from '@babylonjs/core/scene';
1
+ import type { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
2
+ import type { Scene } from '@babylonjs/core/scene.js';
2
3
  import { ParticleSystem } from 'babylon.quarks';
3
- import { type SceneEntry } from './sceneEntries';
4
+ import { type SceneEntry } from './sceneEntries.js';
4
5
  export interface CreateSceneDefaultOptions {
5
- resolveTexture?: (url: string, scene: Scene) => unknown;
6
+ resolveTexture?: (url: string, scene: Scene) => Texture | null | undefined;
6
7
  textureUrl?: string;
7
8
  }
8
9
  /** Creates a simple cone burst preset for new editor effects. */
9
- export declare function createDefaultEffect(scene: Scene, resolveTexture?: (url: string, scene: Scene) => unknown, textureUrl?: string): ParticleSystem;
10
+ export declare function createDefaultEffect(scene: Scene, resolveTexture?: (url: string, scene: Scene) => Texture | null | undefined, textureUrl?: string): ParticleSystem;
10
11
  /** Creates a preset effect as a new scene entry (systems not registered with the renderer yet). */
11
12
  export declare function createSceneDefaultEntry(scene: Scene, existing: SceneEntry[], options?: CreateSceneDefaultOptions): SceneEntry;
@@ -1,7 +1,7 @@
1
- import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
1
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import { ColorOverLife, ConeEmitter, ConstantColor, ConstantValue, IntervalValue, ParticleSystem, RenderMode, Vector4, } from 'babylon.quarks';
3
- import { DEFAULT_GRADIENT_STOPS, buildGradient } from './colors';
4
- import { uniqueEntryName } from './sceneEntries';
3
+ import { DEFAULT_GRADIENT_STOPS, buildGradient } from './colors.js';
4
+ import { uniqueEntryName } from './sceneEntries.js';
5
5
  /** Creates a simple cone burst preset for new editor effects. */
6
6
  export function createDefaultEffect(scene, resolveTexture, textureUrl) {
7
7
  const system = new ParticleSystem({
@@ -15,7 +15,7 @@ export function createDefaultEffect(scene, resolveTexture, textureUrl) {
15
15
  emissionOverTime: new ConstantValue(80),
16
16
  shape: new ConeEmitter({ radius: 0.3, angle: 0.25 }),
17
17
  renderMode: RenderMode.BillBoard,
18
- texture: (textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined),
18
+ texture: textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined,
19
19
  transparent: true,
20
20
  blendMode: 1,
21
21
  });
@@ -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
  /** Marks meshes/nodes created only for editor interaction or overlays (not part of the effect). */
3
3
  export declare const EDITOR_SCENE_NODE_METADATA: {
4
4
  quarksEditorSceneNode: true;