babylon.quarks-editor 0.18.0 → 0.19.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 (83) hide show
  1. package/README.md +6 -6
  2. package/dist/core/binding.d.ts +1 -1
  3. package/dist/core/colors.d.ts +1 -1
  4. package/dist/core/defaultEffect.d.ts +1 -1
  5. package/dist/core/effectTree.d.ts +1 -1
  6. package/dist/core/effectTree.js +18 -4
  7. package/dist/core/emitterShapeWireframe.d.ts +1 -1
  8. package/dist/core/emitterShapeWireframe.js +1 -1
  9. package/dist/core/galleryDropMarker.js +1 -1
  10. package/dist/core/galleryScene.d.ts +1 -1
  11. package/dist/core/galleryTree.js +4 -1
  12. package/dist/core/loadEffect.d.ts +2 -2
  13. package/dist/core/loadEffect.js +1 -1
  14. package/dist/core/loadEffectGallery.d.ts +2 -2
  15. package/dist/core/shapes.d.ts +1 -1
  16. package/dist/core/shapes.js +35 -8
  17. package/dist/core/systems.d.ts +1 -1
  18. package/dist/core/systems.js +3 -1
  19. package/dist/core/timeline.d.ts +1 -2
  20. package/dist/core/values.d.ts +1 -1
  21. package/dist/index.d.ts +11 -11
  22. package/dist/index.js +6 -6
  23. package/dist/react/CurveEditor.d.ts +1 -2
  24. package/dist/react/CurveEditor.js +14 -3
  25. package/dist/react/EffectEditor.d.ts +2 -2
  26. package/dist/react/EffectEditor.js +2 -2
  27. package/dist/react/EffectEditorHost.d.ts +1 -1
  28. package/dist/react/EffectEditorHost.js +123 -33
  29. package/dist/react/GalleryPanel.d.ts +1 -1
  30. package/dist/react/GalleryPanel.js +14 -7
  31. package/dist/react/GradientEditor.d.ts +1 -2
  32. package/dist/react/GradientEditor.js +8 -1
  33. package/dist/react/MessageDialog.js +7 -1
  34. package/dist/react/ModuleSection.js +10 -3
  35. package/dist/react/ObjectModule.d.ts +1 -2
  36. package/dist/react/ObjectModule.js +1 -1
  37. package/dist/react/SubEmittersModule.d.ts +1 -2
  38. package/dist/react/TimelinePanel.d.ts +3 -3
  39. package/dist/react/TimelinePanel.js +55 -9
  40. package/dist/react/TimelineTrackRow.d.ts +1 -1
  41. package/dist/react/TimelineTrackRow.js +20 -6
  42. package/dist/react/ValueField.d.ts +1 -2
  43. package/dist/react/behaviorModules.js +19 -5
  44. package/dist/react/index.d.ts +17 -17
  45. package/dist/react/index.js +11 -11
  46. package/dist/react/modules.js +55 -25
  47. package/package.json +68 -68
  48. package/src/core/binding.ts +2 -2
  49. package/src/core/colors.ts +1 -1
  50. package/src/core/defaultEffect.ts +1 -1
  51. package/src/core/effectTree.ts +22 -7
  52. package/src/core/emitterShapeWireframe.ts +8 -4
  53. package/src/core/galleryDropMarker.ts +6 -2
  54. package/src/core/galleryPlayback.ts +1 -1
  55. package/src/core/galleryScene.ts +3 -7
  56. package/src/core/gallerySelectionMarker.ts +5 -1
  57. package/src/core/galleryTree.ts +8 -5
  58. package/src/core/loadEffect.ts +3 -3
  59. package/src/core/loadEffectGallery.ts +7 -3
  60. package/src/core/pickGalleryFiles.ts +4 -1
  61. package/src/core/shapes.ts +48 -10
  62. package/src/core/systems.ts +9 -3
  63. package/src/core/timeline.ts +8 -8
  64. package/src/core/values.ts +1 -1
  65. package/src/core/viewportPicker.ts +1 -1
  66. package/src/index.ts +18 -18
  67. package/src/react/CurveEditor.tsx +40 -10
  68. package/src/react/EffectEditor.tsx +14 -13
  69. package/src/react/EffectEditorHost.tsx +239 -64
  70. package/src/react/GalleryPanel.tsx +56 -12
  71. package/src/react/GradientEditor.tsx +21 -4
  72. package/src/react/InspectorInfoIcon.tsx +6 -2
  73. package/src/react/MessageDialog.tsx +9 -1
  74. package/src/react/ModuleSection.tsx +12 -3
  75. package/src/react/ObjectModule.tsx +36 -15
  76. package/src/react/PromptDialog.tsx +5 -1
  77. package/src/react/SubEmittersModule.tsx +1 -2
  78. package/src/react/TimelinePanel.tsx +94 -21
  79. package/src/react/TimelineTrackRow.tsx +71 -18
  80. package/src/react/ValueField.tsx +0 -1
  81. package/src/react/behaviorModules.tsx +143 -29
  82. package/src/react/index.ts +33 -33
  83. package/src/react/modules.tsx +173 -57
