babylon.quarks-editor 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +38 -5
  3. package/dist/core/assetDragSession.d.ts +1 -1
  4. package/dist/core/assetTree.d.ts +1 -1
  5. package/dist/core/binding.d.ts +2 -2
  6. package/dist/core/binding.js +1 -1
  7. package/dist/core/defaultEffect.d.ts +2 -2
  8. package/dist/core/defaultEffect.js +3 -3
  9. package/dist/core/editorScene.d.ts +1 -1
  10. package/dist/core/effectTree.d.ts +1 -1
  11. package/dist/core/effectTree.js +13 -88
  12. package/dist/core/emitterShapeWireframe.d.ts +2 -2
  13. package/dist/core/emitterShapeWireframe.js +45 -13
  14. package/dist/core/galleryDropMarker.d.ts +2 -2
  15. package/dist/core/galleryDropMarker.js +6 -6
  16. package/dist/core/galleryLayout.d.ts +3 -3
  17. package/dist/core/galleryLayout.js +1 -1
  18. package/dist/core/galleryPlayback.d.ts +1 -1
  19. package/dist/core/galleryScene.d.ts +4 -4
  20. package/dist/core/galleryScene.js +3 -3
  21. package/dist/core/gallerySelectionMarker.d.ts +4 -4
  22. package/dist/core/gallerySelectionMarker.js +5 -5
  23. package/dist/core/groups.d.ts +1 -1
  24. package/dist/core/groups.js +1 -1
  25. package/dist/core/history.d.ts +1 -1
  26. package/dist/core/indexedDbProjectStore.d.ts +1 -1
  27. package/dist/core/lineShaders.d.ts +2 -2
  28. package/dist/core/lineShaders.js +2 -2
  29. package/dist/core/loadEffect.d.ts +2 -2
  30. package/dist/core/loadEffect.js +1 -1
  31. package/dist/core/pickAssetFiles.js +1 -1
  32. package/dist/core/sceneEntries.d.ts +4 -4
  33. package/dist/core/sceneEntries.js +2 -2
  34. package/dist/core/scrub.d.ts +1 -1
  35. package/dist/core/shapes.d.ts +1 -1
  36. package/dist/core/systems.d.ts +2 -2
  37. package/dist/core/textureExport.d.ts +1 -1
  38. package/dist/core/timeline.d.ts +2 -2
  39. package/dist/core/transport.d.ts +3 -3
  40. package/dist/core/transport.js +2 -2
  41. package/dist/core/viewportPicker.d.ts +4 -4
  42. package/dist/core/viewportPicker.js +8 -7
  43. package/dist/index.d.ts +21 -21
  44. package/dist/index.js +12 -12
  45. package/dist/react/AssetsPanel.d.ts +2 -2
  46. package/dist/react/AssetsPanel.js +6 -6
  47. package/dist/react/ChoiceDialog.js +1 -1
  48. package/dist/react/ContextMenu.js +1 -1
  49. package/dist/react/CurveEditor.d.ts +1 -1
  50. package/dist/react/CurveEditor.js +2 -2
  51. package/dist/react/EditorWorkspace.d.ts +2 -2
  52. package/dist/react/EditorWorkspace.js +4 -4
  53. package/dist/react/EffectEditor.d.ts +3 -3
  54. package/dist/react/EffectEditor.js +7 -7
  55. package/dist/react/EffectEditorHost.d.ts +6 -6
  56. package/dist/react/EffectEditorHost.js +59 -62
  57. package/dist/react/EffectTreeView.d.ts +1 -1
  58. package/dist/react/EffectTreeView.js +3 -3
  59. package/dist/react/GradientEditor.d.ts +1 -1
  60. package/dist/react/GradientEditor.js +3 -3
  61. package/dist/react/InspectorInfoIcon.js +3 -3
  62. package/dist/react/InspectorLabel.js +3 -3
  63. package/dist/react/MessageDialog.js +1 -1
  64. package/dist/react/ModuleSection.js +4 -4
  65. package/dist/react/ObjectModule.d.ts +1 -1
  66. package/dist/react/ObjectModule.js +4 -4
  67. package/dist/react/ProjectsPage.d.ts +1 -1
  68. package/dist/react/ProjectsPage.js +6 -6
  69. package/dist/react/PromptDialog.js +1 -1
  70. package/dist/react/ScenePanel.d.ts +2 -2
  71. package/dist/react/ScenePanel.js +5 -5
  72. package/dist/react/SubEmittersModule.d.ts +1 -1
  73. package/dist/react/SubEmittersModule.js +5 -5
  74. package/dist/react/TextureSelect.d.ts +24 -0
  75. package/dist/react/TextureSelect.js +74 -0
  76. package/dist/react/TextureUVField.d.ts +14 -0
  77. package/dist/react/TextureUVField.js +23 -0
  78. package/dist/react/TimelinePanel.d.ts +7 -7
  79. package/dist/react/TimelinePanel.js +11 -9
  80. package/dist/react/TimelineTrackRow.d.ts +3 -3
  81. package/dist/react/TimelineTrackRow.js +6 -6
  82. package/dist/react/TransportBar.d.ts +4 -4
  83. package/dist/react/TransportBar.js +3 -3
  84. package/dist/react/ValueField.d.ts +1 -1
  85. package/dist/react/ValueField.js +3 -3
  86. package/dist/react/behaviorModules.d.ts +4 -2
  87. package/dist/react/behaviorModules.js +52 -16
  88. package/dist/react/fields.d.ts +1 -0
  89. package/dist/react/fields.js +3 -3
  90. package/dist/react/index.d.ts +40 -38
  91. package/dist/react/index.js +25 -23
  92. package/dist/react/inspectorHints.js +31 -0
  93. package/dist/react/modules.d.ts +3 -1
  94. package/dist/react/modules.js +99 -100
  95. package/package.json +3 -2
  96. package/src/core/assetDragSession.ts +1 -1
  97. package/src/core/assetTree.ts +1 -1
  98. package/src/core/binding.ts +3 -3
  99. package/src/core/defaultEffect.ts +4 -4
  100. package/src/core/editorScene.ts +1 -1
  101. package/src/core/effectTree.ts +13 -101
  102. package/src/core/emitterShapeWireframe.ts +48 -16
  103. package/src/core/galleryDropMarker.ts +8 -8
  104. package/src/core/galleryLayout.ts +4 -4
  105. package/src/core/galleryPlayback.ts +1 -1
  106. package/src/core/galleryScene.ts +5 -5
  107. package/src/core/gallerySelectionMarker.ts +7 -7
  108. package/src/core/groups.ts +1 -1
  109. package/src/core/history.ts +1 -1
  110. package/src/core/indexedDbProjectStore.ts +1 -1
  111. package/src/core/lineShaders.ts +2 -2
  112. package/src/core/loadEffect.ts +3 -3
  113. package/src/core/pickAssetFiles.ts +2 -2
  114. package/src/core/sceneEntries.ts +5 -5
  115. package/src/core/scrub.ts +1 -1
  116. package/src/core/shapes.ts +1 -1
  117. package/src/core/systems.ts +2 -2
  118. package/src/core/textureExport.ts +1 -1
  119. package/src/core/timeline.ts +3 -3
  120. package/src/core/transport.ts +4 -4
  121. package/src/core/viewportPicker.ts +12 -11
  122. package/src/index.ts +21 -21
  123. package/src/react/AssetsPanel.tsx +6 -6
  124. package/src/react/ChoiceDialog.tsx +1 -1
  125. package/src/react/ContextMenu.tsx +1 -1
  126. package/src/react/CurveEditor.tsx +2 -2
  127. package/src/react/EditorWorkspace.tsx +5 -5
  128. package/src/react/EffectEditor.tsx +10 -8
  129. package/src/react/EffectEditorHost.tsx +64 -69
  130. package/src/react/EffectTreeView.tsx +4 -4
  131. package/src/react/GradientEditor.tsx +3 -3
  132. package/src/react/InspectorInfoIcon.tsx +3 -3
  133. package/src/react/InspectorLabel.tsx +3 -3
  134. package/src/react/MessageDialog.tsx +1 -1
  135. package/src/react/ModuleSection.tsx +4 -4
  136. package/src/react/ObjectModule.tsx +5 -5
  137. package/src/react/ProjectsPage.tsx +6 -6
  138. package/src/react/PromptDialog.tsx +1 -1
  139. package/src/react/ScenePanel.tsx +7 -7
  140. package/src/react/SubEmittersModule.tsx +6 -6
  141. package/src/react/TextureSelect.tsx +118 -0
  142. package/src/react/TextureUVField.tsx +62 -0
  143. package/src/react/TimelinePanel.tsx +17 -15
  144. package/src/react/TimelineTrackRow.tsx +9 -9
  145. package/src/react/TransportBar.tsx +7 -7
  146. package/src/react/ValueField.tsx +3 -3
  147. package/src/react/behaviorModules.tsx +211 -28
  148. package/src/react/fields.tsx +10 -3
  149. package/src/react/index.ts +41 -38
  150. package/src/react/inspectorHints.ts +40 -0
  151. package/src/react/modules.tsx +375 -131
