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,48 +1,48 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useReducer, useRef, useState, useCallback } from 'react';
3
- import { createRoot } from 'react-dom/client';
4
- import { ArrowLeftIcon, ArrowPathIcon, ArrowUturnLeftIcon, ArrowUturnRightIcon, ArrowsPointingOutIcon, CubeTransparentIcon, CursorArrowRaysIcon, Square2StackIcon, } from '@heroicons/react/24/solid';
5
- import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents';
6
- import { Engine } from '@babylonjs/core/Engines/engine';
7
- import { Scene } from '@babylonjs/core/scene';
8
2
  import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
3
+ import { Engine } from '@babylonjs/core/Engines/engine';
4
+ import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents';
5
+ import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
9
6
  import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight';
10
- import { Vector3 as BVector3 } from '@babylonjs/core/Maths/math.vector';
11
7
  import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
8
+ import { Vector3 as BVector3 } from '@babylonjs/core/Maths/math.vector';
12
9
  import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
13
10
  import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
14
- import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
15
11
  import { UtilityLayerRenderer } from '@babylonjs/core/Rendering/utilityLayerRenderer';
12
+ import { Scene } from '@babylonjs/core/scene';
16
13
  import { GridMaterial } from '@babylonjs/materials/grid/gridMaterial';
17
- import { BatchedRenderer, QuarksUtil, } from 'babylon.quarks';
14
+ import { ArrowLeftIcon, ArrowPathIcon, ArrowsPointingOutIcon, ArrowUturnLeftIcon, ArrowUturnRightIcon, CubeTransparentIcon, CursorArrowRaysIcon, Square2StackIcon, } from '@heroicons/react/24/solid';
15
+ import { BatchedRenderer, QuarksUtil } from 'babylon.quarks';
16
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
17
+ import { createRoot } from 'react-dom/client';
18
18
  import { EffectBinding } from '../core/binding';
19
- import { EffectHistory } from '../core/history';
20
19
  import { ensureGroundResolver } from '../core/collision';
21
20
  import { createGalleryDefaultEntry } from '../core/defaultEffect';
22
- import { loadEffectFromJson, disposeLoadedEffect, parseEffectFromJson } from '../core/loadEffect';
23
- import { applyGalleryPlayback, countActivePreviews, countGalleryParticles, hasActivePreview, pauseFinishedPreviews } from '../core/galleryPlayback';
24
- import { createPlaybackState, clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed } from '../core/playback';
25
- import { pauseFocus, scrubFocusToSettled } from '../core/scrub';
26
- import { logRenderMismatch } from '../core/renderDebug';
27
- import { cancelGalleryCameraFocus, focusGalleryEntry } from '../core/galleryLayout';
28
- import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
29
- import { GalleryDropMarker } from '../core/galleryDropMarker';
30
- import { GallerySelectionMarker } from '../core/gallerySelectionMarker';
31
21
  import { EmitterShapeWireframes } from '../core/emitterShapeWireframe';
32
22
  import { getGalleryDragSession, setGalleryDragSession } from '../core/galleryDragSession';
23
+ import { GalleryDropMarker } from '../core/galleryDropMarker';
24
+ import { cancelGalleryCameraFocus, focusGalleryEntry } from '../core/galleryLayout';
25
+ import { applyGalleryPlayback, countActivePreviews, countGalleryParticles, hasActivePreview, pauseFinishedPreviews, } from '../core/galleryPlayback';
33
26
  import { clearGalleryEntrySimulation, pickGroundPosition, placeGalleryEntriesAt, placeGalleryEntryInScene, removeGalleryEntryFromScene, } from '../core/galleryScene';
27
+ import { GallerySelectionMarker } from '../core/gallerySelectionMarker';
34
28
  import { describeGalleryDrag, GALLERY_DRAG_MIME, readGalleryDrag } from '../core/galleryTree';
35
- import { loadEffectGallery, collectJsonFiles, bindingFromGalleryEntry, mergeGalleryEntries } from '../core/loadEffectGallery';
29
+ import { EffectHistory } from '../core/history';
30
+ import { disposeLoadedEffect, loadEffectFromJson, parseEffectFromJson } from '../core/loadEffect';
31
+ import { bindingFromGalleryEntry, collectJsonFiles, loadEffectGallery, mergeGalleryEntries, } from '../core/loadEffectGallery';
36
32
  import { pickGalleryJsonFiles } from '../core/pickGalleryFiles';
37
- import { GalleryViewportPicker } from '../core/viewportPicker';
33
+ import { clearFocusFinished, createPlaybackState, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, } from '../core/playback';
34
+ import { logRenderMismatch } from '../core/renderDebug';
35
+ import { pauseFocus, scrubFocusToSettled } from '../core/scrub';
36
+ import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
38
37
  import { notifyTransformChanged } from '../core/transformStore';
38
+ import { GalleryViewportPicker } from '../core/viewportPicker';
39
39
  import { EffectEditor } from './EffectEditor';
40
- import { PromptDialog } from './PromptDialog';
41
- import { MessageDialog } from './MessageDialog';
42
40
  import { GalleryPanel } from './GalleryPanel';
41
+ import { MessageDialog } from './MessageDialog';
42
+ import { PromptDialog } from './PromptDialog';
43
43
  import { TimelinePanel } from './TimelinePanel';
44
- import { buttonStyle, globalEditorStyles, theme } from './theme';
45
44
  import { iconStyle } from './icons';
45
+ import { buttonStyle, globalEditorStyles, theme } from './theme';
46
46
  /** Disposes an effect's systems and its remaining group tree (otherwise the groups leak). */