@@ -1,47 +1,43 @@
1
- import React, {useEffect, useMemo, useReducer, useRef, useState, useCallback} from 'react';
2
- import {createRoot} from 'react-dom/client';
1
+ import {ArcRotateCamera} from '@babylonjs/core/Cameras/arcRotateCamera';
2
+ import {Engine} from '@babylonjs/core/Engines/engine';
3
+ import {PointerEventTypes} from '@babylonjs/core/Events/pointerEvents';
4
+ import {GizmoManager} from '@babylonjs/core/Gizmos/gizmoManager';
5
+ import {DirectionalLight} from '@babylonjs/core/Lights/directionalLight';
6
+ import {Color3, Color4} from '@babylonjs/core/Maths/math.color';
7
+ import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
8
+ import type {Mesh} from '@babylonjs/core/Meshes/mesh';
9
+ import {MeshBuilder} from '@babylonjs/core/Meshes/meshBuilder';
10
+ import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
11
+ import {UtilityLayerRenderer} from '@babylonjs/core/Rendering/utilityLayerRenderer';
12
+ import {Scene} from '@babylonjs/core/scene';
13
+ import {GridMaterial} from '@babylonjs/materials/grid/gridMaterial';
3
14
  import {
4
15
  ArrowLeftIcon,
5
16
  ArrowPathIcon,
17
+ ArrowsPointingOutIcon,
6
18
  ArrowUturnLeftIcon,
7
19
  ArrowUturnRightIcon,
8
- ArrowsPointingOutIcon,
9
20
  CubeTransparentIcon,
10
21
  CursorArrowRaysIcon,
11
22
  Square2StackIcon,
12
23
  } from '@heroicons/react/24/solid';
13
- import {PointerEventTypes} from '@babylonjs/core/Events/pointerEvents';
14
- import {Engine} from '@babylonjs/core/Engines/engine';
15
- import {Scene} from '@babylonjs/core/scene';
16
- import {ArcRotateCamera} from '@babylonjs/core/Cameras/arcRotateCamera';
17
- import {DirectionalLight} from '@babylonjs/core/Lights/directionalLight';
18
- import {Vector3 as BVector3} from '@babylonjs/core/Maths/math.vector';
19
- import {Color3, Color4} from '@babylonjs/core/Maths/math.color';
20
- import {MeshBuilder} from '@babylonjs/core/Meshes/meshBuilder';
21
- import {TransformNode} from '@babylonjs/core/Meshes/transformNode';
22
- import {GizmoManager} from '@babylonjs/core/Gizmos/gizmoManager';
23
- import {UtilityLayerRenderer} from '@babylonjs/core/Rendering/utilityLayerRenderer';
24
- import {GridMaterial} from '@babylonjs/materials/grid/gridMaterial';
25
- import {
26
- BatchedRenderer,
27
- ParticleSystem,
28
- QuarksUtil,
29
- } from 'babylon.quarks';
24
+ import {BatchedRenderer, ParticleSystem, QuarksUtil} from 'babylon.quarks';
25
+ import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
26
+ import {createRoot} from 'react-dom/client';
30
27
  import {EffectBinding} from '../core/binding';
31
- import {EffectHistory} from '../core/history';
32
28
  import {ensureGroundResolver} from '../core/collision';
33
29
  import {createGalleryDefaultEntry} from '../core/defaultEffect';
34
- import {loadEffectFromJson, disposeLoadedEffect, parseEffectFromJson} from '../core/loadEffect';
35
- import {applyGalleryPlayback, countActivePreviews, countGalleryParticles, hasActivePreview, pauseFinishedPreviews} from '../core/galleryPlayback';
36
- import {createPlaybackState, clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, type PlaybackState} from '../core/playback';
37
- import {pauseFocus, scrubFocusToSettled} from '../core/scrub';
38
- import {logRenderMismatch} from '../core/renderDebug';
39
- import {cancelGalleryCameraFocus, focusGalleryEntry} from '../core/galleryLayout';
40
- import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
41
- import {GalleryDropMarker} from '../core/galleryDropMarker';
42
- import {GallerySelectionMarker} from '../core/gallerySelectionMarker';
43
30
  import {EmitterShapeWireframes} from '../core/emitterShapeWireframe';
44
31
  import {getGalleryDragSession, setGalleryDragSession} from '../core/galleryDragSession';