@@ -1,8 +1,9 @@
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
3
  import React, {useEffect, useSyncExternalStore} from 'react';
4
- import {EffectBinding} from '../core/binding';
4
+ import {EffectBinding} from '../core/binding.js';
5
5
  import {
6
+ AttractorModule,
6
7
  CollisionModule,
7
8
  ColorBySpeedModule,
8
9
  EmitDirectionModule,
@@ -22,8 +23,8 @@ import {
22
23
  TurbulenceModule,
23
24
  VelocityOverLifeModule,
24
25
  WidthOverTrailModule,
25
- } from './behaviorModules';
26
- import type {GeometryData, GeometryOption, TextureOption} from './modules';
26
+ } from './behaviorModules.js';
27
+ import type {GeometryData, GeometryOption, TextureOption} from './modules.js';
27
28
  import {
28
29
  ColorOverLifeModule,
29
30
  EmissionModule,
@@ -31,10 +32,10 @@ import {
31
32
  RendererModule,
32
33
  ShapeModule,
33
34
  SizeOverLifeModule,
34
- } from './modules';
35
- import {ObjectModule} from './ObjectModule';
36
- import {SubEmittersModule} from './SubEmittersModule';
37
- import {theme} from './theme';
35
+ } from './modules.js';
36
+ import {ObjectModule} from './ObjectModule.js';
37
+ import {SubEmittersModule} from './SubEmittersModule.js';
38
+ import {theme} from './theme.js';
38
39
 