47
47
  function disposeEffect(root, systems, renderer, inRenderer = true) {
48
48
  disposeLoadedEffect(root, systems, renderer, inRenderer);
@@ -548,7 +548,9 @@ export function EffectEditorHost(props) {
548
548
  }
549
549
  const previewIds = new Set(snapshot.previewRootIds);
550
550
  const binding = snapshot.binding;
551
- if (binding && (binding.root.uniqueId !== focusTimelineMetaRef.current.rootId || binding.getRevision() !== focusTimelineMetaRef.current.revision)) {
551
+ if (binding &&
552
+ (binding.root.uniqueId !== focusTimelineMetaRef.current.rootId ||
553
+ binding.getRevision() !== focusTimelineMetaRef.current.revision)) {
552
554
  const rows = computeTimelineRows(binding);
553
555
  focusTimelineMetaRef.current = {
554
556
  rootId: binding.root.uniqueId,
@@ -660,7 +662,13 @@ export function EffectEditorHost(props) {
660
662
  gm.positionGizmoEnabled = gizmoMode === 'position';
661
663
  gm.rotationGizmoEnabled = gizmoMode === 'rotation';
662
664
  gm.scaleGizmoEnabled = gizmoMode === 'scale';
663
- const activeGizmo = gizmoMode === 'position' ? gm.gizmos.positionGizmo : gizmoMode === 'rotation' ? gm.gizmos.rotationGizmo : gizmoMode === 'scale' ? gm.gizmos.scaleGizmo : null;
665
+ const activeGizmo = gizmoMode === 'position'
666
+ ? gm.gizmos.positionGizmo
667
+ : gizmoMode === 'rotation'
668
+ ? gm.gizmos.rotationGizmo
669
+ : gizmoMode === 'scale'
670
+ ? gm.gizmos.scaleGizmo
671
+ : null;
664
672
  let dragRaf = 0;
665
673
  const scheduleTransformRefresh = () => {
666
674
  if (dragRaf !== 0) {
@@ -798,7 +806,7 @@ export function EffectEditorHost(props) {
798
806
  const activeSystem = binding ? (allSystems.find((s) => s.emitter === selectedNode) ?? binding.system) : null;
799
807
  const gizmoTargetLabel = gizmoNode?.name || 'Node';
800
808
  const selectedGalleryEntry = selectedGalleryRoot
801
- ? gallery.find((entry) => entry.root === selectedGalleryRoot) ?? null
809
+ ? (gallery.find((entry) => entry.root === selectedGalleryRoot) ?? null)
802
810
  : null;
803
811
  /** Timeline is always shown when an effect is selected; playback only when it is on stage. */
804
812
  const showTimeline = Boolean(binding && activeSystem && state);
@@ -815,7 +823,15 @@ export function EffectEditorHost(props) {
815
823
  }
816
824
  return props.resolveGeometry(file, scene);
817
825
  }, [props.resolveGeometry]);
818
- return (_jsxs("div", { className: "qe-root", style: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%', fontFamily: theme.font, color: theme.text, background: '#070b16' }, children: [_jsx("style", { children: globalEditorStyles }), _jsxs("div", { style: { display: 'flex', flex: 1, minHeight: 0 }, children: [_jsxs("aside", { style: {
826
+ return (_jsxs("div", { className: "qe-root", style: {
827
+ display: 'flex',
828
+ flexDirection: 'column',
829
+ width: '100%',
830
+ height: '100%',
831
+ fontFamily: theme.font,
832
+ color: theme.text,
833
+ background: '#070b16',
834
+ }, children: [_jsx("style", { children: globalEditorStyles }), _jsxs("div", { style: { display: 'flex', flex: 1, minHeight: 0 }, children: [_jsxs("aside", { style: {
819
835
  width: GALLERY_PANEL_WIDTH,
820
836
  flexShrink: 0,
821
837
  borderRight: `1px solid ${theme.border}`,
@@ -823,7 +839,14 @@ export function EffectEditorHost(props) {
823
839
  display: 'flex',
824
840
  flexDirection: 'column',
825
841
  minHeight: 0,
826
- }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsxs("div", { style: { fontSize: 14, fontWeight: 600 }, children: ["Gallery", gallery.length > 0 ? ` · ${gallery.length}` : ''] }), _jsx("div", { style: { fontSize: 11, color: theme.textDim, marginTop: 4, lineHeight: 1.4 }, children: "Drag into viewport to place \u00B7 \u2713 preview \u00B7 \u00D7 remove from stage" })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'hidden', padding: '8px 12px 14px', display: 'flex', flexDirection: 'column' }, children: _jsx(GalleryPanel, { entries: gallery, focusedRoot: selectedGalleryRoot, previewRootIds: previewRootIdSet, loading: galleryLoading, folderInputRef: galleryFolderInputRef, onFocus: (entry) => selectGalleryEntryRef.current(entry), onTogglePreview: (entry) => toggleGalleryPreviewRef.current(entry), onRemoveFromScene: (entry) => removeGalleryFromSceneRef.current(entry), onImportFiles: (files) => void runGalleryImport(files), onImportFolder: () => void handleImportFolder(), onCreateDefault: () => createGalleryDefaultRef.current() }) })] }), _jsxs("div", { style: { position: 'relative', flex: 1, minWidth: 0 }, onDragEnter: (e) => {
842
+ }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsxs("div", { style: { fontSize: 14, fontWeight: 600 }, children: ["Gallery", gallery.length > 0 ? ` · ${gallery.length}` : ''] }), _jsx("div", { style: { fontSize: 11, color: theme.textDim, marginTop: 4, lineHeight: 1.4 }, children: "Drag into viewport to place \u00B7 \u2713 preview \u00B7 \u00D7 remove from stage" })] }), _jsx("div", { style: {
843
+ flex: 1,
844
+ minHeight: 0,
845
+ overflow: 'hidden',
846
+ padding: '8px 12px 14px',
847
+ display: 'flex',
848
+ flexDirection: 'column',
849
+ }, children: _jsx(GalleryPanel, { entries: gallery, focusedRoot: selectedGalleryRoot, previewRootIds: previewRootIdSet, loading: galleryLoading, folderInputRef: galleryFolderInputRef, onFocus: (entry) => selectGalleryEntryRef.current(entry), onTogglePreview: (entry) => toggleGalleryPreviewRef.current(entry), onRemoveFromScene: (entry) => removeGalleryFromSceneRef.current(entry), onImportFiles: (files) => void runGalleryImport(files), onImportFolder: () => void handleImportFolder(), onCreateDefault: () => createGalleryDefaultRef.current() }) })] }), _jsxs("div", { style: { position: 'relative', flex: 1, minWidth: 0 }, onDragEnter: (e) => {
827
850
  if (!e.dataTransfer.types.includes(GALLERY_DRAG_MIME))
828
851
  return;
829
852
  galleryDragDepthRef.current += 1;
@@ -889,7 +912,7 @@ export function EffectEditorHost(props) {
889
912
  fontSize: 13,
890
913
  color: theme.text,
891
914
  whiteSpace: 'nowrap',
892
- }, children: ["Drop ", galleryDragHover.count === 1 ? 'effect' : `${galleryDragHover.count} effects`, " \u2014 ", galleryDragHover.label] }) })), galleryLoading && (_jsx("div", { style: {
915
+ }, children: ["Drop ", galleryDragHover.count === 1 ? 'effect' : `${galleryDragHover.count} effects`, " \u2014", ' ', galleryDragHover.label] }) })), galleryLoading && (_jsx("div", { style: {
893
916
  position: 'absolute',
894
917
  inset: 0,
895
918
  display: 'flex',
@@ -897,7 +920,13 @@ export function EffectEditorHost(props) {
897
920
  justifyContent: 'center',
898
921
  background: 'rgba(7, 11, 22, 0.72)',
899
922
  pointerEvents: 'none',
900
- }, children: _jsxs("div", { style: { background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 10, padding: '16px 22px', textAlign: 'center' }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Loading gallery\u2026" }), _jsxs("div", { style: { fontSize: 13, color: theme.textDim }, children: [galleryLoading.done, " / ", galleryLoading.total || '…', " files", galleryLoading.failed > 0 ? ` · ${galleryLoading.failed} skipped` : ''] })] }) })), props.backLink && (_jsxs("a", { className: "qe-hover", href: props.backLink.href, style: {
923
+ }, children: _jsxs("div", { style: {
924
+ background: theme.panelBg,
925
+ border: `1px solid ${theme.border}`,
926
+ borderRadius: 10,
927
+ padding: '16px 22px',
928
+ textAlign: 'center',
929
+ }, children: [_jsx("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Loading gallery\u2026" }), _jsxs("div", { style: { fontSize: 13, color: theme.textDim }, children: [galleryLoading.done, " / ", galleryLoading.total || '…', " files", galleryLoading.failed > 0 ? ` · ${galleryLoading.failed} skipped` : ''] })] }) })), props.backLink && (_jsxs("a", { className: "qe-hover", href: props.backLink.href, style: {
901
930
  position: 'absolute',
902
931
  top: 12,
903
932
  left: 12,
@@ -913,13 +942,74 @@ export function EffectEditorHost(props) {
913
942
  color: theme.text,
914
943
  textDecoration: 'none',
915
944
  fontFamily: theme.font,
916
- }, children: [_jsx(ArrowLeftIcon, { style: iconStyle(14) }), props.backLink.label.replace(/^←\s*/, '')] })), _jsxs("div", { style: { position: 'absolute', top: 12, right: 12, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }, children: [_jsxs("div", { style: { display: 'flex', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4 }, children: [_jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Undo (Ctrl+Z)", disabled: !state?.history.canUndo || !binding, onClick: () => historyAction(state.history.undo()), children: _jsx(ArrowUturnLeftIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Redo (Ctrl+Shift+Z)", disabled: !state?.history.canRedo || !binding, onClick: () => historyAction(state.history.redo()), children: _jsx(ArrowUturnRightIcon, { style: iconStyle(14) }) })] }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4 }, children: GIZMO_MODE_BUTTONS.map((m) => (_jsx("button", { className: "qe-hover", style: { ...squareButtonStyle, ...(gizmoMode === m.value ? activeSquareButtonStyle : {}) }, title: m.title, onClick: () => setGizmoMode(m.value), children: _jsx(m.Icon, { style: iconStyle(14) }) }, m.value))) }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 4, background: theme.panelBg, border: `1px solid ${theme.border}`, borderRadius: 8, padding: 4 }, children: _jsx("button", { className: "qe-hover", style: { ...squareButtonStyle, ...(showEmitterWireframes ? activeSquareButtonStyle : {}) }, title: "Show emitter shape wireframes", disabled: !binding, onClick: () => setShowEmitterWireframes((on) => !on), children: _jsx(CubeTransparentIcon, { style: iconStyle(14) }) }) })] })] }), _jsxs("aside", { style: { width: 340, flexShrink: 0, borderLeft: `1px solid ${theme.border}`, background: theme.panelBg, display: 'flex', flexDirection: 'column', minHeight: 0 }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsx("div", { style: { fontSize: 11, fontWeight: 600, color: theme.textDim, textTransform: 'uppercase', letterSpacing: 0.4, marginBottom: 6 }, children: "Inspector" }), _jsx("div", { className: "qe-hover-bg", title: binding ? 'Rename effect' : undefined, style: { fontSize: 16, fontWeight: 600, cursor: binding ? 'pointer' : 'default', borderRadius: 6, padding: '2px 4px', margin: '-2px -4px' }, onClick: () => binding && setRenamingRoot(true), children: (binding?.root.name || props.title) ?? 'Effect editor' }), renamingRoot && binding && (_jsx(PromptDialog, { title: "Rename effect", defaultValue: binding.root.name, onSubmit: (name) => {
945
+ }, children: [_jsx(ArrowLeftIcon, { style: iconStyle(14) }), props.backLink.label.replace(/^←\s*/, '')] })), _jsxs("div", { style: {
946
+ position: 'absolute',
947
+ top: 12,
948
+ right: 12,
949
+ display: 'flex',
950
+ flexDirection: 'column',
951
+ alignItems: 'flex-end',
952
+ gap: 6,
953
+ }, children: [_jsxs("div", { style: {
954
+ display: 'flex',
955
+ gap: 4,
956
+ background: theme.panelBg,
957
+ border: `1px solid ${theme.border}`,
958
+ borderRadius: 8,
959
+ padding: 4,
960
+ }, children: [_jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Undo (Ctrl+Z)", disabled: !state?.history.canUndo || !binding, onClick: () => historyAction(state.history.undo()), children: _jsx(ArrowUturnLeftIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: squareButtonStyle, title: "Redo (Ctrl+Shift+Z)", disabled: !state?.history.canRedo || !binding, onClick: () => historyAction(state.history.redo()), children: _jsx(ArrowUturnRightIcon, { style: iconStyle(14) }) })] }), _jsx("div", { style: {
961
+ display: 'flex',
962
+ flexDirection: 'column',
963
+ gap: 4,
964
+ background: theme.panelBg,
965
+ border: `1px solid ${theme.border}`,
966
+ borderRadius: 8,
967
+ padding: 4,
968
+ }, children: GIZMO_MODE_BUTTONS.map((m) => (_jsx("button", { className: "qe-hover", style: {
969
+ ...squareButtonStyle,
970
+ ...(gizmoMode === m.value ? activeSquareButtonStyle : {}),
971
+ }, title: m.title, onClick: () => setGizmoMode(m.value), children: _jsx(m.Icon, { style: iconStyle(14) }) }, m.value))) }), _jsx("div", { style: {
972
+ display: 'flex',
973
+ flexDirection: 'column',
974
+ gap: 4,
975
+ background: theme.panelBg,
976
+ border: `1px solid ${theme.border}`,
977
+ borderRadius: 8,
978
+ padding: 4,
979
+ }, children: _jsx("button", { className: "qe-hover", style: {
980
+ ...squareButtonStyle,
981
+ ...(showEmitterWireframes ? activeSquareButtonStyle : {}),
982
+ }, title: "Show emitter shape wireframes", disabled: !binding, onClick: () => setShowEmitterWireframes((on) => !on), children: _jsx(CubeTransparentIcon, { style: iconStyle(14) }) }) })] })] }), _jsxs("aside", { style: {
983
+ width: 340,
984
+ flexShrink: 0,
985
+ borderLeft: `1px solid ${theme.border}`,
986
+ background: theme.panelBg,
987
+ display: 'flex',
988
+ flexDirection: 'column',
989
+ minHeight: 0,
990
+ }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsx("div", { style: {
991
+ fontSize: 11,
992
+ fontWeight: 600,
993
+ color: theme.textDim,
994
+ textTransform: 'uppercase',
995
+ letterSpacing: 0.4,
996
+ marginBottom: 6,
997
+ }, children: "Inspector" }), _jsx("div", { className: "qe-hover-bg", title: binding ? 'Rename effect' : undefined, style: {
998
+ fontSize: 16,
999
+ fontWeight: 600,
1000
+ cursor: binding ? 'pointer' : 'default',
1001
+ borderRadius: 6,
1002
+ padding: '2px 4px',
1003
+ margin: '-2px -4px',
1004
+ }, onClick: () => binding && setRenamingRoot(true), children: (binding?.root.name || props.title) ?? 'Effect editor' }), renamingRoot && binding && (_jsx(PromptDialog, { title: "Rename effect", defaultValue: binding.root.name, onSubmit: (name) => {
917
1005
  binding.apply(() => (binding.root.name = name));
918
1006
  setRenamingRoot(false);
919
1007
  }, onCancel: () => setRenamingRoot(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => {
920
1008
  if (!binding)
921
1009
  return;
922
- const blob = new Blob([binding.exportJSON('EditorEffect')], { type: 'application/json' });
1010
+ const blob = new Blob([binding.exportJSON('EditorEffect')], {
1011
+ type: 'application/json',
1012
+ });
923
1013
  const link = document.createElement('a');
924
1014
  link.href = URL.createObjectURL(blob);
925
1015
  link.download = 'effect.json';
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import React from 'react';
3
3
  import type { GalleryEntry, GalleryLoadProgress } from '../core/loadEffectGallery';
4
4
  export interface GalleryPanelProps {
5
5
  entries: GalleryEntry[];
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useMemo, useRef, useState } from 'react';
3
- import { useVirtualizer } from '@tanstack/react-virtual';
4
2
  import { CheckCircleIcon, ChevronDownIcon, ChevronRightIcon, FolderIcon, FolderOpenIcon, SparklesIcon, XMarkIcon, } from '@heroicons/react/24/solid';
3
+ import { useVirtualizer } from '@tanstack/react-virtual';
4
+ import { useCallback, useMemo, useRef, useState } from 'react';
5
5
  import { setGalleryDragSession } from '../core/galleryDragSession';
6
6
  import { buildGalleryTree, filterGalleryTree, flattenVisibleGalleryRows, writeGalleryDrag, } from '../core/galleryTree';
7
7
  import { iconStyle } from './icons';
@@ -50,7 +50,13 @@ function EffectRow(props) {
50
50
  }, onClick: (e) => {
51
51
  e.stopPropagation();
52
52
  onTogglePreview(entry);
53
- }, children: inPreview ? (_jsx(CheckCircleIcon, { style: iconStyle(14) })) : (_jsx("span", { style: { width: 10, height: 10, border: `1px solid currentColor`, borderRadius: 2, boxSizing: 'border-box' } })) })) : (_jsx("span", { style: { width: 28, flexShrink: 0, borderRight: `1px solid ${theme.border}` } })), _jsxs("button", { type: "button", draggable: true, className: "qe-hover-bg", title: entry.inScene
53
+ }, children: inPreview ? (_jsx(CheckCircleIcon, { style: iconStyle(14) })) : (_jsx("span", { style: {
54
+ width: 10,
55
+ height: 10,
56
+ border: `1px solid currentColor`,
57
+ borderRadius: 2,
58
+ boxSizing: 'border-box',
59
+ } })) })) : (_jsx("span", { style: { width: 28, flexShrink: 0, borderRight: `1px solid ${theme.border}` } })), _jsxs("button", { type: "button", draggable: true, className: "qe-hover-bg", title: entry.inScene
54
60
  ? `${entry.path} — on stage${inPreview ? ' · previewing' : ' · parked'} · Ctrl+click toggles preview`
55
61
  : `${entry.path} — catalog only · drag into viewport to place`, style: {
56
62
  display: 'flex',
@@ -129,7 +135,7 @@ function flatRowKey(row) {
129
135
  return row.kind === 'folder' ? `folder:${row.node.path}` : `effect:${row.node.entry.root.uniqueId}`;
130
136
  }
131
137
  function VirtualGalleryTree(props) {
132
- const { rows, closedFolders, focusedRoot, previewRootIds, onFocus, onTogglePreview, onRemoveFromScene, onToggleFolder } = props;
138
+ const { rows, closedFolders, focusedRoot, previewRootIds, onFocus, onTogglePreview, onRemoveFromScene, onToggleFolder, } = props;
133
139
  const scrollRef = useRef(null);
134
140
  const virtualizer = useVirtualizer({
135
141
  count: rows.length,
@@ -159,7 +165,7 @@ function VirtualGalleryTree(props) {
159
165
  }
160
166
  /** Folder-tree catalog; import effects here and drag them into the viewport. */
161
167
  export function GalleryPanel(props) {
162
- const { entries, focusedRoot, previewRootIds, loading, folderInputRef, onFocus, onTogglePreview, onRemoveFromScene, onImportFiles, onImportFolder, onCreateDefault } = props;
168
+ const { entries, focusedRoot, previewRootIds, loading, folderInputRef, onFocus, onTogglePreview, onRemoveFromScene, onImportFiles, onImportFolder, onCreateDefault, } = props;
163
169
  const [query, setQuery] = useState('');
164
170
  const [closedFolders, setClosedFolders] = useState(() => new Set());
165
171
  const fileInputRef = useRef(null);
@@ -204,7 +210,8 @@ export function GalleryPanel(props) {
204
210
  cursor: isLoading ? 'not-allowed' : 'pointer',
205
211
  opacity: isLoading ? 0.5 : 1,
206
212
  }, onClick: (e) => {
207
- if (typeof window.showDirectoryPicker === 'function') {
213
+ if (typeof window.showDirectoryPicker ===
214
+ 'function') {
208
215
  e.preventDefault();
209
216
  onImportFolder();
210
217
  }
@@ -233,7 +240,7 @@ export function GalleryPanel(props) {
233
240
  border: `1px solid ${theme.border}`,
234
241
  borderRadius: 8,
235
242
  background: theme.sectionBg,
236
- }, children: ["Catalog is empty. Click ", _jsx("strong", { children: "New effect" }), ", or ", _jsx("strong", { children: "Import effect" }), " / ", _jsx("strong", { children: "Import folder" }), "."] })) : filtered.length === 0 ? (_jsx("div", { style: {
243
+ }, children: ["Catalog is empty. Click ", _jsx("strong", { children: "New effect" }), ", or ", _jsx("strong", { children: "Import effect" }), " /", ' ', _jsx("strong", { children: "Import folder" }), "."] })) : filtered.length === 0 ? (_jsx("div", { style: {
237
244
  flex: 1,
238
245
  minHeight: 120,
239
246
  padding: '10px 12px',
@@ -1,7 +1,6 @@
1
- import React from 'react';
2
1
  import { GradientStop } from '../core/colors';
3
2
  /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
4
3
  export declare function GradientEditor(props: {
5
4
  stops: GradientStop[];
6
5
  onChange: (stops: GradientStop[]) => void;
7
- }): React.JSX.Element;
6
+ }): import("react").JSX.Element;
@@ -65,7 +65,14 @@ export function GradientEditor(props) {
65
65
  }, onPointerUp: (e) => {
66
66
  dragging.current = false;
67
67
  e.currentTarget.releasePointerCapture(e.pointerId);
68
- } }, i))) }), current && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Stop color", children: _jsx("input", { type: "color", className: "qe-hover", value: rgbToHex(current.r, current.g, current.b), onChange: (e) => updateStop(selected, hexToRgb(e.target.value)), style: { width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' } }) }), _jsx(Row, { label: "Stop alpha", children: _jsx(NumberField, { value: current.a, min: 0, step: 0.05, onChange: (a) => updateStop(selected, { a: Math.min(1, a) }) }) }), _jsx(Row, { label: "Stop position", children: _jsx(NumberField, { value: current.pos, min: 0, step: 0.05, onChange: (pos) => updateStop(selected, { pos: Math.min(1, pos) }) }) }), stops.length > 2 && (_jsx("button", { className: "qe-hover", style: { ...buttonStyle, marginTop: 6 }, onClick: () => {
68
+ } }, i))) }), current && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Stop color", children: _jsx("input", { type: "color", className: "qe-hover", value: rgbToHex(current.r, current.g, current.b), onChange: (e) => updateStop(selected, hexToRgb(e.target.value)), style: {
69
+ width: 42,
70
+ height: 24,
71
+ border: 'none',
72
+ background: 'transparent',
73
+ padding: 0,
74
+ cursor: 'pointer',
75
+ } }) }), _jsx(Row, { label: "Stop alpha", children: _jsx(NumberField, { value: current.a, min: 0, step: 0.05, onChange: (a) => updateStop(selected, { a: Math.min(1, a) }) }) }), _jsx(Row, { label: "Stop position", children: _jsx(NumberField, { value: current.pos, min: 0, step: 0.05, onChange: (pos) => updateStop(selected, { pos: Math.min(1, pos) }) }) }), stops.length > 2 && (_jsx("button", { className: "qe-hover", style: { ...buttonStyle, marginTop: 6 }, onClick: () => {
69
76
  props.onChange(stops.filter((_, i) => i !== selected));
70
77
  setSelected(0);
71
78
  }, children: "Remove stop" }))] }))] }));