32
+ import {GalleryDropMarker} from '../core/galleryDropMarker';
33
+ import {cancelGalleryCameraFocus, focusGalleryEntry} from '../core/galleryLayout';
34
+ import {
35
+ applyGalleryPlayback,
36
+ countActivePreviews,
37
+ countGalleryParticles,
38
+ hasActivePreview,
39
+ pauseFinishedPreviews,
40
+ } from '../core/galleryPlayback';
45
41
  import {
46
42
  clearGalleryEntrySimulation,
47
43
  pickGroundPosition,
@@ -49,20 +45,41 @@ import {
49
45
  placeGalleryEntryInScene,
50
46
  removeGalleryEntryFromScene,
51
47
  } from '../core/galleryScene';
48
+ import {GallerySelectionMarker} from '../core/gallerySelectionMarker';
52
49
  import {describeGalleryDrag, GALLERY_DRAG_MIME, readGalleryDrag} from '../core/galleryTree';
53
- import {loadEffectGallery, collectJsonFiles, bindingFromGalleryEntry, mergeGalleryEntries, type GalleryEntry, type GalleryLoadProgress} from '../core/loadEffectGallery';
50
+ import {EffectHistory} from '../core/history';
51
+ import {disposeLoadedEffect, loadEffectFromJson, parseEffectFromJson} from '../core/loadEffect';
52
+ import {
53
+ bindingFromGalleryEntry,
54
+ collectJsonFiles,
55
+ loadEffectGallery,
56
+ mergeGalleryEntries,
57
+ type GalleryEntry,
58
+ type GalleryLoadProgress,
59
+ } from '../core/loadEffectGallery';
54
60
  import {pickGalleryJsonFiles} from '../core/pickGalleryFiles';
55
- import {GalleryViewportPicker} from '../core/viewportPicker';
61
+ import {
62
+ clearFocusFinished,
63
+ createPlaybackState,
64
+ getFocusElapsed,
65
+ isFocusFinished,
66
+ markFocusFinished,
67
+ setFocusElapsed,
68
+ type PlaybackState,
69
+ } from '../core/playback';
70
+ import {logRenderMismatch} from '../core/renderDebug';
71
+ import {pauseFocus, scrubFocusToSettled} from '../core/scrub';
72
+ import {computeTimelineRows, computeTimelineSpan} from '../core/timeline';
56
73
  import {notifyTransformChanged} from '../core/transformStore';
74
+ import {GalleryViewportPicker} from '../core/viewportPicker';
57
75
  import {EffectEditor} from './EffectEditor';
58
- import {PromptDialog} from './PromptDialog';
59
- import {MessageDialog} from './MessageDialog';
60
76
  import {GalleryPanel} from './GalleryPanel';
77
+ import {MessageDialog} from './MessageDialog';
78
+ import {PromptDialog} from './PromptDialog';
61
79
  import {TimelinePanel} from './TimelinePanel';
80
+ import {iconStyle, type HeroIcon} from './icons';
62
81
  import type {GeometryData, GeometryOption, TextureOption} from './modules';
63
82
  import {buttonStyle, globalEditorStyles, theme} from './theme';
64
- import {iconStyle, type HeroIcon} from './icons';
65
- import type {Mesh} from '@babylonjs/core/Meshes/mesh';
66
83
 
67
84
  export interface EffectEditorHostHandle {
68
85
  binding: EffectBinding;
@@ -145,7 +162,9 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
145
162
  const selectGalleryEntryRef = useRef<(entry: GalleryEntry) => void>(() => {});
146
163
  const toggleGalleryPreviewRef = useRef<(entry: GalleryEntry) => void>(() => {});
147
164
  const removeGalleryFromSceneRef = useRef<(entry: GalleryEntry) => void>(() => {});
148
- const placeGalleryDropRef = useRef<(clientX: number, clientY: number, dataTransfer?: DataTransfer) => void>(() => {});
165
+ const placeGalleryDropRef = useRef<(clientX: number, clientY: number, dataTransfer?: DataTransfer) => void>(
166
+ () => {}
167
+ );
149
168
  const createGalleryDefaultRef = useRef<() => void>(() => {});
150
169
  const dropMarkerRef = useRef<GalleryDropMarker | null>(null);
151
170
  const selectionMarkerRef = useRef<GallerySelectionMarker | null>(null);
@@ -478,7 +497,9 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
478
497
  const appendGalleryFromFiles = async (files: FileList | File[]) => {
479
498
  setGalleryLoading({done: 0, total: 0, loaded: 0, failed: 0});
480
499
 
481
- const incoming = await loadEffectGallery(scene, files, galleryRoot, (progress) => setGalleryLoading(progress));
500
+ const incoming = await loadEffectGallery(scene, files, galleryRoot, (progress) =>
501
+ setGalleryLoading(progress)
502
+ );
482
503
  const {merged, added} = mergeGalleryEntries(galleryEntries, incoming, renderer);
483
504
 
484
505
  galleryEntries = merged;
@@ -541,10 +562,19 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
541
562
  }
542
563
  if (current) {
543
564
  const catalogEntry = stateRef.current?.galleryEntries.find((entry) => entry.root === current.root);
544
- disposeEffect(current.root, [current.system, ...current.subSystems], renderer, catalogEntry?.inScene ?? true);
565
+ disposeEffect(
566
+ current.root,
567
+ [current.system, ...current.subSystems],
568
+ renderer,
569
+ catalogEntry?.inScene ?? true
570
+ );
545
571
  }
546
572
  const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
547
- const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
573
+ const next = new EffectBinding(
574
+ main,
575
+ systems.filter((s) => s !== main),
576
+ root
577
+ );
548
578
  setSelectedGalleryRoot(null);
549
579
  setPreviewRootIds([]);
550
580
  selectionMarker.hide();
@@ -667,7 +697,11 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
667
697
 
668
698
  const previewIds = new Set(snapshot.previewRootIds);
669
699
  const binding = snapshot.binding;
670
- if (binding && (binding.root.uniqueId !== focusTimelineMetaRef.current.rootId || binding.getRevision() !== focusTimelineMetaRef.current.revision)) {
700
+ if (
701
+ binding &&
702
+ (binding.root.uniqueId !== focusTimelineMetaRef.current.rootId ||
703
+ binding.getRevision() !== focusTimelineMetaRef.current.revision)
704
+ ) {
671
705
  const rows = computeTimelineRows(binding);
672
706
  focusTimelineMetaRef.current = {
673
707
  rootId: binding.root.uniqueId,
@@ -787,7 +821,14 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
787
821
  gm.positionGizmoEnabled = gizmoMode === 'position';
788
822
  gm.rotationGizmoEnabled = gizmoMode === 'rotation';
789
823
  gm.scaleGizmoEnabled = gizmoMode === 'scale';
790
- const activeGizmo = gizmoMode === 'position' ? gm.gizmos.positionGizmo : gizmoMode === 'rotation' ? gm.gizmos.rotationGizmo : gizmoMode === 'scale' ? gm.gizmos.scaleGizmo : null;
824
+ const activeGizmo =
825
+ gizmoMode === 'position'
826
+ ? gm.gizmos.positionGizmo
827
+ : gizmoMode === 'rotation'
828
+ ? gm.gizmos.rotationGizmo
829
+ : gizmoMode === 'scale'
830
+ ? gm.gizmos.scaleGizmo
831
+ : null;
791
832
  let dragRaf = 0;
792
833
  const scheduleTransformRefresh = () => {
793
834
  if (dragRaf !== 0) {
@@ -872,7 +913,11 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
872
913
  root.name = prev.name;
873
914
  }
874
915
  const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
875
- const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
916
+ const next = new EffectBinding(
917
+ main,
918
+ systems.filter((s) => s !== main),
919
+ root
920
+ );
876
921
  const oldRootId = current.root.uniqueId;
877
922
  const updated = [...state.galleryEntries];
878
923
  updated[entryIndex] = {name: prev.name, path: prev.path, root, systems, inScene: wasInScene};
@@ -905,7 +950,11 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
905
950
  }
906
951
  disposeEffect(current.root, [current.system, ...current.subSystems], state.renderer);
907
952
  const main = systems.find((s) => !s.onlyUsedByOther) ?? systems[0];
908
- const next = new EffectBinding(main, systems.filter((s) => s !== main), root);
953
+ const next = new EffectBinding(
954
+ main,
955
+ systems.filter((s) => s !== main),
956
+ root
957
+ );
909
958
  next.root.parent = null;
910
959
  state.history.attach(next);
911
960
  next.subscribe(force);
@@ -927,7 +976,7 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
927
976
  const activeSystem = binding ? (allSystems.find((s) => s.emitter === selectedNode) ?? binding.system) : null;
928
977
  const gizmoTargetLabel = gizmoNode?.name || 'Node';
929
978
  const selectedGalleryEntry = selectedGalleryRoot
930
- ? gallery.find((entry) => entry.root === selectedGalleryRoot) ?? null
979
+ ? (gallery.find((entry) => entry.root === selectedGalleryRoot) ?? null)
931
980
  : null;
932
981
  /** Timeline is always shown when an effect is selected; playback only when it is on stage. */
933
982
  const showTimeline = Boolean(binding && activeSystem && state);
@@ -952,7 +1001,18 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
952
1001
  );
953
1002
 
954
1003
  return (
955
- <div className="qe-root" style={{display: 'flex', flexDirection: 'column', width: '100%', height: '100%', fontFamily: theme.font, color: theme.text, background: '#070b16'}}>
1004
+ <div
1005
+ className="qe-root"
1006
+ style={{
1007
+ display: 'flex',
1008
+ flexDirection: 'column',
1009
+ width: '100%',
1010
+ height: '100%',
1011
+ fontFamily: theme.font,
1012
+ color: theme.text,
1013
+ background: '#070b16',
1014
+ }}
1015
+ >
956
1016
  <style>{globalEditorStyles}</style>
957
1017
  <div style={{display: 'flex', flex: 1, minHeight: 0}}>
958
1018
  <aside
@@ -974,7 +1034,16 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
974
1034
  Drag into viewport to place · ✓ preview · × remove from stage
975
1035
  </div>
976
1036
  </div>
977
- <div style={{flex: 1, minHeight: 0, overflow: 'hidden', padding: '8px 12px 14px', display: 'flex', flexDirection: 'column'}}>
1037
+ <div
1038
+ style={{
1039
+ flex: 1,
1040
+ minHeight: 0,
1041
+ overflow: 'hidden',
1042
+ padding: '8px 12px 14px',
1043
+ display: 'flex',
1044
+ flexDirection: 'column',
1045
+ }}
1046
+ >
978
1047
  <GalleryPanel
979
1048
  entries={gallery}
980
1049
  focusedRoot={selectedGalleryRoot}
@@ -1028,7 +1097,10 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1028
1097
  placeGalleryDropRef.current(e.clientX, e.clientY, e.dataTransfer);
1029
1098
  }}
1030
1099
  >
1031
- <canvas ref={canvasRef} style={{width: '100%', height: '100%', display: 'block', touchAction: 'none', outline: 'none'}} />
1100
+ <canvas
1101
+ ref={canvasRef}
1102
+ style={{width: '100%', height: '100%', display: 'block', touchAction: 'none', outline: 'none'}}
1103
+ />
1032
1104
  <div
1033
1105
  style={{
1034
1106
  position: 'absolute',
@@ -1073,7 +1145,8 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1073
1145
  whiteSpace: 'nowrap',
1074
1146
  }}
1075
1147
  >
1076
- Drop {galleryDragHover.count === 1 ? 'effect' : `${galleryDragHover.count} effects`} — {galleryDragHover.label}
1148
+ Drop {galleryDragHover.count === 1 ? 'effect' : `${galleryDragHover.count} effects`} —{' '}
1149
+ {galleryDragHover.label}
1077
1150
  </div>
1078
1151
  </div>
1079
1152
  )}
@@ -1089,7 +1162,15 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1089
1162
  pointerEvents: 'none',
1090
1163
  }}
