babylon.quarks-editor 0.27.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 (138) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +31 -4
  3. package/dist/core/assetTree.d.ts +5 -0
  4. package/dist/core/assets.d.ts +7 -0
  5. package/dist/core/assets.js +11 -7
  6. package/dist/core/binding.d.ts +9 -0
  7. package/dist/core/binding.js +8 -0
  8. package/dist/core/collision.d.ts +4 -0
  9. package/dist/core/collision.js +4 -0
  10. package/dist/core/colors.d.ts +16 -0
  11. package/dist/core/colors.js +8 -0
  12. package/dist/core/defaultEffect.d.ts +3 -2
  13. package/dist/core/defaultEffect.js +1 -1
  14. package/dist/core/editorSession.d.ts +229 -0
  15. package/dist/core/editorSession.js +983 -0
  16. package/dist/core/effectTree.d.ts +5 -0
  17. package/dist/core/galleryPlayback.js +2 -1
  18. package/dist/core/history.d.ts +6 -0
  19. package/dist/core/history.js +4 -0
  20. package/dist/core/indexedDbProjectStore.d.ts +13 -0
  21. package/dist/core/indexedDbProjectStore.js +13 -0
  22. package/dist/core/material.d.ts +12 -2
  23. package/dist/core/playback.d.ts +6 -0
  24. package/dist/core/projectStore.d.ts +26 -0
  25. package/dist/core/projectStore.js +1 -0
  26. package/dist/core/sceneEntries.d.ts +3 -0
  27. package/dist/core/scrub.d.ts +1 -1
  28. package/dist/core/scrub.js +2 -2
  29. package/dist/core/shapes.d.ts +24 -0
  30. package/dist/core/shapes.js +5 -0
  31. package/dist/core/systems.d.ts +2 -0
  32. package/dist/core/systems.js +3 -1
  33. package/dist/core/timeline.d.ts +23 -3
  34. package/dist/core/values.d.ts +21 -4
  35. package/dist/core/values.js +5 -0
  36. package/dist/index.d.ts +5 -1
  37. package/dist/react/AssetsPanel.d.ts +7 -0
  38. package/dist/react/ChoiceDialog.d.ts +7 -0
  39. package/dist/react/ContextMenu.d.ts +10 -1
  40. package/dist/react/EditorWorkspace.d.ts +7 -0
  41. package/dist/react/EditorWorkspace.js +6 -3
  42. package/dist/react/EffectEditor.d.ts +8 -2
  43. package/dist/react/EffectEditorHost.d.ts +32 -28
  44. package/dist/react/EffectEditorHost.js +241 -1665
  45. package/dist/react/EffectTreeView.d.ts +3 -0
  46. package/dist/react/MessageDialog.d.ts +5 -0
  47. package/dist/react/ModuleSection.d.ts +10 -0
  48. package/dist/react/ModuleSection.js +4 -1
  49. package/dist/react/ProjectsPage.d.ts +6 -0
  50. package/dist/react/ProjectsPage.js +27 -11
  51. package/dist/react/PromptDialog.d.ts +7 -0
  52. package/dist/react/ScenePanel.d.ts +7 -0
  53. package/dist/react/ScenePanel.js +7 -5
  54. package/dist/react/TextureSelect.d.ts +5 -5
  55. package/dist/react/TimelinePanel.d.ts +13 -0
  56. package/dist/react/TimelinePanel.js +10 -6
  57. package/dist/react/TimelineTrackRow.d.ts +10 -0
  58. package/dist/react/TransportBar.d.ts +9 -0
  59. package/dist/react/ValueField.d.ts +32 -5
  60. package/dist/react/ValueField.js +20 -9
  61. package/dist/react/behaviorModules.d.ts +16 -5
  62. package/dist/react/behaviorModules.js +16 -1
  63. package/dist/react/fields.d.ts +8 -0
  64. package/dist/react/fields.js +3 -0
  65. package/dist/react/host/BottomDocks.d.ts +36 -0
  66. package/dist/react/host/BottomDocks.js +63 -0
  67. package/dist/react/host/HostDialogs.d.ts +50 -0
  68. package/dist/react/host/HostDialogs.js +113 -0
  69. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  70. package/dist/react/host/InspectorSidebar.js +59 -0
  71. package/dist/react/host/SceneSidebar.d.ts +53 -0
  72. package/dist/react/host/SceneSidebar.js +62 -0
  73. package/dist/react/host/ViewportPane.d.ts +56 -0
  74. package/dist/react/host/ViewportPane.js +91 -0
  75. package/dist/react/host/hostHooks.d.ts +44 -0
  76. package/dist/react/host/hostHooks.js +164 -0
  77. package/dist/react/host/hostStyles.d.ts +21 -0
  78. package/dist/react/host/hostStyles.js +57 -0
  79. package/dist/react/index.d.ts +3 -1
  80. package/dist/react/modules.d.ts +17 -6
  81. package/dist/react/modules.js +38 -20
  82. package/dist/react/theme.d.ts +15 -0
  83. package/dist/react/theme.js +15 -0
  84. package/dist/session.d.ts +9 -0
  85. package/dist/session.js +8 -0
  86. package/package.json +17 -11
  87. package/src/core/assetTree.ts +14 -1
  88. package/src/core/assets.ts +25 -10
  89. package/src/core/binding.ts +9 -0
  90. package/src/core/collision.ts +4 -0
  91. package/src/core/colors.ts +21 -1
  92. package/src/core/defaultEffect.ts +4 -3
  93. package/src/core/editorSession.ts +1299 -0
  94. package/src/core/effectTree.ts +5 -0
  95. package/src/core/galleryPlayback.ts +2 -1
  96. package/src/core/history.ts +6 -0
  97. package/src/core/indexedDbProjectStore.ts +13 -0
  98. package/src/core/loadEffect.ts +1 -1
  99. package/src/core/material.ts +14 -4
  100. package/src/core/playback.ts +6 -0
  101. package/src/core/projectStore.ts +26 -0
  102. package/src/core/sceneEntries.ts +3 -0
  103. package/src/core/scrub.ts +2 -2
  104. package/src/core/shapes.ts +28 -1
  105. package/src/core/systems.ts +9 -2
  106. package/src/core/timeline.ts +40 -15
  107. package/src/core/values.ts +20 -4
  108. package/src/index.ts +5 -1
  109. package/src/react/AssetsPanel.tsx +7 -0
  110. package/src/react/ChoiceDialog.tsx +7 -0
  111. package/src/react/ContextMenu.tsx +10 -1
  112. package/src/react/EditorWorkspace.tsx +11 -5
  113. package/src/react/EffectEditor.tsx +7 -1
  114. package/src/react/EffectEditorHost.tsx +406 -2340
  115. package/src/react/EffectTreeView.tsx +3 -0
  116. package/src/react/MessageDialog.tsx +5 -0
  117. package/src/react/ModuleSection.tsx +11 -0
  118. package/src/react/ProjectsPage.tsx +38 -11
  119. package/src/react/PromptDialog.tsx +7 -0
  120. package/src/react/ScenePanel.tsx +14 -5
  121. package/src/react/TextureSelect.tsx +6 -6
  122. package/src/react/TimelinePanel.tsx +23 -6
  123. package/src/react/TimelineTrackRow.tsx +10 -0
  124. package/src/react/TransportBar.tsx +9 -0
  125. package/src/react/ValueField.tsx +58 -14
  126. package/src/react/behaviorModules.tsx +34 -32
  127. package/src/react/fields.tsx +12 -1
  128. package/src/react/host/BottomDocks.tsx +178 -0
  129. package/src/react/host/HostDialogs.tsx +228 -0
  130. package/src/react/host/InspectorSidebar.tsx +147 -0
  131. package/src/react/host/SceneSidebar.tsx +227 -0
  132. package/src/react/host/ViewportPane.tsx +300 -0
  133. package/src/react/host/hostHooks.ts +198 -0
  134. package/src/react/host/hostStyles.ts +65 -0
  135. package/src/react/index.ts +3 -1
  136. package/src/react/modules.tsx +70 -44
  137. package/src/react/theme.ts +15 -0
  138. package/src/session.ts +17 -0