@@ -20,5 +20,11 @@ export function MessageDialog(props) {
20
20
  minWidth: 320,
21
21
  maxWidth: 480,
22
22
  boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
23
- }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontSize: 14, fontWeight: 600, color: theme.text, marginBottom: 8 }, children: title }), _jsx("div", { style: { fontSize: 13, lineHeight: 1.5, color: theme.textDim, marginBottom: 14, whiteSpace: 'pre-wrap' }, children: message }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end' }, children: _jsx("button", { className: "qe-hover", style: { ...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent }, onClick: onClose, children: confirmLabel }) })] }) }));
23
+ }, onClick: (e) => e.stopPropagation(), children: [_jsx("div", { style: { fontSize: 14, fontWeight: 600, color: theme.text, marginBottom: 8 }, children: title }), _jsx("div", { style: {
24
+ fontSize: 13,
25
+ lineHeight: 1.5,
26
+ color: theme.textDim,
27
+ marginBottom: 14,
28
+ whiteSpace: 'pre-wrap',
29
+ }, children: message }), _jsx("div", { style: { display: 'flex', justifyContent: 'flex-end' }, children: _jsx("button", { className: "qe-hover", style: { ...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent }, onClick: onClose, children: confirmLabel }) })] }) }));
24
30
  }
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
2
  import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