1091
1164
  >
1092
- <div style={{background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 10, padding: '16px 22px', textAlign: 'center'}}>
1165
+ <div
1166
+ style={{
1167
+ background: theme.panelBg,
1168
+ border: `1px solid ${theme.border}`,
1169
+ borderRadius: 10,
1170
+ padding: '16px 22px',
1171
+ textAlign: 'center',
1172
+ }}
1173
+ >
1093
1174
  <div style={{fontWeight: 600, marginBottom: 6}}>Loading gallery…</div>
1094
1175
  <div style={{fontSize: 13, color: theme.textDim}}>
1095
1176
  {galleryLoading.done} / {galleryLoading.total || '…'} files
@@ -1124,21 +1205,65 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1124
1205
  {props.backLink.label.replace(/^←\s*/, '')}
1125
1206
  </a>
1126
1207
  )}
1127
- <div style={{position: 'absolute', top: 12, right: 12, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6}}>
1128
- <div style={{display: 'flex', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4}}>
1129
- <button className="qe-hover" style={squareButtonStyle} title="Undo (Ctrl+Z)" disabled={!state?.history.canUndo || !binding} onClick={() => historyAction(state!.history.undo())}>
1208
+ <div
1209
+ style={{
1210
+ position: 'absolute',
1211
+ top: 12,
1212
+ right: 12,
1213
+ display: 'flex',
1214
+ flexDirection: 'column',
1215
+ alignItems: 'flex-end',
1216
+ gap: 6,
1217
+ }}
1218
+ >
1219
+ <div
1220
+ style={{
1221
+ display: 'flex',
1222
+ gap: 4,
1223
+ background: theme.panelBg,
1224
+ border: `1px solid ${theme.border}`,
1225
+ borderRadius: 8,
1226
+ padding: 4,
1227
+ }}
1228
+ >
1229
+ <button
1230
+ className="qe-hover"
1231
+ style={squareButtonStyle}
1232
+ title="Undo (Ctrl+Z)"
1233
+ disabled={!state?.history.canUndo || !binding}
1234
+ onClick={() => historyAction(state!.history.undo())}
1235
+ >
1130
1236
  <ArrowUturnLeftIcon style={iconStyle(14)} />