@@ -1,15 +1,27 @@
1
+ /** The editor's colours and font, for hosts that style their own panels to match. */
1
2
  export const theme = {
3
+ /** Background of the side panels and docks. */
2
4
  panelBg: 'linear-gradient(180deg, rgba(12, 18, 38, 0.92), rgba(6, 10, 22, 0.94))',
5
+ /** Background of an inspector module or a framed list. */
3
6
  sectionBg: 'rgba(10, 16, 34, 0.6)',
7
+ /** Borders and separators. */
4
8
  border: '#2a365f',
9
+ /** The border of a selected or switched-on control. */
5
10
  borderActive: '#4f6fbe',
11
+ /** Body text. */
6
12
  text: '#eef3ff',
13
+ /** Labels and secondary text. */
7
14
  textDim: '#b7c6ea',
15
+ /** Highlights, such as a Save button with something to save. */
8
16
  accent: '#9eb9ff',
17
+ /** Background of inputs. */
9
18
  inputBg: 'rgba(7, 11, 22, 0.82)',
19
+ /** The line of curve editors. */
10
20
  curveStroke: '#78a5ff',
21
+ /** The editor's font stack. */
11
22
  font: 'Inter, Segoe UI, Roboto, sans-serif',
12
23
  };
24
+ /** Style of a text or number input. */
13
25
  export const inputStyle = {
14
26
  background: theme.inputBg,
15
27
  border: `1px solid ${theme.border}`,
@@ -22,6 +34,7 @@ export const inputStyle = {
22
34
  boxSizing: 'border-box',
23
35
  fontFamily: theme.font,
24
36
  };
37
+ /** Style of an inspector row: a label column, then the controls. */
25
38
  export const rowStyle = {
26
39
  display: 'grid',
27
40
  gridTemplateColumns: '110px 1fr',
@@ -29,6 +42,7 @@ export const rowStyle = {
29
42
  gap: 8,
30
43
  marginTop: 6,
31
44
  };
45
+ /** Style of an inspector row's label. */
32
46
  export const labelStyle = {
33
47
  color: theme.textDim,
34
48
  fontSize: 12,
@@ -36,6 +50,7 @@ export const labelStyle = {
36
50
  overflow: 'hidden',
37
51
  textOverflow: 'ellipsis',
38
52
  };
53
+ /** Style of a button. */
39
54
  export const buttonStyle = {
40
55
  border: `1px solid #4563ad`,
41
56
  background: 'rgba(28, 48, 98, 0.55)',
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The editor's headless session: scene, camera, gizmos, history, scene entries, assets, prefab
3
+ * mode and project save/load, without React. It builds the whole viewport (grid, gizmos, markers),
4
+ * so it has its own entry point and the UI-free `babylon.quarks-editor` entry stays light.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ export { createEditorSession } from './core/editorSession.js';
9
+ export type { EditorFrameStats, EditorProject, EditorSession, EditorSessionOptions, EditorSessionView, EffectEditorHostHandle, PrefabInfo, } from './core/editorSession.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The editor's headless session: scene, camera, gizmos, history, scene entries, assets, prefab
3
+ * mode and project save/load, without React. It builds the whole viewport (grid, gizmos, markers),
4
+ * so it has its own entry point and the UI-free `babylon.quarks-editor` entry stays light.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ export { createEditorSession } from './core/editorSession.js';
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "babylon.quarks-editor",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./session": {
14
+ "types": "./dist/session.d.ts",
15
+ "import": "./dist/session.js"
12
16
  },
13
17
  "./react": {
14
- "import": "./dist/react/index.js",
15
- "types": "./dist/react/index.d.ts"
16
- }
18
+ "types": "./dist/react/index.d.ts",
19
+ "import": "./dist/react/index.js"
20
+ },
21
+ "./package.json": "./package.json"
17
22
  },
18
23
  "files": [
19
24
  "dist",
@@ -25,7 +30,8 @@
25
30
  "scripts": {
26
31
  "clean": "rimraf dist",
27
32
  "build": "npm run clean && tsc -p tsconfig.json",
28
- "test": "jest"
33
+ "test": "jest",
34
+ "docs:check": "typedoc --out node_modules/.cache/typedoc-check"
29
35
  },
30
36
  "repository": {
31
37
  "type": "git",
@@ -51,11 +57,11 @@
51
57
  },
52
58
  "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
53
59
  "peerDependencies": {
54
- "@babylonjs/core": ">=9.0.0",
55
- "@babylonjs/materials": ">=9.0.0",
60
+ "@babylonjs/core": "^9.0.0",
61
+ "@babylonjs/materials": "^9.0.0",
56
62
  "@heroicons/react": ">=2.0.0",
57
63
  "@tanstack/react-virtual": ">=3.0.0",
58
- "babylon.quarks": "~0.27.0",
64
+ "babylon.quarks": "~0.28.0",
59
65
  "react": ">=18",
60
66
  "react-dom": ">=18"
61
67
  },
@@ -69,6 +75,6 @@
69
75
  "react": "^19.0.0",
70
76
  "react-dom": "^19.0.0",
71
77
  "rimraf": "^6.0.0",
72
- "typescript": "^5.9.3"
78
+ "typescript": "^6.0.3"
73
79
  }
74
80
  }