3
+ import { useState } from 'react';
4
+ import { iconStyle } from './icons';
4
5
  import { getInspectorHint } from './inspectorHints';
5
6
  import { InspectorInfoIcon } from './InspectorInfoIcon';
6
7
  import { theme } from './theme';
7
- import { iconStyle } from './icons';
8
8
  /** Unity-style module foldout with an optional enable checkbox. */
9
9
  export function ModuleSection(props) {
10
10
  const [open, setOpen] = useState(props.defaultOpen ?? true);
@@ -27,5 +27,12 @@ export function ModuleSection(props) {
27
27
  cursor: 'pointer',
28
28
  userSelect: 'none',
29
29
  background: 'rgba(20, 30, 60, 0.5)',
30
- }, onClick: () => setOpen(!open), children: [_jsx("span", { style: { color: theme.accent, display: 'inline-flex' }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(12) }) : _jsx(ChevronRightIcon, { style: iconStyle(12) }) }), toggleable && (_jsx("input", { type: "checkbox", className: "qe-hover", checked: props.enabled, onClick: (e) => e.stopPropagation(), onChange: (e) => props.onToggle?.(e.target.checked), style: { width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer' } })), _jsxs("span", { style: { color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 5 }, children: [props.title, tooltip ? _jsx(InspectorInfoIcon, { content: tooltip, onClick: (e) => e.stopPropagation() }) : null] })] }), open && !dimmed && _jsx("div", { style: { padding: '4px 10px 10px' }, children: props.children })] }));
30
+ }, onClick: () => setOpen(!open), children: [_jsx("span", { style: { color: theme.accent, display: 'inline-flex' }, children: open ? _jsx(ChevronDownIcon, { style: iconStyle(12) }) : _jsx(ChevronRightIcon, { style: iconStyle(12) }) }), toggleable && (_jsx("input", { type: "checkbox", className: "qe-hover", checked: props.enabled, onClick: (e) => e.stopPropagation(), onChange: (e) => props.onToggle?.(e.target.checked), style: { width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer' } })), _jsxs("span", { style: {
31
+ color: dimmed ? theme.textDim : theme.text,
32
+ fontSize: 13,
33
+ fontWeight: 600,
34
+ display: 'inline-flex',
35
+ alignItems: 'center',
36
+ gap: 5,
37
+ }, children: [props.title, tooltip ? _jsx(InspectorInfoIcon, { content: tooltip, onClick: (e) => e.stopPropagation() }) : null] })] }), open && !dimmed && _jsx("div", { style: { padding: '4px 10px 10px' }, children: props.children })] }));
31
38
  }