1131
1237
  </button>
1132
- <button className="qe-hover" style={squareButtonStyle} title="Redo (Ctrl+Shift+Z)" disabled={!state?.history.canRedo || !binding} onClick={() => historyAction(state!.history.redo())}>
1238
+ <button
1239
+ className="qe-hover"
1240
+ style={squareButtonStyle}
1241
+ title="Redo (Ctrl+Shift+Z)"
1242
+ disabled={!state?.history.canRedo || !binding}
1243
+ onClick={() => historyAction(state!.history.redo())}
1244
+ >
1133
1245
  <ArrowUturnRightIcon style={iconStyle(14)} />
1134
1246
  </button>
1135
1247
  </div>
1136
- <div style={{display: 'flex', flexDirection: 'column', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4}}>
1248
+ <div
1249
+ style={{
1250
+ display: 'flex',
1251
+ flexDirection: 'column',
1252
+ gap: 4,
1253
+ background: theme.panelBg,
1254
+ border: `1px solid ${theme.border}`,
1255
+ borderRadius: 8,
1256
+ padding: 4,
1257
+ }}
1258
+ >
1137
1259
  {GIZMO_MODE_BUTTONS.map((m) => (
1138
1260
  <button
1139
1261
  key={m.value}
1140
1262
  className="qe-hover"
1141
- style={{...squareButtonStyle, ...(gizmoMode === m.value ? activeSquareButtonStyle : {})}}
1263
+ style={{
1264
+ ...squareButtonStyle,
1265
+ ...(gizmoMode === m.value ? activeSquareButtonStyle : {}),
1266
+ }}
1142
1267
  title={m.title}
1143
1268
  onClick={() => setGizmoMode(m.value)}
1144
1269
  >
@@ -1146,10 +1271,23 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1146
1271
  </button>
1147
1272
  ))}