39
40
  export interface EffectEditorProps {
40
41
  binding: EffectBinding;
@@ -112,6 +113,7 @@ export const EffectEditor = React.memo(function EffectEditor(props: EffectEditor
112
113
  <InheritVelocityModule binding={binding} />
113
114
  <ForceOverLifeModule binding={binding} />
114
115
  <GravityModule binding={binding} />
116
+ <AttractorModule binding={binding} />
115
117
  <OrbitOverLifeModule binding={binding} />
116
118
  <ColorOverLifeModule binding={binding} />
117
119
  <ColorBySpeedModule binding={binding} />
@@ -1,19 +1,19 @@
1
- import {ArcRotateCamera} from '@babylonjs/core/Cameras/arcRotateCamera';
1
+ import {ArcRotateCamera} from '@babylonjs/core/Cameras/arcRotateCamera.js';
2
2
  // scene.pick lives in Ray's side effect (Babylon 9.31 no longer pulls it in); gizmos pick through it.
3
- import '@babylonjs/core/Culling/ray';
4
- import {Engine} from '@babylonjs/core/Engines/engine';
5
- import {PointerEventTypes} from '@babylonjs/core/Events/pointerEvents';
6
- import {GizmoManager} from '@babylonjs/core/Gizmos/gizmoManager';
7
- import {DirectionalLight} from '@babylonjs/core/Lights/directionalLight';
8
- import {Color3, Color4} from '@babylonjs/core/Maths/math.color';
9
- import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
10
- import {Viewport} from '@babylonjs/core/Maths/math.viewport';
11
- import type {Mesh} from '@babylonjs/core/Meshes/mesh';
12
- import {MeshBuilder} from '@babylonjs/core/Meshes/meshBuilder';
13
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
14
- import {UtilityLayerRenderer} from '@babylonjs/core/Rendering/utilityLayerRenderer';
15
- import {Scene} from '@babylonjs/core/scene';
16
- import {GridMaterial} from '@babylonjs/materials/grid/gridMaterial';
3
+ import '@babylonjs/core/Culling/ray.js';
4
+ import {Engine} from '@babylonjs/core/Engines/engine.js';
5
+ import {PointerEventTypes} from '@babylonjs/core/Events/pointerEvents.js';
6
+ import {GizmoManager} from '@babylonjs/core/Gizmos/gizmoManager.js';
7
+ import {DirectionalLight} from '@babylonjs/core/Lights/directionalLight.js';
8
+ import {Color3, Color4} from '@babylonjs/core/Maths/math.color.js';
9
+ import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector.js';
10
+ import {Viewport} from '@babylonjs/core/Maths/math.viewport.js';
11
+ import type {Mesh} from '@babylonjs/core/Meshes/mesh.js';
12
+ import {MeshBuilder} from '@babylonjs/core/Meshes/meshBuilder.js';
13
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
14
+ import {UtilityLayerRenderer} from '@babylonjs/core/Rendering/utilityLayerRenderer.js';
15
+ import {Scene} from '@babylonjs/core/scene.js';
16
+ import {GridMaterial} from '@babylonjs/materials/grid/gridMaterial.js';
17
17
  import {
18
18
  ArrowLeftIcon,
19
19
  ArrowPathIcon,
@@ -31,8 +31,8 @@ import {
31
31
  import {BatchedRenderer, QuarksUtil} from 'babylon.quarks';
32
32
  import React, {useCallback, useDeferredValue, useEffect, useMemo, useReducer, useRef, useState} from 'react';
33
33
  import {createRoot} from 'react-dom/client';
34
- import {getAssetDragSession, setAssetDragSession} from '../core/assetDragSession';
35
- import {ASSET_DRAG_MIME, describeAssetDrag, readAssetDrag, type AssetDragPayload} from '../core/assetTree';
34
+ import {getAssetDragSession, setAssetDragSession} from '../core/assetDragSession.js';
35
+ import {ASSET_DRAG_MIME, describeAssetDrag, readAssetDrag, type AssetDragPayload} from '../core/assetTree.js';
36
36
  import {
37
37
  collectJsonFiles,
38
38
  createEffectAsset,
@@ -41,24 +41,24 @@ import {
41
41
  removeAssets,
42
42
  type AssetLoadProgress,
43
43
  type EffectAsset,
44
- } from '../core/assets';
45
- import {EffectBinding} from '../core/binding';
46
- import {createSceneDefaultEntry} from '../core/defaultEffect';
47
- import {serializeEffectTree} from '../core/effectTree';
48
- import {EmitterShapeWireframes} from '../core/emitterShapeWireframe';
49
- import {GalleryDropMarker} from '../core/galleryDropMarker';
50
- import {cancelGalleryCameraFocus, focusGalleryEntry} from '../core/galleryLayout';
44
+ } from '../core/assets.js';
45
+ import {EffectBinding} from '../core/binding.js';
46
+ import {createSceneDefaultEntry} from '../core/defaultEffect.js';
47
+ import {serializeEffectTree} from '../core/effectTree.js';
48
+ import {EmitterShapeWireframes} from '../core/emitterShapeWireframe.js';
49
+ import {GalleryDropMarker} from '../core/galleryDropMarker.js';
50
+ import {cancelGalleryCameraFocus, focusGalleryEntry} from '../core/galleryLayout.js';
51
51
  import {
52
52
  applyGalleryPlayback,
53
53
  countGalleryParticles,
54
54
  hasActivePreview,
55
55
  pauseFinishedPreviews,
56
- } from '../core/galleryPlayback';
57
- import {pickGroundPosition, placeSceneEntriesAt, setSceneEntryVisible} from '../core/galleryScene';
58
- import {GallerySelectionMarker} from '../core/gallerySelectionMarker';
59
- import {EffectHistory, type HistoryStacks} from '../core/history';
60
- import {parseEffectFromJson} from '../core/loadEffect';
61
- import {pickAssetFolderFiles} from '../core/pickAssetFiles';
56
+ } from '../core/galleryPlayback.js';
57
+ import {pickGroundPosition, placeSceneEntriesAt, setSceneEntryVisible} from '../core/galleryScene.js';
58
+ import {GallerySelectionMarker} from '../core/gallerySelectionMarker.js';
59
+ import {EffectHistory, type HistoryStacks} from '../core/history.js';
60
+ import {parseEffectFromJson} from '../core/loadEffect.js';
61
+ import {pickAssetFolderFiles} from '../core/pickAssetFiles.js';
62
62
  import {
63
63
  clearFocusFinished,
64
64
  createPlaybackState,
@@ -67,33 +67,33 @@ import {
67
67
  markFocusFinished,
68
68
  setFocusElapsed,
69
69
  type PlaybackState,
70
- } from '../core/playback';
71
- import type {ProjectStore, StoredAsset, StoredScene} from '../core/projectStore';
72
- import {logRenderMismatch} from '../core/renderDebug';
70
+ } from '../core/playback.js';
71
+ import type {ProjectStore, StoredAsset, StoredScene} from '../core/projectStore.js';
72
+ import {logRenderMismatch} from '../core/renderDebug.js';
73
73
  import {
74
74
  bindingFromSceneEntry,
75
75
  countInstancesByAsset,
76
76
  disposeSceneEntry,
77
77
  instantiateSceneEntry,
78
78
  type SceneEntry,
79
- } from '../core/sceneEntries';
80
- import {pauseFocus, scrubFocusToSettled} from '../core/scrub';
81
- import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
82
- import {notifyTransformChanged, subscribeTransform} from '../core/transformStore';
83
- import {toggleStagePlayback} from '../core/transport';
84
- import {GalleryViewportPicker} from '../core/viewportPicker';
85
- import {AssetsPanel} from './AssetsPanel';
86
- import {ChoiceDialog} from './ChoiceDialog';
87
- import {EffectEditor} from './EffectEditor';
88
- import {EffectTreeView} from './EffectTreeView';
89
- import {MessageDialog} from './MessageDialog';
90
- import {PromptDialog} from './PromptDialog';
91
- import {ScenePanel} from './ScenePanel';
92
- import {TimelinePanel} from './TimelinePanel';
93
- import {TransportBar} from './TransportBar';
94
- import {iconStyle, type HeroIcon} from './icons';
95
- import type {GeometryData, GeometryOption, TextureOption} from './modules';
96
- import {buttonStyle, globalEditorStyles, theme} from './theme';
79
+ } from '../core/sceneEntries.js';
80
+ import {pauseFocus, scrubFocusToSettled} from '../core/scrub.js';
81
+ import {computeTimelineRows, computeTimelineSpan} from '../core/timeline.js';
82
+ import {notifyTransformChanged, subscribeTransform} from '../core/transformStore.js';
83
+ import {toggleStagePlayback} from '../core/transport.js';
84
+ import {GalleryViewportPicker} from '../core/viewportPicker.js';
85
+ import {AssetsPanel} from './AssetsPanel.js';
86
+ import {ChoiceDialog} from './ChoiceDialog.js';
87
+ import {EffectEditor} from './EffectEditor.js';
88
+ import {EffectTreeView} from './EffectTreeView.js';
89
+ import {MessageDialog} from './MessageDialog.js';
90
+ import {PromptDialog} from './PromptDialog.js';
91
+ import {ScenePanel} from './ScenePanel.js';
92
+ import {TimelinePanel} from './TimelinePanel.js';
93
+ import {TransportBar} from './TransportBar.js';
94
+ import {iconStyle, type HeroIcon} from './icons.js';
95
+ import type {GeometryData, GeometryOption, TextureOption} from './modules.js';
96
+ import {buttonStyle, globalEditorStyles, theme} from './theme.js';
97
97
 
98
98
  /**
99
99
  * The effect's JSON, with a warning when it points at object URLs: a texture
@@ -390,11 +390,12 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
390
390
  };
391
391
 
392
392
  useEffect(() => {
393
- const engine = new Engine(canvasRef.current!, true);
393
+ const canvas = canvasRef.current!;
394
+ const engine = new Engine(canvas, true);
394
395
  const scene = new Scene(engine);
395
396
  scene.clearColor = new Color4(0.03, 0.04, 0.09, 1);
396
397
  const camera = new ArcRotateCamera('cam', -Math.PI / 2, 1.15, 9, new BVector3(0, 1.4, 0), scene);
397
- camera.attachControl(canvasRef.current!, true);
398
+ camera.attachControl(canvas, true);
398
399
  camera.wheelDeltaPercentage = 0.01;
399
400
 
400
401
  // Mesh render mode particles are lit (billboard modes ignore these); the grid is visual
@@ -453,7 +454,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
453
454
  const state = stateRef.current;
454
455
 
455
456
  const handleViewportPointerLeave = () => viewportPicker.setHover(null);
456
- canvasRef.current?.addEventListener('pointerleave', handleViewportPointerLeave);
457
+ canvas.addEventListener('pointerleave', handleViewportPointerLeave);
457
458
 
458
459
  const pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
459
460
  if (assetDragDepthRef.current > 0) {
@@ -1313,7 +1314,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1313
1314
  });
1314
1315
 
1315
1316
  const resizeObserver = new ResizeObserver(() => engine.resize());
1316
- resizeObserver.observe(canvasRef.current!);
1317
+ resizeObserver.observe(canvas);
1317
1318
  const onKey = (e: KeyboardEvent) => {
1318
1319
  const key = e.key.toLowerCase();
1319
1320
  if ((e.ctrlKey || e.metaKey) && key === 's') {
@@ -1375,13 +1376,13 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1375
1376
  emitterWireframesRef.current = null;
1376
1377
  viewportPicker.dispose();
1377
1378
  viewportPickerRef.current = null;
1378
- canvasRef.current?.removeEventListener('pointerleave', handleViewportPointerLeave);
1379
+ canvas.removeEventListener('pointerleave', handleViewportPointerLeave);
1379
1380
  scene.onPointerObservable.remove(pointerObserver);
1380
1381
  gizmoManager.dispose();
1381
1382
  gizmoUtilityLayer.dispose();
1382
1383
  engine.dispose();
1383
1384
  };
1384
- // eslint-disable-next-line react-hooks/exhaustive-deps
1385
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- mount only: builds the engine and scene once
1385
1386
  }, []);
1386
1387
 
1387
1388
  useEffect(() => {
@@ -1527,13 +1528,6 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1527
1528
  }, [selectedNode, binding, isAssetsLoading]);
1528
1529
 
1529
1530
  const state = stateRef.current;
1530
- const gizmoNode =
1531
- selectedNode && !selectedNode.isDisposed()
1532
- ? selectedNode
1533
- : binding?.root && !binding.root.isDisposed()
1534
- ? binding.root
1535
- : null;
1536
-
1537
1531
  const allSystems = binding ? [binding.system, ...binding.subSystems] : [];
1538
1532
  const activeSystem = binding ? (allSystems.find((s) => s.emitter === selectedNode) ?? binding.system) : null;
1539
1533
  // The inspector is the expensive part of a selection change (every module re-renders for the
@@ -1554,22 +1548,23 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1554
1548
  const instanceCounts = useMemo(() => countInstancesByAsset(sceneEntries), [sceneEntries]);
1555
1549
  const showTimeline = Boolean(binding && activeSystem && state);
1556
1550
  const timelinePlaybackEnabled = !focusedRoot || visibleRootIdSet.has(focusedRoot.uniqueId);
1551
+ const {resolveTexture, resolveGeometry} = props;
1557
1552
  const resolveTextureForEditor = useCallback(
1558
1553
  (url: string) => {
1559
1554
  const scene = stateRef.current?.scene;
1560
- return scene && props.resolveTexture ? props.resolveTexture(url, scene) : undefined;
1555
+ return scene && resolveTexture ? resolveTexture(url, scene) : undefined;
1561
1556
  },
1562
- [props.resolveTexture]
1557
+ [resolveTexture]
1563
1558
  );
1564
1559
  const resolveGeometryForEditor = useCallback(
1565
1560
  (file: File) => {
1566
1561
  const scene = stateRef.current?.scene;
1567
- if (!scene || !props.resolveGeometry) {
1562
+ if (!scene || !resolveGeometry) {
1568
1563
  return Promise.reject(new Error('Geometry loader is not available.'));
1569
1564
  }
1570
- return props.resolveGeometry(file, scene);
1565
+ return resolveGeometry(file, scene);
1571
1566
  },
1572
- [props.resolveGeometry]
1567
+ [resolveGeometry]
1573
1568
  );
1574
1569
 
1575
1570
  return (
@@ -1,9 +1,9 @@
1
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
1
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
2
2
  import {ChevronDownIcon, ChevronRightIcon, FolderIcon, SparklesIcon} from '@heroicons/react/24/solid';
3
3
  import React, {useState} from 'react';
4
- import {buildEffectTree, type EffectTreeNode} from '../core/effectTree';
5
- import {iconStyle} from './icons';
6
- import {theme} from './theme';
4
+ import {buildEffectTree, type EffectTreeNode} from '../core/effectTree.js';
5
+ import {iconStyle} from './icons.js';
6
+ import {theme} from './theme.js';
7
7
 
8
8
  export interface EffectTreeViewProps {
9
9
  root: TransformNode;
@@ -1,7 +1,7 @@
1
1
  import {useRef, useState} from 'react';
2
- import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
3
- import {NumberField, Row} from './fields';
4
- import {buttonStyle, theme} from './theme';
2
+ import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors.js';
3
+ import {NumberField, Row} from './fields.js';
4
+ import {buttonStyle, theme} from './theme.js';
5
5
 
6
6
  const BAR_WIDTH = 218;
7
7
 
@@ -1,8 +1,8 @@
1
1
  import {InformationCircleIcon} from '@heroicons/react/24/solid';
2
2
  import React from 'react';
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
 
7
7
  /** Small info icon with a themed inspector tooltip. */
8
8
  export function InspectorInfoIcon(props: {
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
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
 
6
6
  /** Inspector row label with an optional info icon tooltip. */
7
7
  export function InspectorLabel(props: {label: string; hint?: string; hintKey?: string}): React.ReactElement | null {
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import {buttonStyle, theme} from './theme';
2
+ import {buttonStyle, theme} from './theme.js';
3
3
 
4
4
  export interface MessageDialogProps {
5
5
  title: string;
@@ -1,9 +1,9 @@
1
1
  import {ChevronDownIcon, ChevronRightIcon} from '@heroicons/react/24/solid';
2
2
  import React, {useState} from 'react';
3
- import {iconStyle} from './icons';
4
- import {getInspectorHint} from './inspectorHints';
5
- import {InspectorInfoIcon} from './InspectorInfoIcon';
6
- import {theme} from './theme';
3
+ import {iconStyle} from './icons.js';
4
+ import {getInspectorHint} from './inspectorHints.js';
5
+ import {InspectorInfoIcon} from './InspectorInfoIcon.js';
6
+ import {theme} from './theme.js';
7
7
 
8
8
  /** Unity-style module foldout with an optional enable checkbox. */
9
9
  export function ModuleSection(props: {
@@ -1,9 +1,9 @@
1
- import {Quaternion} from '@babylonjs/core/Maths/math.vector';
2
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
1
+ import {Quaternion} from '@babylonjs/core/Maths/math.vector.js';
2
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.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
 
8
8
  const RAD_TO_DEG = 180 / Math.PI;
9
9
  const DEG_TO_RAD = Math.PI / 180;
@@ -1,11 +1,11 @@
1
1
  import {ArrowLeftIcon, FolderIcon, PlusIcon} from '@heroicons/react/24/solid';
2
2
  import React, {useCallback, useEffect, useState} from 'react';
3
- import {formatBytes, type ProjectMeta, type ProjectStore, type StorageEstimate} from '../core/projectStore';
4
- import {ChoiceDialog} from './ChoiceDialog';
5
- import {ContextMenu, type ContextMenuItem} from './ContextMenu';
6
- import {iconStyle} from './icons';
7
- import {PromptDialog} from './PromptDialog';
8
- import {buttonStyle, globalEditorStyles, theme} from './theme';
3
+ import {formatBytes, type ProjectMeta, type ProjectStore, type StorageEstimate} from '../core/projectStore.js';
4
+ import {ChoiceDialog} from './ChoiceDialog.js';
5
+ import {ContextMenu, type ContextMenuItem} from './ContextMenu.js';
6
+ import {iconStyle} from './icons.js';
7
+ import {PromptDialog} from './PromptDialog.js';
8
+ import {buttonStyle, globalEditorStyles, theme} from './theme.js';
9
9
 
10
10
  export interface ProjectsPageProps {
11
11
  store: ProjectStore;
@@ -1,5 +1,5 @@
1
1
  import React, {useEffect, useRef, useState} from 'react';
2
- import {buttonStyle, inputStyle, theme} from './theme';
2
+ import {buttonStyle, inputStyle, theme} from './theme.js';
3
3
 
4
4
  export interface PromptDialogProps {
5
5
  title: string;
@@ -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
  import {
3
3
  ChevronDownIcon,
4
4
  ChevronRightIcon,
@@ -9,12 +9,12 @@ import {
9
9
  ViewfinderCircleIcon,
10
10
  } from '@heroicons/react/24/solid';
11
11
  import React, {useEffect, useState} from 'react';
12
- import {buildEffectTree} from '../core/effectTree';
13
- import type {SceneEntry} from '../core/sceneEntries';
14
- import {ContextMenu, type ContextMenuItem} from './ContextMenu';
15
- import {EffectTreeView} from './EffectTreeView';
16
- import {iconStyle} from './icons';
17
- import {buttonStyle, theme} from './theme';
12
+ import {buildEffectTree} from '../core/effectTree.js';
13
+ import type {SceneEntry} from '../core/sceneEntries.js';
14
+ import {ContextMenu, type ContextMenuItem} from './ContextMenu.js';
15
+ import {EffectTreeView} from './EffectTreeView.js';
16
+ import {iconStyle} from './icons.js';
17
+ import {buttonStyle, theme} from './theme.js';
18
18
 
19
19
  const ROW_HEIGHT = 34;
20
20
 
@@ -1,11 +1,11 @@
1
1
  import type {ParticleEmitter, ParticleSystem} from 'babylon.quarks';
2
2
  import {EmitSubParticleSystem, SubParticleEmitMode} from 'babylon.quarks';
3
- import {EffectBinding} from '../core/binding';
4
- import {findBehavior} from '../core/colors';
5
- import {createChildSystem} from '../core/systems';
6
- import {ModuleSection} from './ModuleSection';
7
- import {CheckboxField, NumberField, Row, SelectField} from './fields';
8
- import {theme} from './theme';
3
+ import {EffectBinding} from '../core/binding.js';
4
+ import {findBehavior} from '../core/colors.js';
5
+ import {createChildSystem} from '../core/systems.js';
6
+ import {ModuleSection} from './ModuleSection.js';
7
+ import {CheckboxField, NumberField, Row, SelectField} from './fields.js';
8
+ import {theme} from './theme.js';
9
9
 
10
10
  /**
11
11
  * Sub Emitters module for the currently selected system. The spawned target lives as a
@@ -0,0 +1,118 @@
1
+ import type {BaseTexture} from '@babylonjs/core/Materials/Textures/baseTexture.js';
2
+ import {useState} from 'react';
3
+ import {SelectField} from './fields.js';
4
+ import type {TextureOption} from './modules.js';
5
+ import {PromptDialog} from './PromptDialog.js';
6
+
7
+ /** A texture-like object the host's `resolveTexture` returned; only its `url` is read here. */
8
+ type TextureLike = {url?: string} | null | undefined;
9
+
10
+ /** The url a texture-like object was loaded from, or '' for none. */
11
+ export function textureUrlOf(texture: unknown): string {
12
+ return (texture as TextureLike)?.url ?? '';
13
+ }
14
+
15
+ /**
16
+ * Picks a texture for one slot: a host preset, a custom URL, a file from disk, or none. The
17
+ * current texture stays selectable when it is not among the presets. Used by the Renderer
18
+ * module for the map and for the ramp, mask and noise of the texture effects.
19
+ */
20
+ export function TextureSelect(props: {
21
+ /** The slot's current texture (anything with a `url`), or null for none. */
22
+ value: unknown;
23
+ textureOptions?: TextureOption[];
24
+ /** Turns a URL or `data:` URI into the host's texture object; null when it cannot. */
25
+ loadTexture: (url: string) => unknown;
26
+ /** The host's texture object, assumed to be a Babylon texture, or null for none. */
27
+ onChange: (texture: BaseTexture | null) => void;
28
+ /** Label of the "none" entry. Default "(none)". */
29
+ noneLabel?: string;
30
+ /** Shows a thumbnail of the current texture under the select. Default false. */
31
+ preview?: boolean;
32
+ /** Title of the custom URL prompt. Default "Texture URL". */
33
+ promptTitle?: string;
34
+ }) {
35
+ const currentUrl = textureUrlOf(props.value);
36
+ const options = props.textureOptions ?? [];
37
+ const isPreset = options.some((o) => o.url === currentUrl);
38
+ const [prompting, setPrompting] = useState(false);
39
+ const apply = (url: string) => {
40
+ const texture = props.loadTexture(url);
41
+ if (texture) {
42
+ props.onChange(texture as BaseTexture);
43
+ }
44
+ };
45
+ return (
46
+ <>
47
+ <SelectField
48
+ value={!currentUrl ? '__none' : isPreset ? currentUrl : '__current'}
49
+ options={[
50
+ {value: '__none', label: props.noneLabel ?? '(none)'},
51
+ ...(currentUrl && !isPreset
52
+ ? [{value: '__current', label: `(current) ${currentUrl.slice(-24)}`}]
53
+ : []),
54
+ ...options.map((o) => ({value: o.url, label: o.label})),
55
+ {value: '__url', label: 'Custom URL…'},
56
+ {value: '__file', label: 'Load from file…'},
57
+ ]}
58
+ onChange={(url) => {
59
+ if (url === '__current') {
60
+ return;
61
+ }
62
+ if (url === '__none') {
63
+ props.onChange(null);
64
+ return;
65
+ }
66
+ if (url === '__url') {
67
+ setPrompting(true);
68
+ return;
69
+ }
70
+ if (url === '__file') {
71
+ const input = document.createElement('input');
72
+ input.type = 'file';
73
+ input.accept = 'image/*';
74
+ input.onchange = () => {
75
+ const file = input.files?.[0];
76
+ if (!file) {
77
+ return;
78
+ }
79
+ const reader = new FileReader();
80
+ reader.onload = () => apply(reader.result as string);
81
+ reader.readAsDataURL(file);
82
+ };
83
+ input.click();
84
+ return;
85
+ }
86
+ apply(url);
87
+ }}
88
+ />
89
+ {prompting && (
90
+ <PromptDialog
91
+ title={props.promptTitle ?? 'Texture URL'}
92
+ defaultValue={currentUrl}
93
+ placeholder="https://…"
94
+ onSubmit={(url) => {
95
+ apply(url);
96
+ setPrompting(false);
97
+ }}
98
+ onCancel={() => setPrompting(false)}
99
+ />
100
+ )}
101
+ {props.preview && currentUrl && (
102
+ <img
103
+ src={currentUrl}
104
+ alt="texture preview"
105
+ style={{
106
+ width: 64,
107
+ height: 64,
108
+ marginTop: 6,
109
+ objectFit: 'contain',
110
+ borderRadius: 6,
111
+ border: '1px solid #2b3761',
112
+ background: 'repeating-conic-gradient(#2a3252 0% 25%, #171d33 0% 50%) 0 0 / 12px 12px',
113
+ }}
114
+ />
115
+ )}
116
+ </>
117
+ );
118
+ }
@@ -0,0 +1,62 @@
1
+ import type {TextureUV, UVChannel} from 'babylon.quarks';
2
+ import {NumberField, Row, SelectField} from './fields.js';
3
+
4
+ /** A `[u, v]` pair of number inputs. */
5
+ function PairField(props: {value: [number, number]; step: number; onChange: (next: [number, number]) => void}) {
6
+ return (
7
+ <div style={{display: 'flex', gap: 6}}>
8
+ {[0, 1].map((i) => (
9
+ <NumberField
10
+ key={i}
11
+ value={props.value[i]}
12
+ step={props.step}
13
+ onChange={(v) => {
14
+ const next: [number, number] = [props.value[0], props.value[1]];
15
+ next[i] = v;
16
+ props.onChange(next);
17
+ }}
18
+ />
19
+ ))}
20
+ </div>
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Rows for how a texture is laid over the particle (`TextureUV`): its UV channel, then
26
+ * tiling and offset as Unity's material Tiling / Offset, then a scroll as Unreal's Panner.
27
+ * One field for the map, the erosion mask and the distortion noise alike.
28
+ */
29
+ export function TextureUVField(props: {
30
+ /** The complete layout as the system holds it. */
31
+ value: TextureUV;
32
+ /** Called with the whole layout, one field changed. */
33
+ onChange: (next: TextureUV) => void;
34
+ /** Prefix of each row's label, so several layouts in one module stay apart. Default none. */
35
+ labelPrefix?: string;
36
+ }) {
37
+ const prefix = props.labelPrefix ?? '';
38
+ const patch = (fields: Partial<TextureUV>) => props.onChange({...props.value, ...fields});
39
+ return (
40
+ <>
41
+ <Row label={`${prefix}UV channel`} hintKey="UV channel">
42
+ <SelectField<UVChannel>
43
+ value={props.value.channel}
44
+ options={[
45
+ {value: 0, label: 'UV0 (sprite / mesh UV)'},
46
+ {value: 1, label: 'UV1 (mesh second set, along a trail)'},
47
+ ]}
48
+ onChange={(channel) => patch({channel})}
49
+ />
50
+ </Row>
51
+ <Row label={`${prefix}Tiling`} hintKey="Tiling">
52
+ <PairField value={props.value.tiling} step={0.5} onChange={(tiling) => patch({tiling})} />
53
+ </Row>
54
+ <Row label={`${prefix}Offset`} hintKey="Offset">
55
+ <PairField value={props.value.offset} step={0.05} onChange={(offset) => patch({offset})} />
56
+ </Row>
57
+ <Row label={`${prefix}Scroll`} hintKey="Scroll">
58
+ <PairField value={props.value.scroll} step={0.05} onChange={(scroll) => patch({scroll})} />
59
+ </Row>
60
+ </>
61
+ );
62
+ }