@@ -1,6 +1,5 @@
1
- import React from 'react';
2
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
3
2
  export declare function ObjectModule(props: {
4
3
  node: TransformNode;
5
4
  label: string;
6
- }): React.JSX.Element;
5
+ }): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useSyncExternalStore } from 'react';
3
2
  import { Quaternion } from '@babylonjs/core/Maths/math.vector';
3
+ import { useSyncExternalStore } from 'react';
4
4
  import { getTransformRevision, notifyTransformChanged, subscribeTransform } from '../core/transformStore';
5
5
  import { ModuleSection } from './ModuleSection';
6
6
  import { NumberField, Row } from './fields';
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import { EffectBinding } from '../core/binding';
3
2
  /**
4
3
  * Sub Emitters module for the currently selected system. The spawned target lives as a
@@ -7,4 +6,4 @@ import { EffectBinding } from '../core/binding';
7
6
  export declare function SubEmittersModule(props: {
8
7
  binding: EffectBinding;
9
8
  rootBinding: EffectBinding;
10
- }): React.JSX.Element;
9
+ }): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
- import React from 'react';
2
- import type { BatchedRenderer } from 'babylon.quarks';
3
- import type { Scene } from '@babylonjs/core/scene';
4
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import type { Scene } from '@babylonjs/core/scene';
3
+ import type { BatchedRenderer } from 'babylon.quarks';
4
+ import React from 'react';
5
5
  import type { EffectBinding } from '../core/binding';
6
6
  import { type PlaybackState } from '../core/playback';
7
7
  export type { PlaybackState } from '../core/playback';
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon, RectangleGroupIcon } from '@heroicons/react/24/solid';
2
3
  import React, { useMemo, useRef, useState } from 'react';
3
- import { ArrowPathIcon, ForwardIcon, PauseIcon, PlayIcon, RectangleGroupIcon, } from '@heroicons/react/24/solid';
4
- import { clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, } from '../core/playback';
5
4
  import { groupNodes } from '../core/groups';
5
+ import { clearFocusFinished, getFocusElapsed, isFocusFinished, markFocusFinished, setFocusElapsed, } from '../core/playback';
6
6
  import { pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled } from '../core/scrub';
7
7
  import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
8
8
  import { TimelineTrackRow } from './TimelineTrackRow';
@@ -29,7 +29,7 @@ function pickTickInterval(pxPerSecond) {
29
29
  * apply only to the focused effect without stopping other previews.
30
30
  */