1148
1273
  </div>
1149
- <div style={{display: 'flex', flexDirection: 'column', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4}}>
1274
+ <div
1275
+ style={{
1276
+ display: 'flex',
1277
+ flexDirection: 'column',
1278
+ gap: 4,
1279
+ background: theme.panelBg,
1280
+ border: `1px solid ${theme.border}`,
1281
+ borderRadius: 8,
1282
+ padding: 4,
1283
+ }}
1284
+ >
1150
1285
  <button
1151
1286
  className="qe-hover"
1152
- style={{...squareButtonStyle, ...(showEmitterWireframes ? activeSquareButtonStyle : {})}}
1287
+ style={{
1288
+ ...squareButtonStyle,
1289
+ ...(showEmitterWireframes ? activeSquareButtonStyle : {}),
1290
+ }}
1153
1291
  title="Show emitter shape wireframes"
1154
1292
  disabled={!binding}
1155
1293
  onClick={() => setShowEmitterWireframes((on) => !on)}
@@ -1159,15 +1297,41 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1159
1297
  </div>
1160
1298
  </div>
1161
1299
  </div>
1162
- <aside style={{width: 340, flexShrink: 0, borderLeft: `1px solid ${theme.border}`, background: theme.panelBg, display: 'flex', flexDirection: 'column', minHeight: 0}}>
1300
+ <aside
1301
+ style={{
1302
+ width: 340,
1303
+ flexShrink: 0,
1304
+ borderLeft: `1px solid ${theme.border}`,
1305
+ background: theme.panelBg,
1306
+ display: 'flex',
1307
+ flexDirection: 'column',
1308
+ minHeight: 0,
1309
+ }}
1310
+ >
1163
1311
  <div style={{padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}`}}>
1164
- <div style={{fontSize: 11, fontWeight: 600, color: theme.textDim, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 6}}>
1312
+ <div
1313
+ style={{
1314
+ fontSize: 11,
1315
+ fontWeight: 600,
1316
+ color: theme.textDim,
1317
+ textTransform: 'uppercase',
1318
+ letterSpacing: 0.4,
1319
+ marginBottom: 6,
1320
+ }}
1321
+ >
1165
1322
  Inspector
1166
1323
  </div>
1167
1324
  <div
1168
1325
  className="qe-hover-bg"
1169
1326
  title={binding ? 'Rename effect' : undefined}
1170
- style={{fontSize: 16, fontWeight: 600, cursor: binding ? 'pointer' : 'default', borderRadius: 6, padding: '2px 4px', margin: '-2px -4px'}}
1327
+ style={{
1328
+ fontSize: 16,
1329
+ fontWeight: 600,
1330
+ cursor: binding ? 'pointer' : 'default',
1331
+ borderRadius: 6,
1332
+ padding: '2px 4px',
1333
+ margin: '-2px -4px',
1334
+ }}
1171
1335
  onClick={() => binding && setRenamingRoot(true)}
1172
1336
  >
1173
1337
  {(binding?.root.name || props.title) ?? 'Effect editor'}
@@ -1190,7 +1354,9 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1190
1354
  disabled={!binding}
1191
1355
  onClick={() => {
1192
1356
  if (!binding) return;
1193
- const blob = new Blob([binding.exportJSON('EditorEffect')], {type: 'application/json'});
1357
+ const blob = new Blob([binding.exportJSON('EditorEffect')], {
1358
+ type: 'application/json',
1359
+ });
1194
1360
  const link = document.createElement('a');
1195
1361
  link.href = URL.createObjectURL(blob);
1196
1362
  link.download = 'effect.json';
@@ -1201,7 +1367,12 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1201
1367
  Export
1202
1368
  </button>
1203
1369
  {props.onSave && (
1204
- <button className="qe-hover" style={buttonStyle} disabled={!binding} onClick={() => binding && props.onSave!(binding.exportJSON('EditorEffect'))}>
1370
+ <button
1371
+ className="qe-hover"
1372
+ style={buttonStyle}
1373
+ disabled={!binding}
1374
+ onClick={() => binding && props.onSave!(binding.exportJSON('EditorEffect'))}
1375
+ >
1205
1376
  Save
1206
1377
  </button>
1207
1378
  )}
@@ -1243,7 +1414,11 @@ export function EffectEditorHost(props: EffectEditorHostProps) {
1243
1414
  />
1244
1415
  )}
1245
1416
  {messageDialog && (
1246
- <MessageDialog title={messageDialog.title} message={messageDialog.message} onClose={() => setMessageDialog(null)} />
1417
+ <MessageDialog
1418
+ title={messageDialog.title}
1419
+ message={messageDialog.message}
1420
+ onClose={() => setMessageDialog(null)}
1421
+ />
1247
1422
  )}
1248
1423
  </div>
1249
1424
  );
@@ -1,5 +1,4 @@
1
- import React, {useCallback, useMemo, useRef, useState} from 'react';
2
- import {useVirtualizer} from '@tanstack/react-virtual';
1
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
2
  import {
4
3
  CheckCircleIcon,
5
4
  ChevronDownIcon,
@@ -9,8 +8,8 @@ import {
9
8
  SparklesIcon,
10
9
  XMarkIcon,
11
10
  } from '@heroicons/react/24/solid';
12
- import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
13
- import type {GalleryEntry, GalleryLoadProgress} from '../core/loadEffectGallery';
11
+ import {useVirtualizer} from '@tanstack/react-virtual';
12
+ import React, {useCallback, useMemo, useRef, useState} from 'react';
14
13
  import {setGalleryDragSession} from '../core/galleryDragSession';
15
14
  import {
16
15
  buildGalleryTree,
@@ -20,6 +19,7 @@ import {
20
19
  type GalleryFlatRow,
21
20
  type GalleryTreeFolder,
22
21
  } from '../core/galleryTree';
22
+ import type {GalleryEntry, GalleryLoadProgress} from '../core/loadEffectGallery';
23
23
  import {iconStyle} from './icons';
24
24
  import {buttonStyle, hiddenFileInputStyle, inputStyle, theme} from './theme';
25
25
 
@@ -111,7 +111,15 @@ function EffectRow(props: {
111
111
  {inPreview ? (
112
112
  <CheckCircleIcon style={iconStyle(14)} />
113
113
  ) : (
114
- <span style={{width: 10, height: 10, border: `1px solid currentColor`, borderRadius: 2, boxSizing: 'border-box'}} />
114
+ <span
115
+ style={{
116
+ width: 10,
117
+ height: 10,
118
+ border: `1px solid currentColor`,
119
+ borderRadius: 2,
120
+ boxSizing: 'border-box',
121
+ }}
122
+ />
115
123
  )}
116
124
  </button>
117
125
  ) : (
@@ -166,7 +174,10 @@ function EffectRow(props: {
166
174
  </span>
167
175
  <span style={{overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{entry.name}</span>
168
176
  {parkedOnStage && (
169
- <span title="On stage but not in preview" style={{flexShrink: 0, fontSize: 10, color: theme.textDim, letterSpacing: 0.3}}>
177
+ <span
178
+ title="On stage but not in preview"
179
+ style={{flexShrink: 0, fontSize: 10, color: theme.textDim, letterSpacing: 0.3}}
180
+ >
170
181
  parked
171
182
  </span>
172
183
  )}
@@ -258,7 +269,9 @@ function FolderRow(props: {
258
269
  <span style={{width: 14, display: 'inline-flex', color: theme.accent}}>
259
270
  {open ? <FolderOpenIcon style={iconStyle(14)} /> : <FolderIcon style={iconStyle(14)} />}
260
271
  </span>
261
- <span style={{flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>{node.name}</span>
272
+ <span style={{flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap'}}>
273
+ {node.name}
274
+ </span>
262
275
  {effectCount > 0 && (
263
276
  <span style={{fontSize: 11, fontWeight: 500, color: theme.textDim, flexShrink: 0}}>{effectCount}</span>
264
277
  )}
@@ -280,7 +293,16 @@ function VirtualGalleryTree(props: {
280
293
  onRemoveFromScene: (entry: GalleryEntry) => void;
281
294
  onToggleFolder: (path: string) => void;
282
295
  }): React.ReactElement {
283
- const {rows, closedFolders, focusedRoot, previewRootIds, onFocus, onTogglePreview, onRemoveFromScene, onToggleFolder} = props;
296
+ const {
297
+ rows,
298
+ closedFolders,
299
+ focusedRoot,
300
+ previewRootIds,
301
+ onFocus,
302
+ onTogglePreview,
303
+ onRemoveFromScene,
304
+ onToggleFolder,
305
+ } = props;
284
306
  const scrollRef = useRef<HTMLDivElement>(null);
285
307
  const virtualizer = useVirtualizer({
286
308
  count: rows.length,
@@ -346,7 +368,19 @@ function VirtualGalleryTree(props: {
346
368
 
347
369
  /** Folder-tree catalog; import effects here and drag them into the viewport. */
348
370
  export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
349
- const {entries, focusedRoot, previewRootIds, loading, folderInputRef, onFocus, onTogglePreview, onRemoveFromScene, onImportFiles, onImportFolder, onCreateDefault} = props;
371
+ const {
372
+ entries,
373
+ focusedRoot,
374
+ previewRootIds,
375
+ loading,
376
+ folderInputRef,
377
+ onFocus,
378
+ onTogglePreview,
379
+ onRemoveFromScene,
380
+ onImportFiles,
381
+ onImportFolder,
382
+ onCreateDefault,
383
+ } = props;
350
384
  const [query, setQuery] = useState('');
351
385
  const [closedFolders, setClosedFolders] = useState<Set<string>>(() => new Set());
352
386
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -380,7 +414,13 @@ export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
380
414
  return (
381
415
  <div style={{display: 'flex', flexDirection: 'column', gap: 10, height: '100%', minHeight: 0}}>
382
416
  <div style={{display: 'flex', flexWrap: 'wrap', gap: 6}}>
383
- <button type="button" className="qe-hover" style={buttonStyle} disabled={isLoading} onClick={onCreateDefault}>
417
+ <button
418
+ type="button"
419
+ className="qe-hover"
420
+ style={buttonStyle}
421
+ disabled={isLoading}
422
+ onClick={onCreateDefault}
423
+ >
384
424
  New effect
385
425
  </button>
386
426
  <label
@@ -422,7 +462,10 @@ export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
422
462
  opacity: isLoading ? 0.5 : 1,
423
463
  }}
424
464
  onClick={(e) => {
425
- if (typeof (window as Window & {showDirectoryPicker?: unknown}).showDirectoryPicker === 'function') {
465
+ if (
466
+ typeof (window as Window & {showDirectoryPicker?: unknown}).showDirectoryPicker ===
467
+ 'function'
468
+ ) {
426
469
  e.preventDefault();
427
470
  onImportFolder();
428
471
  }
@@ -483,7 +526,8 @@ export function GalleryPanel(props: GalleryPanelProps): React.ReactElement {
483
526
  background: theme.sectionBg,
484
527
  }}
485
528
  >
486
- Catalog is empty. Click <strong>New effect</strong>, or <strong>Import effect</strong> / <strong>Import folder</strong>.
529
+ Catalog is empty. Click <strong>New effect</strong>, or <strong>Import effect</strong> /{' '}
530
+ <strong>Import folder</strong>.
487
531
  </div>
488
532
  ) : filtered.length === 0 ? (
489
533
  <div
@@ -1,4 +1,4 @@
1
- import React, {useRef, useState} from 'react';
1
+ import {useRef, useState} from 'react';
2
2
  import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
3
3
  import {NumberField, Row} from './fields';
4
4
  import {buttonStyle, theme} from './theme';
@@ -99,14 +99,31 @@ export function GradientEditor(props: {stops: GradientStop[]; onChange: (stops:
99
99
  className="qe-hover"
100
100
  value={rgbToHex(current.r, current.g, current.b)}
101
101
  onChange={(e) => updateStop(selected, hexToRgb(e.target.value))}
102
- style={{width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer'}}
102
+ style={{
103
+ width: 42,
104
+ height: 24,
105
+ border: 'none',
106
+ background: 'transparent',
107
+ padding: 0,
108
+ cursor: 'pointer',
109
+ }}
103
110
  />
104
111
  </Row>
105
112
  <Row label="Stop alpha">
106
- <NumberField value={current.a} min={0} step={0.05} onChange={(a) => updateStop(selected, {a: Math.min(1, a)})} />
113
+ <NumberField
114
+ value={current.a}
115
+ min={0}
116
+ step={0.05}
117
+ onChange={(a) => updateStop(selected, {a: Math.min(1, a)})}
118
+ />
107
119
  </Row>
108
120
  <Row label="Stop position">
109
- <NumberField value={current.pos} min={0} step={0.05} onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})} />
121
+ <NumberField
122
+ value={current.pos}
123
+ min={0}
124
+ step={0.05}
125
+ onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})}
126
+ />
110
127
  </Row>
111
128
  {stops.length > 2 && (
112
129
  <button