@@ -85,7 +85,20 @@ export function searchAssets(assets: EffectAsset[], query: string): EffectAsset[
85
85
 
86
86
  export const ASSET_DRAG_MIME = 'application/x-quarks-asset-drop';
87
87
 
88
- export type AssetDragPayload = {kind: 'asset'; id: number} | {kind: 'folder'; path: string};
88
+ /** What is dragged out of the Assets panel: one asset, or every asset under a folder. */
89
+ export type AssetDragPayload =
90
+ | {
91
+ /** One asset. */
92
+ kind: 'asset';
93
+ /** The asset's id. */
94
+ id: number;
95
+ }
96
+ | {
97
+ /** A folder. */
98
+ kind: 'folder';
99
+ /** The folder's path; every asset under it is dragged. */
100
+ path: string;
101
+ };
89
102
 
90
103
  export function writeAssetDrag(dataTransfer: DataTransfer, payload: AssetDragPayload): void {
91
104
  dataTransfer.setData(ASSET_DRAG_MIME, JSON.stringify(payload));
@@ -7,7 +7,9 @@ import {QuarksLoader} from 'babylon.quarks';
7
7
  * fresh, independent copy (see `instantiateSceneEntry`).
8
8
  */
9
9
  export interface EffectAsset {
10
+ /** The asset's id in this session; scene entries made from it keep it. */
10
11
  id: number;
12
+ /** The asset's display name, its file name without `.json`. */
11
13
  name: string;
12
14
  /** Relative path inside the imported folder (preserves subfolders). */
13
15
  path: string;
@@ -17,10 +19,15 @@ export interface EffectAsset {
17
19
  missingFiles: string[];
18
20
  }
19
21
 
22
+ /** How far an asset import got. */
20
23
  export interface AssetLoadProgress {
24
+ /** JSON files read so far. */
21
25
  done: number;
26
+ /** JSON files in the import. */
22
27
  total: number;
28
+ /** Effects found so far. */
23
29
  loaded: number;
30
+ /** Files that could not be read. */
24
31
  failed: number;
25
32
  }
26
33
 
@@ -75,6 +82,13 @@ function readAsDataUrl(file: File): Promise<string> {
75
82
  });
76
83
  }
77
84
 
85
+ /** An object with fields of unknown types, as an imported file's JSON may hold anywhere. */
86
+ type JSONRecord = {[key: string]: unknown};
87
+
88
+ function isRecord(value: unknown): value is JSONRecord {
89
+ return value !== null && typeof value === 'object';
90
+ }
91
+
78
92
  /** True for a Quarks effect envelope with at least one particle emitter. */
79
93
  export function isEffectJson(json: unknown): boolean {
80
94
  const object = (json as {object?: unknown} | null)?.object;
@@ -166,7 +180,7 @@ function createFileResolver(effectPath: string, index: ImportedFileIndex, missin
166
180
  * {@link QuarksLoader.resolveLinkedResources}, so the effect loads (and later exports) as a
167
181
  * self-contained effect. Returns the references that could not be found among the files.
168
182
  */
169
- async function inlineExternalFiles(json: any, effectPath: string, index: ImportedFileIndex): Promise<string[]> {
183
+ async function inlineExternalFiles(json: unknown, effectPath: string, index: ImportedFileIndex): Promise<string[]> {
170
184
  const missing: string[] = [];
171
185
  // baseUrl is the effect file path so resolveRelativePath can walk from its folder; the
172
186
  // File resolver ignores HTTP join and looks up files by path/name instead.
@@ -263,22 +277,23 @@ export function assetIconUrl(asset: EffectAsset): string | null {
263
277
  if (cached !== undefined) {
264
278
  return cached;
265
279
  }
266
- const json = asset.json as any;
267
- const findMaterial = (node: any): string | undefined => {
268
- if (!node || typeof node !== 'object') return undefined;
269
- if (node.type === 'ParticleEmitter' && node.ps?.material) return node.ps.material;
280
+ const json = isRecord(asset.json) ? asset.json : {};
281
+ const findMaterial = (node: unknown): unknown => {
282
+ if (!isRecord(node)) return undefined;
283
+ if (node.type === 'ParticleEmitter' && isRecord(node.ps) && node.ps.material) return node.ps.material;
270
284
  for (const child of Array.isArray(node.children) ? node.children : []) {
271
285
  const found = findMaterial(child);
272
286
  if (found) return found;
273
287
  }
274
288
  return undefined;
275
289
  };
276
- const byUuid = (list: unknown, uuid: unknown) =>
277
- (Array.isArray(list) ? list : []).find((item: any) => item?.uuid === uuid) as any;
290
+ const byUuid = (list: unknown, uuid: unknown): JSONRecord | undefined =>
291
+ (Array.isArray(list) ? list : []).find((item: unknown) => isRecord(item) && item.uuid === uuid);
278
292
 
279
- const material = byUuid(json?.materials, findMaterial(json?.object));
280
- const texture = byUuid(json?.textures, material?.map ?? material?.texture);
281
- const image = byUuid(json?.images, texture?.image) ?? (Array.isArray(json?.images) ? json.images[0] : undefined);
293
+ const material = byUuid(json.materials, findMaterial(json.object));
294
+ const texture = byUuid(json.textures, material?.map ?? material?.texture);
295
+ const firstImage: unknown = Array.isArray(json.images) ? json.images[0] : undefined;
296
+ const image = byUuid(json.images, texture?.image) ?? (isRecord(firstImage) ? firstImage : undefined);
282
297
  const url = typeof image?.url === 'string' && image.url ? image.url : null;
283
298
  iconCache.set(asset, url);
284
299
  return url;
@@ -3,6 +3,7 @@ import type {ParticleSystem} from 'babylon.quarks';
3
3
  import type {EffectTreeNode} from './effectTree.js';
4
4
  import {buildEffectTree, serializeEffectTree} from './effectTree.js';
5
5
 
6
+ /** Called after every change made through an {@link EffectBinding}. */
6
7
  export type EditorListener = () => void;
7
8
 
8
9
  /**
@@ -32,6 +33,7 @@ export class EffectBinding {
32
33
  return buildEffectTree(this.root);
33
34
  }
34
35
 
36
+ /** Calls `listener` after every change; returns the call that stops it. */
35
37
  subscribe(listener: EditorListener): () => void {
36
38
  this.listeners.add(listener);
37
39
  return () => this.listeners.delete(listener);
@@ -42,17 +44,23 @@ export class EffectBinding {
42
44
  return this.revision;
43
45
  }
44
46
 
47
+ /**
48
+ * Changes the main system through `mutate`: takes an undo snapshot first, then notifies
49
+ * subscribers. Every inspector edit goes through here.
50
+ */
45
51
  apply(mutate: (system: ParticleSystem) => void): void {
46
52
  this.onBeforeChange?.();
47
53
  mutate(this.system);
48
54
  this.notify();
49
55
  }
50
56
 
57
+ /** Adds a system to the effect's sub systems. */
51
58
  addSubSystem(sub: ParticleSystem): void {
52
59
  this.subSystems.push(sub);
53
60
  this.notify();
54
61
  }
55
62
 
63
+ /** Removes a system from the effect's sub systems; a system it does not have is ignored. */
56
64
  removeSubSystem(sub: ParticleSystem): void {
57
65
  const index = this.subSystems.indexOf(sub);
58
66
  if (index >= 0) {
@@ -92,6 +100,7 @@ export class EffectBinding {
92
100
  }
93
101
  }
94
102
 
103
+ /** Restarts every system of the effect from time 0. */
95
104
  restart(): void {
96
105
  // onlyUsedByOther systems skip self-emission in update(); play() only unpauses sim.
97
106
  for (const system of [this.system, ...this.subSystems]) {
@@ -8,8 +8,12 @@ import {Vector3, getPhysicsResolver, setPhysicsResolver} from 'babylon.quarks';
8
8
  * which is exactly what ApplyCollision.update reflects the velocity against.
9
9
  */
10
10
  export class GroundPlaneResolver {
11
+ /** @param y - Height of the ground plane. */
11
12
  constructor(public y = 0) {}
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: Vector3, normal: Vector3): boolean {
14
18
  if (position.y <= this.y) {
15
19
  normal.set(0, 1, 0);
@@ -3,18 +3,25 @@ import {ColorOverLife, Gradient, Vector3, Vector4} from 'babylon.quarks';
3
3
 
4
4
  /** One RGBA stop of the editable gradient (all channels 0..1, pos 0..1). */
5
5
  export interface GradientStop {
6
+ /** Where the stop sits over the particle's life, 0..1. */
6
7
  pos: number;
8
+ /** Red, 0..1. */
7
9
  r: number;
10
+ /** Green, 0..1. */
8
11
  g: number;
12
+ /** Blue, 0..1. */
9
13
  b: number;
14
+ /** Alpha, 0..1. */
10
15
  a: number;
11
16
  }
12
17
 
18
+ /** The gradient a new Color over Lifetime starts with: warm yellow fading out through red. */
13
19
  export const DEFAULT_GRADIENT_STOPS: GradientStop[] = [
14
20
  {pos: 0, r: 1, g: 0.85, b: 0.4, a: 1},
15
21
  {pos: 1, r: 1, g: 0.25, b: 0.05, a: 0},
16
22
  ];
17
23
 
24
+ /** A quarks gradient through `stops`, sorted by position; colour and alpha share the stops. */
18
25
  export function buildGradient(stops: GradientStop[]): Gradient {
19
26
  const sorted = [...stops].sort((a, b) => a.pos - b.pos);
20
27
  return new Gradient(
@@ -23,26 +30,38 @@ export function buildGradient(stops: GradientStop[]): Gradient {
23
30
  );
24
31
  }
25
32
 
33
+ /** A Color over Lifetime behavior through `stops`. */
26
34
  export function buildColorOverLife(stops: GradientStop[]): ColorOverLife {
27
35
  return new ColorOverLife(buildGradient(stops));
28
36
  }
29
37
 
38
+ /** An RGBA colour as a quarks `Vector4`. */
30
39
  export function rgbaToVector4(stop: {r: number; g: number; b: number; a: number}): Vector4 {
31
40
  return new Vector4(stop.r, stop.g, stop.b, stop.a);
32
41
  }
33
42
 
43
+ /** A stop's colour as CSS `rgba()`. */
34
44
  export function stopToCss(stop: GradientStop): string {
35
45
  const c = (v: number) => Math.round(v * 255);
36
46
  return `rgba(${c(stop.r)}, ${c(stop.g)}, ${c(stop.b)}, ${stop.a.toFixed(2)})`;
37
47
  }
38
48
 
49
+ /** The stops as a left-to-right CSS `linear-gradient`, for a gradient preview. */
39
50
  export function stopsToCssGradient(stops: GradientStop[]): string {
40
51
  const sorted = [...stops].sort((a, b) => a.pos - b.pos);
41
52
  const parts = sorted.map((s) => `${stopToCss(s)} ${(s.pos * 100).toFixed(1)}%`);
42
53
  return `linear-gradient(90deg, ${parts.join(', ')})`;
43
54
  }
44
55
 
45
- export function hexToRgb(hex: string): {r: number; g: number; b: number} {
56
+ /** A `#rrggbb` colour as channels 0..1. */
57
+ export function hexToRgb(hex: string): {
58
+ /** Red, 0..1. */
59
+ r: number;
60
+ /** Green, 0..1. */
61
+ g: number;
62
+ /** Blue, 0..1. */
63
+ b: number;
64
+ } {
46
65
  const value = hex.replace('#', '');
47
66
  return {
48
67
  r: parseInt(value.slice(0, 2), 16) / 255,
@@ -51,6 +70,7 @@ export function hexToRgb(hex: string): {r: number; g: number; b: number} {
51
70
  };
52
71
  }
53
72
 
73
+ /** Channels 0..1, clamped, as a `#rrggbb` colour. */
54
74
  export function rgbToHex(r: number, g: number, b: number): string {
55
75
  const c = (v: number) =>
56
76
  Math.round(Math.min(1, Math.max(0, v)) * 255)
@@ -1,3 +1,4 @@
1
+ import type {Texture} from '@babylonjs/core/Materials/Textures/texture.js';
1
2
  import {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
2
3
  import type {Scene} from '@babylonjs/core/scene.js';
3
4
  import {
@@ -14,14 +15,14 @@ import {DEFAULT_GRADIENT_STOPS, buildGradient} from './colors.js';
14
15
  import {uniqueEntryName, type SceneEntry} from './sceneEntries.js';
15
16
 
16
17
  export interface CreateSceneDefaultOptions {
17
- resolveTexture?: (url: string, scene: Scene) => unknown;
18
+ resolveTexture?: (url: string, scene: Scene) => Texture | null | undefined;
18
19
  textureUrl?: string;
19
20
  }
20
21
 
21
22
  /** Creates a simple cone burst preset for new editor effects. */
22
23
  export function createDefaultEffect(
23
24
  scene: Scene,
24
- resolveTexture?: (url: string, scene: Scene) => unknown,
25
+ resolveTexture?: (url: string, scene: Scene) => Texture | null | undefined,
25
26
  textureUrl?: string
26
27
  ): ParticleSystem {
27
28
  const system = new ParticleSystem({
@@ -35,7 +36,7 @@ export function createDefaultEffect(
35
36
  emissionOverTime: new ConstantValue(80),
36
37
  shape: new ConeEmitter({radius: 0.3, angle: 0.25}),
37
38
  renderMode: RenderMode.BillBoard,
38
- texture: (textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined) as never,
39
+ texture: textureUrl && resolveTexture ? resolveTexture(textureUrl, scene) : undefined,
39
40
  transparent: true,
40
41
  blendMode: 1,
41
42
  });