31
31
  export function TimelinePanel(props) {
32
- const { binding, selectedNode, onSelectNode, scene, renderer, playbackRef, playheadRef, counterRef, stagePlaying, setStagePlaying, speed, setSpeed, playbackEnabled = true } = props;
32
+ const { binding, selectedNode, onSelectNode, scene, renderer, playbackRef, playheadRef, counterRef, stagePlaying, setStagePlaying, speed, setSpeed, playbackEnabled = true, } = props;
33
33
  const [bodyWidthPx, setBodyWidthPx] = useState(600);
34
34
  const [collapsedGroups, setCollapsedGroups] = useState(() => new Set());
35
35
  const [hiddenSystems, setHiddenSystems] = useState(() => new Set());
@@ -129,7 +129,18 @@ export function TimelinePanel(props) {
129
129
  };
130
130
  raf = requestAnimationFrame(tick);
131
131
  return () => cancelAnimationFrame(raf);
132
- }, [pxPerSecond, timelineSpan, anyLooping, playbackRef, playheadRef, focusRootId, playbackEnabled, binding, scene, renderer]);
132
+ }, [
133
+ pxPerSecond,
134
+ timelineSpan,
135
+ anyLooping,
136
+ playbackRef,
137
+ playheadRef,
138
+ focusRootId,
139
+ playbackEnabled,
140
+ binding,
141
+ scene,
142
+ renderer,
143
+ ]);
133
144
  const visibleRows = useMemo(() => {
134
145
  let hideUntilDepth = null;
135
146
  const visible = rows.filter((row) => {
@@ -246,11 +257,23 @@ export function TimelinePanel(props) {
246
257
  flexDirection: 'column',
247
258
  fontFamily: theme.font,
248
259
  color: theme.text,
249
- }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '6px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsx("button", { className: "qe-hover", style: { ...overlayButton, opacity: playbackEnabled ? 1 : 0.45 }, title: stagePlaying ? 'Pause stage preview' : 'Play stage preview', disabled: !playbackEnabled, onClick: handleStagePlayPause, children: stagePlaying ? _jsx(PauseIcon, { style: iconStyle(14) }) : _jsx(PlayIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: { ...overlayButton, opacity: playbackEnabled ? 1 : 0.45 }, title: "Step one frame (stage)", disabled: !playbackEnabled || stagePlaying, onClick: () => (playbackRef.current.stepQueued = true), children: _jsx(ForwardIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: { ...overlayButton, opacity: playbackEnabled ? 1 : 0.45 }, title: "Restart focused effect only", disabled: !playbackEnabled, onClick: restartFocus, children: _jsx(ArrowPathIcon, { style: iconStyle(14) }) }), _jsx("select", { className: "qe-hover", style: { ...overlayButton, padding: '5px 4px', opacity: playbackEnabled ? 1 : 0.45 }, title: "Playback speed", value: speed, disabled: !playbackEnabled, onChange: (e) => {
260
+ }, children: [_jsxs("div", { style: {
261
+ display: 'flex',
262
+ alignItems: 'center',
263
+ gap: 6,
264
+ padding: '6px 10px',
265
+ borderBottom: `1px solid ${theme.border}`,
266
+ }, children: [_jsx("button", { className: "qe-hover", style: { ...overlayButton, opacity: playbackEnabled ? 1 : 0.45 }, title: stagePlaying ? 'Pause stage preview' : 'Play stage preview', disabled: !playbackEnabled, onClick: handleStagePlayPause, children: stagePlaying ? _jsx(PauseIcon, { style: iconStyle(14) }) : _jsx(PlayIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: { ...overlayButton, opacity: playbackEnabled ? 1 : 0.45 }, title: "Step one frame (stage)", disabled: !playbackEnabled || stagePlaying, onClick: () => (playbackRef.current.stepQueued = true), children: _jsx(ForwardIcon, { style: iconStyle(14) }) }), _jsx("button", { className: "qe-hover", style: { ...overlayButton, opacity: playbackEnabled ? 1 : 0.45 }, title: "Restart focused effect only", disabled: !playbackEnabled, onClick: restartFocus, children: _jsx(ArrowPathIcon, { style: iconStyle(14) }) }), _jsx("select", { className: "qe-hover", style: { ...overlayButton, padding: '5px 4px', opacity: playbackEnabled ? 1 : 0.45 }, title: "Playback speed", value: speed, disabled: !playbackEnabled, onChange: (e) => {
250
267
  const next = Number(e.target.value);
251
268
  playbackRef.current.speed = next;
252
269
  setSpeed(next);
253
- }, children: [0.1, 0.25, 0.5, 1, 2, 4].map((s) => (_jsxs("option", { value: s, children: ["\u00D7", s] }, s))) }), _jsx("span", { ref: counterRef, style: { fontSize: 12, color: theme.textDim, fontVariantNumeric: 'tabular-nums', paddingLeft: 4, minWidth: 120 } }), groupSelection.size >= 2 && (_jsxs("button", { className: "qe-hover", style: overlayButton, title: "Group the checked rows", onClick: groupSelectedNodes, children: [_jsx(RectangleGroupIcon, { style: iconStyle(14) }), "Group (", groupSelection.size, ")"] }))] }), _jsx("div", { ref: bodyRef, style: { position: 'relative', flex: 1, minHeight: 0 }, children: !playbackEnabled ? (_jsx("div", { style: {
270
+ }, children: [0.1, 0.25, 0.5, 1, 2, 4].map((s) => (_jsxs("option", { value: s, children: ["\u00D7", s] }, s))) }), _jsx("span", { ref: counterRef, style: {
271
+ fontSize: 12,
272
+ color: theme.textDim,
273
+ fontVariantNumeric: 'tabular-nums',
274
+ paddingLeft: 4,
275
+ minWidth: 120,
276
+ } }), groupSelection.size >= 2 && (_jsxs("button", { className: "qe-hover", style: overlayButton, title: "Group the checked rows", onClick: groupSelectedNodes, children: [_jsx(RectangleGroupIcon, { style: iconStyle(14) }), "Group (", groupSelection.size, ")"] }))] }), _jsx("div", { ref: bodyRef, style: { position: 'relative', flex: 1, minHeight: 0 }, children: !playbackEnabled ? (_jsx("div", { style: {
254
277
  position: 'absolute',
255
278
  inset: 0,
256
279
  display: 'flex',
@@ -271,7 +294,12 @@ export function TimelinePanel(props) {
271
294
  gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
272
295
  background: theme.panelBg,
273
296
  zIndex: 2,
274
- }, children: [_jsx("div", { style: { height: RULER_HEIGHT } }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: RULER_HEIGHT, cursor: 'ew-resize', overflow: 'hidden' }, onPointerDown: (e) => {
297
+ }, children: [_jsx("div", { style: { height: RULER_HEIGHT } }), _jsx("div", { className: "qe-hover-bg", style: {
298
+ position: 'relative',
299
+ height: RULER_HEIGHT,
300
+ cursor: 'ew-resize',
301
+ overflow: 'hidden',
302
+ }, onPointerDown: (e) => {
275
303
  playbackRef.current.scrubbing = true;
276
304
  e.target.setPointerCapture(e.pointerId);
277
305
  queueScrub(e.clientX);
@@ -281,7 +309,16 @@ export function TimelinePanel(props) {
281
309
  }, onPointerUp: (e) => {
282
310
  playbackRef.current.scrubbing = false;
283
311
  e.target.releasePointerCapture?.(e.pointerId);
284
- }, children: ticks.map((t) => (_jsxs("div", { style: { position: 'absolute', left: TIMELINE_INSET + t * pxPerSecond, top: 0, bottom: 0, borderLeft: `1px solid ${theme.border}`, fontSize: 10, color: theme.textDim, paddingLeft: 3 }, children: [t, "s"] }, t))) })] }), _jsx("div", { style: {
312
+ }, children: ticks.map((t) => (_jsxs("div", { style: {
313
+ position: 'absolute',
314
+ left: TIMELINE_INSET + t * pxPerSecond,
315
+ top: 0,
316
+ bottom: 0,
317
+ borderLeft: `1px solid ${theme.border}`,
318
+ fontSize: 10,
319
+ color: theme.textDim,
320
+ paddingLeft: 3,
321
+ }, children: [t, "s"] }, t))) })] }), _jsx("div", { style: {
285
322
  position: 'absolute',
286
323
  top: RULER_HEIGHT,
287
324
  left: 0,
@@ -293,7 +330,16 @@ export function TimelinePanel(props) {
293
330
  display: 'grid',
294
331
  gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
295
332
  gridAutoRows: 'min-content',
296
- }, children: visibleRows.map((row) => (_jsx(TimelineTrackRow, { row: row, binding: binding, selected: row.node === selectedNode, onSelectNode: onSelectNode, pxPerSecond: pxPerSecond, offsetPx: TIMELINE_INSET, collapsed: row.kind === 'group' && collapsedGroups.has(row.node), onToggleCollapse: row.kind === 'group' ? () => toggleCollapse(row.node) : undefined, hidden: row.kind === 'track' && hiddenSystems.has(row.system), onToggleVisible: row.kind === 'track' ? () => toggleVisible(row.system) : undefined, groupSelected: groupSelection.has(row.node), onToggleGroupSelect: () => toggleGroupSelect(row.node) }, row.node.uniqueId))) }) }), _jsx("div", { style: { position: 'absolute', top: 0, bottom: 0, left: LABEL_WIDTH, width: 1, background: theme.border, pointerEvents: 'none', zIndex: 3 } }), _jsx("div", { ref: playheadRef, style: {
333
+ }, children: visibleRows.map((row) => (_jsx(TimelineTrackRow, { row: row, binding: binding, selected: row.node === selectedNode, onSelectNode: onSelectNode, pxPerSecond: pxPerSecond, offsetPx: TIMELINE_INSET, collapsed: row.kind === 'group' && collapsedGroups.has(row.node), onToggleCollapse: row.kind === 'group' ? () => toggleCollapse(row.node) : undefined, hidden: row.kind === 'track' && hiddenSystems.has(row.system), onToggleVisible: row.kind === 'track' ? () => toggleVisible(row.system) : undefined, groupSelected: groupSelection.has(row.node), onToggleGroupSelect: () => toggleGroupSelect(row.node) }, row.node.uniqueId))) }) }), _jsx("div", { style: {
334
+ position: 'absolute',
335
+ top: 0,
336
+ bottom: 0,
337
+ left: LABEL_WIDTH,
338
+ width: 1,
339
+ background: theme.border,
340
+ pointerEvents: 'none',
341
+ zIndex: 3,
342
+ } }), _jsx("div", { ref: playheadRef, style: {
297
343
  position: 'absolute',
298
344
  top: 0,
299
345
  bottom: 0,
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import type { TransformNode } from '@babylonjs/core/Meshes/transformNode';
2
+ import React from 'react';
3
3
  import type { EffectBinding } from '../core/binding';
4
4
  import type { TimelineRow } from '../core/timeline';
5
5
  export interface TimelineTrackRowProps {