babylon.quarks-editor 0.17.8 → 0.18.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 (122) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/core/binding.js +2 -5
  4. package/dist/core/defaultEffect.d.ts +12 -0
  5. package/dist/core/defaultEffect.js +43 -0
  6. package/dist/core/editorScene.d.ts +7 -0
  7. package/dist/core/editorScene.js +11 -0
  8. package/dist/core/effectTree.js +2 -1
  9. package/dist/core/emitterShapeWireframe.d.ts +20 -0
  10. package/dist/core/emitterShapeWireframe.js +329 -0
  11. package/dist/core/galleryDragSession.d.ts +4 -0
  12. package/dist/core/galleryDragSession.js +8 -0
  13. package/dist/core/galleryDropMarker.d.ts +13 -0
  14. package/dist/core/galleryDropMarker.js +42 -0
  15. package/dist/core/galleryLayout.d.ts +20 -0
  16. package/dist/core/galleryLayout.js +68 -0
  17. package/dist/core/galleryPlayback.d.ts +14 -0
  18. package/dist/core/galleryPlayback.js +72 -0
  19. package/dist/core/galleryScene.d.ts +20 -0
  20. package/dist/core/galleryScene.js +65 -0
  21. package/dist/core/gallerySelectionMarker.d.ts +25 -0
  22. package/dist/core/gallerySelectionMarker.js +69 -0
  23. package/dist/core/galleryTree.d.ts +49 -0
  24. package/dist/core/galleryTree.js +138 -0
  25. package/dist/core/loadEffect.d.ts +13 -0
  26. package/dist/core/loadEffect.js +31 -17
  27. package/dist/core/loadEffectGallery.d.ts +36 -0
  28. package/dist/core/loadEffectGallery.js +97 -0
  29. package/dist/core/material.d.ts +13 -0
  30. package/dist/core/material.js +68 -0
  31. package/dist/core/pickGalleryFiles.d.ts +5 -0
  32. package/dist/core/pickGalleryFiles.js +42 -0
  33. package/dist/core/playback.d.ts +23 -0
  34. package/dist/core/playback.js +32 -0
  35. package/dist/core/renderDebug.d.ts +8 -0
  36. package/dist/core/renderDebug.js +42 -0
  37. package/dist/core/scrub.d.ts +14 -0
  38. package/dist/core/scrub.js +70 -0
  39. package/dist/core/timeline.d.ts +4 -1
  40. package/dist/core/timeline.js +30 -3
  41. package/dist/core/transformStore.d.ts +3 -0
  42. package/dist/core/transformStore.js +16 -0
  43. package/dist/core/viewportPicker.d.ts +25 -0
  44. package/dist/core/viewportPicker.js +86 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +1 -0
  47. package/dist/react/EffectEditor.js +1 -4
  48. package/dist/react/EffectEditorHost.d.ts +7 -0
  49. package/dist/react/EffectEditorHost.js +745 -115
  50. package/dist/react/GalleryPanel.d.ts +20 -0
  51. package/dist/react/GalleryPanel.js +246 -0
  52. package/dist/react/InspectorInfoIcon.d.ts +7 -0
  53. package/dist/react/InspectorInfoIcon.js +22 -0
  54. package/dist/react/InspectorLabel.d.ts +7 -0
  55. package/dist/react/InspectorLabel.js +12 -0
  56. package/dist/react/MessageDialog.d.ts +9 -0
  57. package/dist/react/MessageDialog.js +24 -0
  58. package/dist/react/ModuleSection.d.ts +2 -0
  59. package/dist/react/ModuleSection.js +8 -1
  60. package/dist/react/ObjectModule.js +14 -2
  61. package/dist/react/TimelinePanel.d.ts +8 -11
  62. package/dist/react/TimelinePanel.js +129 -98
  63. package/dist/react/TimelineTrackRow.js +29 -15
  64. package/dist/react/Tooltip.d.ts +6 -0
  65. package/dist/react/Tooltip.js +65 -0
  66. package/dist/react/ValueField.d.ts +2 -0
  67. package/dist/react/ValueField.js +1 -1
  68. package/dist/react/behaviorModules.js +8 -8
  69. package/dist/react/fields.d.ts +2 -0
  70. package/dist/react/fields.js +14 -5
  71. package/dist/react/icons.d.ts +7 -0
  72. package/dist/react/icons.js +10 -0
  73. package/dist/react/index.d.ts +4 -0
  74. package/dist/react/index.js +2 -0
  75. package/dist/react/inspectorHints.d.ts +2 -0
  76. package/dist/react/inspectorHints.js +138 -0
  77. package/dist/react/modules.js +109 -81
  78. package/dist/react/theme.d.ts +2 -0
  79. package/dist/react/theme.js +53 -1
  80. package/package.json +8 -4
  81. package/src/core/binding.ts +2 -5
  82. package/src/core/defaultEffect.ts +74 -0
  83. package/src/core/editorScene.ts +14 -0
  84. package/src/core/effectTree.ts +2 -1
  85. package/src/core/emitterShapeWireframe.ts +373 -0
  86. package/src/core/galleryDragSession.ts +12 -0
  87. package/src/core/galleryDropMarker.ts +57 -0
  88. package/src/core/galleryLayout.ts +90 -0
  89. package/src/core/galleryPlayback.ts +90 -0
  90. package/src/core/galleryScene.ts +92 -0
  91. package/src/core/gallerySelectionMarker.ts +84 -0
  92. package/src/core/galleryTree.ts +187 -0
  93. package/src/core/loadEffect.ts +55 -18
  94. package/src/core/loadEffectGallery.ts +146 -0
  95. package/src/core/material.ts +84 -0
  96. package/src/core/pickGalleryFiles.ts +58 -0
  97. package/src/core/playback.ts +49 -0
  98. package/src/core/renderDebug.ts +46 -0
  99. package/src/core/scrub.ts +76 -1
  100. package/src/core/timeline.ts +34 -3
  101. package/src/core/transformStore.ts +19 -0
  102. package/src/core/viewportPicker.ts +114 -0
  103. package/src/index.ts +2 -0
  104. package/src/react/EffectEditor.tsx +5 -7
  105. package/src/react/EffectEditorHost.tsx +936 -142
  106. package/src/react/GalleryPanel.tsx +517 -0
  107. package/src/react/InspectorInfoIcon.tsx +36 -0
  108. package/src/react/InspectorLabel.tsx +18 -0
  109. package/src/react/MessageDialog.tsx +57 -0
  110. package/src/react/ModuleSection.tsx +17 -2
  111. package/src/react/ObjectModule.tsx +17 -6
  112. package/src/react/TimelinePanel.tsx +227 -174
  113. package/src/react/TimelineTrackRow.tsx +58 -23
  114. package/src/react/Tooltip.tsx +93 -0
  115. package/src/react/ValueField.tsx +3 -1
  116. package/src/react/behaviorModules.tsx +13 -8
  117. package/src/react/fields.tsx +15 -6
  118. package/src/react/icons.ts +19 -0
  119. package/src/react/index.ts +4 -0
  120. package/src/react/inspectorHints.ts +150 -0
  121. package/src/react/modules.tsx +198 -183
  122. package/src/react/theme.ts +54 -1
@@ -1,13 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  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';
3
5
  import { groupNodes } from '../core/groups';
4
- import { scrubTo } from '../core/scrub';
6
+ import { pauseFocus, playFocus, scrubFocusTo, scrubFocusToSettled } from '../core/scrub';
5
7
  import { computeTimelineRows, computeTimelineSpan } from '../core/timeline';
6
8
  import { TimelineTrackRow } from './TimelineTrackRow';
9
+ import { iconStyle } from './icons';
7
10
  import { buttonStyle, theme } from './theme';
8
11
  const LABEL_WIDTH = 200;
9
12
  const RULER_HEIGHT = 22;
10
- const STAGE_ROW_HEIGHT = 26;
11
13
  const PANEL_HEIGHT = 220;
12
14
  /** Small gap between the label column and the first pixel of the time axis. */
13
15
  export const TIMELINE_INSET = 10;
@@ -23,20 +25,18 @@ function pickTickInterval(pxPerSecond) {
23
25
  return 10;
24
26
  }
25
27
  /**
26
- * Bottom-docked, Premiere/Resolve-style timeline: transport controls, a time ruler with a
27
- * draggable playhead (approximate scrub — see core/scrub.ts), and one track row per system.
28
+ * Bottom-docked timeline: stage Play/Pause drives all preview effects; playhead/scrub/restart
29
+ * apply only to the focused effect without stopping other previews.
28
30
  */
29
31
  export function TimelinePanel(props) {
30
- const { binding, selectedNode, onSelectNode, scene, renderer, playbackRef, playheadRef, counterRef, paused, setPaused, speed, setSpeed } = props;
32
+ const { binding, selectedNode, onSelectNode, scene, renderer, playbackRef, playheadRef, counterRef, stagePlaying, setStagePlaying, speed, setSpeed, playbackEnabled = true } = props;
31
33
  const [bodyWidthPx, setBodyWidthPx] = useState(600);
32
34
  const [collapsedGroups, setCollapsedGroups] = useState(() => new Set());
33
35
  const [hiddenSystems, setHiddenSystems] = useState(() => new Set());
34
36
  const [groupSelection, setGroupSelection] = useState(() => new Set());
35
37
  const bodyRef = useRef(null);
36
38
  const dragRef = useRef({ raf: null, pendingTime: null });
37
- // A new effect (import/undo/redo/initial mount) replaces every system, so stale
38
- // visibility toggles and grouping selection from the previous effect no longer refer to
39
- // anything real.
39
+ const focusRootId = binding.root.uniqueId;
40
40
  React.useEffect(() => {
41
41
  setHiddenSystems(new Set());
42
42
  setGroupSelection(new Set());
@@ -90,42 +90,35 @@ export function TimelinePanel(props) {
90
90
  return () => observer.disconnect();
91
91
  }, []);
92
92
  const rows = useMemo(() => computeTimelineRows(binding), [binding, binding.getRevision()]);
93
- // The ruler spans exactly the furthest track extent (no artificial stretch), minus a
94
- // right-hand gap so the full-duration mark never sits flush against the panel edge.
95
93
  const timelineSpan = useMemo(() => computeTimelineSpan(rows), [rows]);
96
94
  const pxPerSecond = Math.max(1, bodyWidthPx - TIMELINE_INSET - RIGHT_PADDING) / timelineSpan;
97
- // Follows playbackRef.elapsed independently of Babylon's render loop (Host only owns
98
- // simulation time; this owns translating it into pixels for the playhead line). Whether
99
- // the *whole* effect loops is an OR across tracks (there's no single canonical "root" —
100
- // an effect can have several top-level systems): if any track loops, elapsed grows
101
- // unbounded and we wrap it at timelineSpan. If none loop, finishing a run snaps playback
102
- // straight back to frame 0 (paused) — a one-shot preview parked and ready for the next Play.
103
95
  const anyLooping = useMemo(() => rows.some((r) => r.kind === 'track' && r.looping), [rows]);
104
- // Tracks elapsed from the previous tick so the finish-reset below only fires on a genuine
105
- // crossing during live playback. Without this, manually dragging the playhead to (or past)
106
- // the end pins elapsed at timelineSpan while scrubbing=true skips the reset — but the very
107
- // next tick after release would otherwise see elapsed >= timelineSpan with no memory of
108
- // *how* it got there, and wrongly treat a drag as a finished run.
109
96
  const prevElapsedRef = useRef(0);
110
97
  React.useEffect(() => {
98
+ if (!playbackEnabled) {
99
+ return;
100
+ }
111
101
  let raf;
112
102
  const tick = () => {
113
103
  const state = playbackRef.current;
114
- const elapsed = state.elapsed;
104
+ const elapsed = getFocusElapsed(state, focusRootId);
115
105
  let shown = elapsed;
116
106
  if (anyLooping) {
117
107
  shown = timelineSpan > 0 ? elapsed % timelineSpan : 0;
118
108
  }
109
+ else if (isFocusFinished(state, focusRootId)) {
110
+ // Park at real simulation time — core may finish before the ruler's worst-case span.
111
+ shown = elapsed;
112
+ }
119
113
  else if (elapsed >= timelineSpan) {
120
114
  shown = timelineSpan;
121
- const justFinishedPlaying = !state.scrubbing && !state.paused && prevElapsedRef.current < timelineSpan;
115
+ const justFinishedPlaying = !state.scrubbing && state.stagePlaying && prevElapsedRef.current < timelineSpan;
122
116
  if (justFinishedPlaying) {
123
- scrubTo(binding, renderer, 0);
124
- state.elapsed = 0;
125
- state.paused = true;
126
- setPaused(true);
117
+ scrubFocusToSettled(binding, renderer, timelineSpan);
118
+ markFocusFinished(state, focusRootId);
119
+ setFocusElapsed(state, focusRootId, timelineSpan);
120
+ pauseFocus(binding);
127
121
  scene.render();
128
- shown = 0;
129
122
  }
130
123
  }
131
124
  prevElapsedRef.current = elapsed;
@@ -136,7 +129,7 @@ export function TimelinePanel(props) {
136
129
  };
137
130
  raf = requestAnimationFrame(tick);
138
131
  return () => cancelAnimationFrame(raf);
139
- }, [pxPerSecond, timelineSpan, anyLooping, playbackRef, playheadRef, setPaused, binding, renderer, scene]);
132
+ }, [pxPerSecond, timelineSpan, anyLooping, playbackRef, playheadRef, focusRootId, playbackEnabled, binding, scene, renderer]);
140
133
  const visibleRows = useMemo(() => {
141
134
  let hideUntilDepth = null;
142
135
  const visible = rows.filter((row) => {
@@ -170,10 +163,24 @@ export function TimelinePanel(props) {
170
163
  return Math.min(timelineSpan, Math.max(0, x / pxPerSecond));
171
164
  };
172
165
  const applyScrub = (time) => {
173
- scrubTo(binding, renderer, time);
174
- playbackRef.current.elapsed = time;
166
+ const state = playbackRef.current;
167
+ const atEnd = !anyLooping && time >= timelineSpan;
168
+ const shown = atEnd ? timelineSpan : time;
169
+ if (atEnd) {
170
+ scrubFocusToSettled(binding, renderer, timelineSpan);
171
+ markFocusFinished(state, focusRootId);
172
+ pauseFocus(binding);
173
+ }
174
+ else {
175
+ scrubFocusTo(binding, renderer, time);
176
+ clearFocusFinished(state, focusRootId);
177
+ if (stagePlaying) {
178
+ playFocus(binding);
179
+ }
180
+ }
181
+ setFocusElapsed(state, focusRootId, shown);
175
182
  if (playheadRef.current) {
176
- playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + time * pxPerSecond}px`;
183
+ playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET + shown * pxPerSecond}px`;
177
184
  }
178
185
  scene.render();
179
186
  };
@@ -188,14 +195,48 @@ export function TimelinePanel(props) {
188
195
  });
189
196
  }
190
197
  };
191
- const restart = () => {
192
- applyScrub(0);
198
+ const restartFocus = () => {
199
+ scrubFocusTo(binding, renderer, 0);
200
+ const state = playbackRef.current;
201
+ setFocusElapsed(state, focusRootId, 0);
202
+ clearFocusFinished(state, focusRootId);
203
+ if (stagePlaying) {
204
+ playFocus(binding);
205
+ }
206
+ if (playheadRef.current) {
207
+ playheadRef.current.style.left = `${LABEL_WIDTH + TIMELINE_INSET}px`;
208
+ }
209
+ scene.render();
210
+ };
211
+ const handleStagePlayPause = () => {
212
+ const state = playbackRef.current;
213
+ if (!stagePlaying) {
214
+ if (isFocusFinished(state, focusRootId)) {
215
+ binding.restart();
216
+ clearFocusFinished(state, focusRootId);
217
+ setFocusElapsed(state, focusRootId, 0);
218
+ scene.render();
219
+ }
220
+ state.stagePlaying = true;
221
+ setStagePlaying(true);
222
+ return;
223
+ }
224
+ state.stagePlaying = false;
225
+ setStagePlaying(false);
193
226
  };
194
227
  const ticks = [];
195
228
  for (let t = 0; t <= timelineSpan; t += pickTickInterval(pxPerSecond)) {
196
229
  ticks.push(t);
197
230
  }
198
- const overlayButton = { ...buttonStyle, padding: '5px 9px', minWidth: 32 };
231
+ const overlayButton = {
232
+ ...buttonStyle,
233
+ padding: '5px 9px',
234
+ minWidth: 32,
235
+ display: 'inline-flex',
236
+ alignItems: 'center',
237
+ justifyContent: 'center',
238
+ gap: 6,
239
+ };
199
240
  return (_jsxs("div", { style: {
200
241
  flexShrink: 0,
201
242
  height: PANEL_HEIGHT,
@@ -205,71 +246,61 @@ export function TimelinePanel(props) {
205
246
  flexDirection: 'column',
206
247
  fontFamily: theme.font,
207
248
  color: theme.text,
208
- }, 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, title: paused ? 'Play' : 'Pause', onClick: () => {
209
- // A finished one-shot effect already snapped itself back to frame 0 and
210
- // paused (see the tick effect above), so a plain toggle is enough here.
211
- playbackRef.current.paused = !paused;
212
- setPaused(!paused);
213
- }, children: paused ? '▶' : '⏸' }), _jsx("button", { className: "qe-hover", style: overlayButton, title: "Step one frame", disabled: !paused, onClick: () => (playbackRef.current.stepQueued = true), children: "\u23ED" }), _jsx("button", { className: "qe-hover", style: overlayButton, title: "Restart", onClick: restart, children: "\u27F2" }), _jsx("select", { className: "qe-hover", style: { ...overlayButton, padding: '5px 4px' }, title: "Playback speed", value: speed, onChange: (e) => {
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) => {
214
250
  const next = Number(e.target.value);
215
251
  playbackRef.current.speed = next;
216
252
  setSpeed(next);
217
- }, 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: ["Group (", groupSelection.size, ")"] }))] }), _jsxs("div", { ref: bodyRef, style: { position: 'relative', flex: 1, minHeight: 0 }, children: [_jsxs("div", { style: {
218
- position: 'absolute',
219
- top: 0,
220
- left: 0,
221
- right: 0,
222
- height: RULER_HEIGHT,
223
- display: 'grid',
224
- gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
225
- background: theme.panelBg,
226
- zIndex: 2,
227
- }, 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) => {
228
- playbackRef.current.scrubbing = true;
229
- e.target.setPointerCapture(e.pointerId);
230
- queueScrub(e.clientX);
231
- }, onPointerMove: (e) => {
232
- if (playbackRef.current.scrubbing)
233
- queueScrub(e.clientX);
234
- }, onPointerUp: (e) => {
235
- playbackRef.current.scrubbing = false;
236
- e.target.releasePointerCapture?.(e.pointerId);
237
- }, 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))) })] }), _jsxs("div", { className: "qe-hover-bg", title: "Preview parent \u2014 simulates effect.parent = someNode; not saved in the exported JSON", style: {
238
- position: 'absolute',
239
- top: RULER_HEIGHT,
240
- left: 0,
241
- right: 0,
242
- height: STAGE_ROW_HEIGHT,
243
- display: 'flex',
244
- alignItems: 'center',
245
- gap: 6,
246
- padding: '0 8px',
247
- fontSize: 12.5,
248
- fontStyle: 'italic',
249
- cursor: 'pointer',
250
- color: selectedNode === null ? theme.text : theme.textDim,
251
- borderBottom: `1px solid ${theme.border}`,
252
- ...(selectedNode === null ? { background: 'rgba(60, 105, 209, 0.3)' } : {}),
253
- }, onClick: () => onSelectNode(null), children: [_jsx("span", { style: { fontSize: 10, width: 10 }, children: "\u25CE" }), _jsx("span", { children: "Stage (preview parent)" })] }), _jsx("div", { style: {
254
- position: 'absolute',
255
- top: RULER_HEIGHT + STAGE_ROW_HEIGHT,
256
- left: 0,
257
- right: 0,
258
- bottom: 0,
259
- overflowY: 'auto',
260
- overflowX: 'hidden',
261
- }, children: _jsx("div", { style: {
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: {
254
+ position: 'absolute',
255
+ inset: 0,
256
+ display: 'flex',
257
+ alignItems: 'center',
258
+ justifyContent: 'center',
259
+ padding: '0 24px',
260
+ textAlign: 'center',
261
+ fontSize: 13,
262
+ color: theme.textDim,
263
+ lineHeight: 1.5,
264
+ }, children: "Selected effect is not in the scene. Drag it into the viewport to preview playback." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
265
+ position: 'absolute',
266
+ top: 0,
267
+ left: 0,
268
+ right: 0,
269
+ height: RULER_HEIGHT,
262
270
  display: 'grid',
263
271
  gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
264
- gridAutoRows: 'min-content',
265
- }, 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: {
266
- position: 'absolute',
267
- top: 0,
268
- bottom: 0,
269
- left: LABEL_WIDTH + TIMELINE_INSET,
270
- width: 1,
271
- background: theme.accent,
272
- pointerEvents: 'none',
273
- zIndex: 3,
274
- } })] })] }));
272
+ background: theme.panelBg,
273
+ 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) => {
275
+ playbackRef.current.scrubbing = true;
276
+ e.target.setPointerCapture(e.pointerId);
277
+ queueScrub(e.clientX);
278
+ }, onPointerMove: (e) => {
279
+ if (playbackRef.current.scrubbing)
280
+ queueScrub(e.clientX);
281
+ }, onPointerUp: (e) => {
282
+ playbackRef.current.scrubbing = false;
283
+ 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: {
285
+ position: 'absolute',
286
+ top: RULER_HEIGHT,
287
+ left: 0,
288
+ right: 0,
289
+ bottom: 0,
290
+ overflowY: 'auto',
291
+ overflowX: 'hidden',
292
+ }, children: _jsx("div", { style: {
293
+ display: 'grid',
294
+ gridTemplateColumns: `${LABEL_WIDTH}px 1fr`,
295
+ 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: {
297
+ position: 'absolute',
298
+ top: 0,
299
+ bottom: 0,
300
+ left: LABEL_WIDTH + TIMELINE_INSET,
301
+ width: 1,
302
+ background: theme.accent,
303
+ pointerEvents: 'none',
304
+ zIndex: 3,
305
+ } })] })) })] }));
275
306
  }
@@ -1,13 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
+ import { CheckIcon, ChevronDownIcon, ChevronRightIcon, EyeIcon, EyeSlashIcon, PencilSquareIcon, PlusIcon, SparklesIcon, SquaresPlusIcon, XMarkIcon, } from '@heroicons/react/24/solid';
3
4
  import { buildEffectTree, collectSystems } from '../core/effectTree';
4
5
  import { createChildSystem, removeSystems } from '../core/systems';
5
6
  import { ungroupNode } from '../core/groups';
6
7
  import { PromptDialog } from './PromptDialog';
8
+ import { iconStyle } from './icons';
7
9
  import { theme } from './theme';
8
- const iconButton = { background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, padding: 0 };
10
+ const iconButton = {
11
+ background: 'none',
12
+ border: 'none',
13
+ cursor: 'pointer',
14
+ padding: 0,
15
+ display: 'inline-flex',
16
+ alignItems: 'center',
17
+ justifyContent: 'center',
18
+ };
9
19
  const ROW_HEIGHT = 28;
10
- const checkboxButton = { ...iconButton, fontSize: 12, width: 12 };
20
+ const checkboxButton = { ...iconButton, width: 12, height: 12 };
11
21
  /** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
12
22
  export function TimelineTrackRow(props) {
13
23
  const { row, binding, selected, onSelectNode, pxPerSecond, offsetPx, collapsed, onToggleCollapse, hidden, onToggleVisible, groupSelected, onToggleGroupSelect } = props;
@@ -34,10 +44,10 @@ export function TimelineTrackRow(props) {
34
44
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, borderTop: `1px solid ${theme.border}` }, onClick: () => onSelectNode(row.node), children: [!isRoot && (_jsx("button", { className: "qe-hover", title: groupSelected ? 'Remove from group selection' : 'Select for grouping', style: { ...checkboxButton, color: groupSelected ? theme.accent : theme.textDim }, onClick: (e) => {
35
45
  e.stopPropagation();
36
46
  onToggleGroupSelect();
37
- }, children: groupSelected ? '☑' : '☐' })), _jsx("span", { className: "qe-hover", title: collapsed ? 'Expand' : 'Collapse', style: { color: theme.textDim, fontSize: 10, width: 10, cursor: 'pointer' }, onClick: (e) => {
47
+ }, children: groupSelected ? (_jsx(CheckIcon, { style: iconStyle(12) })) : (_jsx("span", { style: { width: 10, height: 10, border: `1px solid currentColor`, borderRadius: 2, display: 'block', boxSizing: 'border-box' } })) })), _jsx("span", { className: "qe-hover", title: collapsed ? 'Expand' : 'Collapse', style: { color: theme.textDim, width: 12, cursor: 'pointer', display: 'inline-flex' }, onClick: (e) => {
38
48
  e.stopPropagation();
39
49
  onToggleCollapse?.();
40
- }, children: collapsed ? '▸' : '▾' }), _jsx("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: row.name }), _jsx("button", { className: "qe-hover", title: "Add system to this group", style: { ...iconButton, color: theme.accent, fontSize: 13 }, onClick: (e) => {
50
+ }, children: collapsed ? _jsx(ChevronRightIcon, { style: iconStyle(12) }) : _jsx(ChevronDownIcon, { style: iconStyle(12) }) }), _jsx("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: row.name }), _jsx("button", { className: "qe-hover", title: "Add system to this group", style: { ...iconButton, color: theme.accent }, onClick: (e) => {
41
51
  e.stopPropagation();
42
52
  let created;
43
53
  binding.apply(() => {
@@ -51,17 +61,17 @@ export function TimelineTrackRow(props) {
51
61
  binding.addSubSystem(created);
52
62
  onSelectNode(created.emitter);
53
63
  }
54
- }, children: "+" }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
64
+ }, children: _jsx(PlusIcon, { style: iconStyle(13) }) }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
55
65
  e.stopPropagation();
56
66
  setRenaming(true);
57
- }, children: "\u270E" })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Ungroup (moves its contents up a level)", style: { ...iconButton, color: theme.textDim, fontSize: 12 }, onClick: (e) => {
67
+ }, children: _jsx(PencilSquareIcon, { style: iconStyle(12) }) })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Ungroup (moves its contents up a level)", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
58
68
  e.stopPropagation();
59
69
  binding.apply(() => ungroupNode(row.node));
60
- }, children: "\u26F6" })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Remove group", style: { ...iconButton, color: '#e08c8c', fontSize: 12 }, onClick: (e) => {
70
+ }, children: _jsx(SquaresPlusIcon, { style: iconStyle(12) }) })), !isRoot && (_jsx("button", { className: "qe-hover", title: "Remove group", style: { ...iconButton, color: '#e08c8c' }, onClick: (e) => {
61
71
  e.stopPropagation();
62
72
  removeSystems(binding, collectSystems(buildEffectTree(row.node)), (s) => onSelectNode(s.emitter));
63
73
  binding.apply(() => row.node.dispose(true, false));
64
- }, children: "\u2715" }))] }), _jsx("div", { className: "qe-hover-bg", style: { height: ROW_HEIGHT, borderTop: `1px solid ${theme.border}` }, onClick: () => onSelectNode(row.node) }), renaming && (_jsx(PromptDialog, { title: "Rename", defaultValue: row.name, onSubmit: (name) => {
74
+ }, children: _jsx(XMarkIcon, { style: iconStyle(12) }) }))] }), _jsx("div", { className: "qe-hover-bg", style: { height: ROW_HEIGHT, borderTop: `1px solid ${theme.border}` }, onClick: () => onSelectNode(row.node) }), renaming && (_jsx(PromptDialog, { title: "Rename", defaultValue: row.name, onSubmit: (name) => {
65
75
  binding.apply(() => (row.node.name = name));
66
76
  setRenaming(false);
67
77
  }, onCancel: () => setRenaming(false) }))] }));
@@ -72,16 +82,16 @@ export function TimelineTrackRow(props) {
72
82
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "qe-hover-bg", style: { ...labelStyle, opacity: hidden ? 0.5 : 1 }, onClick: () => onSelectNode(row.node), children: [!isRoot && (_jsx("button", { className: "qe-hover", title: groupSelected ? 'Remove from group selection' : 'Select for grouping', style: { ...checkboxButton, color: groupSelected ? theme.accent : theme.textDim }, onClick: (e) => {
73
83
  e.stopPropagation();
74
84
  onToggleGroupSelect();
75
- }, children: groupSelected ? '☑' : '☐' })), _jsx("button", { className: "qe-hover", title: hidden ? 'Show in viewport' : 'Hide in viewport', style: { ...iconButton, color: hidden ? theme.textDim : theme.accent, fontSize: 12 }, onClick: (e) => {
85
+ }, children: groupSelected ? (_jsx(CheckIcon, { style: iconStyle(12) })) : (_jsx("span", { style: { width: 10, height: 10, border: `1px solid currentColor`, borderRadius: 2, display: 'block', boxSizing: 'border-box' } })) })), _jsx("button", { className: "qe-hover", title: hidden ? 'Show in viewport' : 'Hide in viewport', style: { ...iconButton, color: hidden ? theme.textDim : theme.accent }, onClick: (e) => {
76
86
  e.stopPropagation();
77
87
  onToggleVisible?.();
78
- }, children: hidden ? '◯' : '◉' }), _jsx("span", { style: { color: theme.accent, fontSize: 10, width: 10 }, children: "\u25C6" }), _jsxs("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: [row.name, system.onlyUsedByOther ? ' (sub)' : ''] }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
88
+ }, children: hidden ? _jsx(EyeSlashIcon, { style: iconStyle(12) }) : _jsx(EyeIcon, { style: iconStyle(12) }) }), _jsx("span", { style: { color: theme.accent, display: 'inline-flex' }, children: _jsx(SparklesIcon, { style: iconStyle(11) }) }), _jsxs("span", { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }, children: [row.name, system.onlyUsedByOther ? ' (sub)' : ''] }), !isRoot && (_jsx("button", { className: "qe-hover", title: "Rename", style: { ...iconButton, color: theme.textDim }, onClick: (e) => {
79
89
  e.stopPropagation();
80
90
  setRenaming(true);
81
- }, children: "\u270E" })), !isRoot && row.removable && (_jsx("button", { className: "qe-hover", title: "Remove system", style: { ...iconButton, color: '#e08c8c', fontSize: 12 }, onClick: (e) => {
91
+ }, children: _jsx(PencilSquareIcon, { style: iconStyle(12) }) })), !isRoot && row.removable && (_jsx("button", { className: "qe-hover", title: "Remove system", style: { ...iconButton, color: '#e08c8c' }, onClick: (e) => {
82
92
  e.stopPropagation();
83
93
  removeSystems(binding, [system], (s) => onSelectNode(s.emitter));
84
- }, children: "\u2715" })), selected && (_jsx("button", { className: "qe-hover", title: "Add child system", style: { ...iconButton, color: theme.accent, fontSize: 13 }, onClick: (e) => {
94
+ }, children: _jsx(XMarkIcon, { style: iconStyle(12) }) })), selected && (_jsx("button", { className: "qe-hover", title: "Add child system", style: { ...iconButton, color: theme.accent }, onClick: (e) => {
85
95
  e.stopPropagation();
86
96
  let created;
87
97
  binding.apply(() => {
@@ -92,7 +102,7 @@ export function TimelineTrackRow(props) {
92
102
  binding.addSubSystem(created);
93
103
  onSelectNode(created.emitter);
94
104
  }
95
- }, children: "+" }))] }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: ROW_HEIGHT }, onClick: () => onSelectNode(row.node), children: _jsx("div", { className: "qe-hover", title: row.startOffsetVariable ? 'Start time varies per spawn' : undefined, style: {
105
+ }, children: _jsx(PlusIcon, { style: iconStyle(13) }) }))] }), _jsx("div", { className: "qe-hover-bg", style: { position: 'relative', height: ROW_HEIGHT }, onClick: () => onSelectNode(row.node), children: _jsx("div", { className: "qe-hover", title: row.startOffsetVariable ? 'Start time varies per spawn' : undefined, style: {
96
106
  position: 'absolute',
97
107
  left: barLeft,
98
108
  width: barWidth,
@@ -100,10 +110,14 @@ export function TimelineTrackRow(props) {
100
110
  height: ROW_HEIGHT - 10,
101
111
  borderRadius: 4,
102
112
  cursor: 'pointer',
103
- background: system.onlyUsedByOther ? 'repeating-linear-gradient(135deg, rgba(158,185,255,0.25), rgba(158,185,255,0.25) 4px, rgba(158,185,255,0.4) 4px, rgba(158,185,255,0.4) 8px)' : selected ? theme.accent : '#3c5aa0',
113
+ boxSizing: 'border-box',
114
+ background: system.onlyUsedByOther
115
+ ? 'repeating-linear-gradient(135deg, rgba(158,185,255,0.25), rgba(158,185,255,0.25) 4px, rgba(158,185,255,0.4) 4px, rgba(158,185,255,0.4) 8px)'
116
+ : selected
117
+ ? theme.accent
118
+ : '#3c5aa0',
104
119
  border: system.onlyUsedByOther ? `1px dashed ${theme.accent}` : `1px solid ${theme.border}`,
105
120
  opacity: hidden ? 0.3 : system.onlyUsedByOther ? 0.75 : 1,
106
- boxSizing: 'border-box',
107
121
  }, children: row.looping && (_jsx("span", { style: { position: 'absolute', right: 4, top: -1, fontSize: 10, color: theme.text }, children: "\u27F3" })) }) }), renaming && (_jsx(PromptDialog, { title: "Rename", defaultValue: row.name, onSubmit: (name) => {
108
122
  binding.apply(() => (row.node.name = name));
109
123
  setRenaming(false);
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ /** Themed hover/focus tooltip rendered in a portal so it is not clipped by scroll containers. */
3
+ export declare function Tooltip(props: {
4
+ content: string;
5
+ children: React.ReactNode;
6
+ }): React.ReactElement;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ const TOOLTIP_MAX_WIDTH = 260;
5
+ const VIEWPORT_PAD = 8;
6
+ /** Positions a fixed tooltip beside the anchor, flipping when near the viewport edge. */
7
+ function placeTooltip(anchor) {
8
+ const centerY = anchor.top + anchor.height / 2;
9
+ const top = Math.min(Math.max(centerY, VIEWPORT_PAD + 24), window.innerHeight - VIEWPORT_PAD - 24);
10
+ const preferRight = anchor.right + 8 + TOOLTIP_MAX_WIDTH <= window.innerWidth - VIEWPORT_PAD;
11
+ if (preferRight) {
12
+ return { left: anchor.right + 8, top, placement: 'right' };
13
+ }
14
+ return { left: anchor.left - 8, top, placement: 'left' };
15
+ }
16
+ /** Themed hover/focus tooltip rendered in a portal so it is not clipped by scroll containers. */
17
+ export function Tooltip(props) {
18
+ const { content, children } = props;
19
+ const anchorRef = useRef(null);
20
+ const [open, setOpen] = useState(false);
21
+ const [layout, setLayout] = useState(null);
22
+ const updateLayout = useCallback(() => {
23
+ const anchor = anchorRef.current?.getBoundingClientRect();
24
+ if (!anchor) {
25
+ return;
26
+ }
27
+ setLayout(placeTooltip(anchor));
28
+ }, []);
29
+ const show = () => {
30
+ updateLayout();
31
+ setOpen(true);
32
+ };
33
+ const hide = () => setOpen(false);
34
+ useLayoutEffect(() => {
35
+ if (!open) {
36
+ return;
37
+ }
38
+ updateLayout();
39
+ const onReflow = () => updateLayout();
40
+ window.addEventListener('scroll', onReflow, true);
41
+ window.addEventListener('resize', onReflow);
42
+ return () => {
43
+ window.removeEventListener('scroll', onReflow, true);
44
+ window.removeEventListener('resize', onReflow);
45
+ };
46
+ }, [open, updateLayout]);
47
+ const tooltipStyle = layout?.placement === 'left'
48
+ ? {
49
+ position: 'fixed',
50
+ left: layout.left,
51
+ top: layout.top,
52
+ transform: 'translate(-100%, -50%)',
53
+ maxWidth: TOOLTIP_MAX_WIDTH,
54
+ }
55
+ : {
56
+ position: 'fixed',
57
+ left: layout?.left,
58
+ top: layout?.top,
59
+ transform: 'translateY(-50%)',
60
+ maxWidth: TOOLTIP_MAX_WIDTH,
61
+ };
62
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ref: anchorRef, style: { display: 'inline-flex', verticalAlign: 'middle' }, onMouseEnter: show, onMouseLeave: hide, onFocus: show, onBlur: hide, children: children }), open && layout
63
+ ? createPortal(_jsx("div", { role: "tooltip", className: `qe-tooltip qe-tooltip--${layout.placement}`, style: tooltipStyle, children: content }), document.body)
64
+ : null] }));
65
+ }
@@ -6,6 +6,8 @@ import { ScalarGenerator } from '../core/values';
6
6
  */
7
7
  export declare function ValueField(props: {
8
8
  label: string;
9
+ hint?: string;
10
+ hintKey?: string;
9
11
  generator: ScalarGenerator | undefined;
10
12
  onChange: (generator: ScalarGenerator) => void;
11
13
  min?: number;
@@ -11,7 +11,7 @@ export function ValueField(props) {
11
11
  const commit = (patch) => {
12
12
  props.onChange(buildScalar({ ...state, ...patch }));
13
13
  };
14
- return (_jsxs(_Fragment, { children: [_jsx(Row, { label: props.label, children: _jsx(SelectField, { value: state.mode, options: [
14
+ return (_jsxs(_Fragment, { children: [_jsx(Row, { label: props.label, hint: props.hint, hintKey: props.hintKey, children: _jsx(SelectField, { value: state.mode, options: [
15
15
  { value: 'constant', label: 'Constant' },
16
16
  { value: 'random', label: 'Random between two' },
17
17
  { value: 'curve', label: 'Curve' },
@@ -32,7 +32,7 @@ export function LimitSpeedOverLifeModule({ binding }) {
32
32
  return (_jsx(BehaviorModule, { binding: binding, title: "Limit Speed over Lifetime", type: "LimitSpeedOverLife", create: () => new LimitSpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [4, 2, 1, 1] }), 0.5), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 8, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) }), _jsx(Row, { label: "Dampen", children: _jsx(NumberField, { value: behavior.dampen, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.dampen = Math.min(1, v))) }) })] })) }));
33
33
  }
34
34
  export function ForceOverLifeModule({ binding }) {
35
- return (_jsx(BehaviorModule, { binding: binding, title: "Force over Lifetime", type: "ForceOverLife", create: () => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { label: "X", generator: behavior.x, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.x = g)) }), _jsx(ValueField, { label: "Y", generator: behavior.y, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.y = g)) }), _jsx(ValueField, { label: "Z", generator: behavior.z, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.z = g)) })] })) }));
35
+ return (_jsx(BehaviorModule, { binding: binding, title: "Force over Lifetime", type: "ForceOverLife", create: () => new ForceOverLife(new ConstantValue(0), new ConstantValue(2), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "force.x", label: "X", generator: behavior.x, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.x = g)) }), _jsx(ValueField, { hintKey: "force.y", label: "Y", generator: behavior.y, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.y = g)) }), _jsx(ValueField, { hintKey: "force.z", label: "Z", generator: behavior.z, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.z = g)) })] })) }));
36
36
  }
37
37
  export function VelocityOverLifeModule({ binding }) {
38
38
  return (_jsx(BehaviorModule, { binding: binding, title: "Velocity over Lifetime", type: "VelocityOverLife", create: () => new VelocityOverLife(new ConstantValue(0), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
@@ -41,19 +41,19 @@ export function VelocityOverLifeModule({ binding }) {
41
41
  ], onChange: (v) => binding.apply(() => (behavior.space = v)) }) }), _jsx(ValueField, { label: "Linear X", generator: behavior.linearX, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearX = g)) }), _jsx(ValueField, { label: "Linear Y", generator: behavior.linearY, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearY = g)) }), _jsx(ValueField, { label: "Linear Z", generator: behavior.linearZ, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearZ = g)) }), _jsx(ValueField, { label: "Orbital X", generator: behavior.orbitalX, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalX = g)) }), _jsx(ValueField, { label: "Orbital Y", generator: behavior.orbitalY, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalY = g)) }), _jsx(ValueField, { label: "Orbital Z", generator: behavior.orbitalZ, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalZ = g)) })] })) }));
42
42
  }
43
43
  export function InheritVelocityModule({ binding }) {
44
- return (_jsx(BehaviorModule, { binding: binding, title: "Inherit Velocity", type: "InheritVelocity", create: () => new InheritVelocity(new ConstantValue(1), 'initial'), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Mode", children: _jsx(SelectField, { value: behavior.mode, options: [
44
+ return (_jsx(BehaviorModule, { binding: binding, title: "Inherit Velocity", type: "InheritVelocity", create: () => new InheritVelocity(new ConstantValue(1), 'initial'), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Mode", hintKey: "inheritVelocity.mode", children: _jsx(SelectField, { value: behavior.mode, options: [
45
45
  { value: 'initial', label: 'Initial' },
46
46
  { value: 'current', label: 'Current' },
47
47
  ], onChange: (v) => binding.apply(() => (behavior.mode = v)) }) }), _jsx(ValueField, { label: "Multiplier", generator: behavior.multiplier, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.multiplier = g)) })] })) }));
48
48
  }
49
49
  export function GravityModule({ binding }) {
50
- return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsx(Row, { label: "Strength", children: _jsx(NumberField, { value: readScalar(behavior.magnitude).value, step: 0.5, onChange: (v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v))) }) })) }));
50
+ return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsx(Row, { label: "Strength", hintKey: "gravity.strength", children: _jsx(NumberField, { value: readScalar(behavior.magnitude).value, step: 0.5, onChange: (v) => binding.apply(() => (behavior.magnitude = new ConstantValue(v))) }) })) }));
51
51
  }
52
52
  export function RotationOverLifeModule({ binding }) {
53
- return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime", type: "RotationOverLife", create: () => new RotationOverLife(new ConstantValue(Math.PI)), children: (behavior) => (_jsx(ValueField, { label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) })) }));
53
+ return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime", type: "RotationOverLife", create: () => new RotationOverLife(new ConstantValue(Math.PI)), children: (behavior) => (_jsx(ValueField, { hintKey: "rotationOverLife.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) })) }));
54
54
  }
55
55
  export function NoiseModule({ binding }) {
56
- return (_jsx(BehaviorModule, { binding: binding, title: "Noise", type: "Noise", create: () => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { label: "Frequency", generator: behavior.frequency, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.frequency = g)) }), _jsx(ValueField, { label: "Strength", generator: behavior.power, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.power = g)) }), _jsx(ValueField, { label: "Position amount", generator: behavior.positionAmount, min: 0, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.positionAmount = g)) }), _jsx(ValueField, { label: "Rotation amount", generator: behavior.rotationAmount, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.rotationAmount = g)) })] })) }));
56
+ return (_jsx(BehaviorModule, { binding: binding, title: "Noise", type: "Noise", create: () => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "noise.frequency", label: "Frequency", generator: behavior.frequency, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.frequency = g)) }), _jsx(ValueField, { hintKey: "noise.strength", label: "Strength", generator: behavior.power, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.power = g)) }), _jsx(ValueField, { label: "Position amount", generator: behavior.positionAmount, min: 0, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.positionAmount = g)) }), _jsx(ValueField, { label: "Rotation amount", generator: behavior.rotationAmount, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.rotationAmount = g)) })] })) }));
57
57
  }
58
58
  /** Three numeric inputs for a Vector3 field, replacing the whole vector on any edit. */
59
59
  function Vector3Row(props) {
@@ -62,10 +62,10 @@ function Vector3Row(props) {
62
62
  next[axis] = n;
63
63
  props.onChange(next);
64
64
  };
65
- return (_jsx(Row, { label: props.label, children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: props.value.x, min: props.min, step: props.step, onChange: (n) => set('x', n) }), _jsx(NumberField, { value: props.value.y, min: props.min, step: props.step, onChange: (n) => set('y', n) }), _jsx(NumberField, { value: props.value.z, min: props.min, step: props.step, onChange: (n) => set('z', n) })] }) }));
65
+ return (_jsx(Row, { label: props.label, hintKey: props.hintKey, children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: props.value.x, min: props.min, step: props.step, onChange: (n) => set('x', n) }), _jsx(NumberField, { value: props.value.y, min: props.min, step: props.step, onChange: (n) => set('y', n) }), _jsx(NumberField, { value: props.value.z, min: props.min, step: props.step, onChange: (n) => set('z', n) })] }) }));
66
66
  }
67
67
  export function TurbulenceModule({ binding }) {
68
- return (_jsx(BehaviorModule, { binding: binding, title: "Turbulence", type: "TurbulenceField", create: () => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { label: "Scale", value: behavior.scale, min: 0.01, step: 0.1, onChange: (v) => binding.apply(() => (behavior.scale = v)) }), _jsx(Row, { label: "Octaves", children: _jsx(NumberField, { value: behavior.octaves, min: 1, step: 1, onChange: (v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v)))) }) }), _jsx(Vector3Row, { label: "Strength", value: behavior.velocityMultiplier, step: 0.5, onChange: (v) => binding.apply(() => (behavior.velocityMultiplier = v)) }), _jsx(Vector3Row, { label: "Time scale", value: behavior.timeScale, step: 0.1, onChange: (v) => binding.apply(() => (behavior.timeScale = v)) })] })) }));
68
+ return (_jsx(BehaviorModule, { binding: binding, title: "Turbulence", type: "TurbulenceField", create: () => new TurbulenceField(new Vector3(1, 1, 1), 3, new Vector3(1, 1, 1), new Vector3(1, 1, 1)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { hintKey: "turbulence.scale", label: "Scale", value: behavior.scale, min: 0.01, step: 0.1, onChange: (v) => binding.apply(() => (behavior.scale = v)) }), _jsx(Row, { label: "Octaves", children: _jsx(NumberField, { value: behavior.octaves, min: 1, step: 1, onChange: (v) => binding.apply(() => (behavior.octaves = Math.max(1, Math.round(v)))) }) }), _jsx(Vector3Row, { hintKey: "turbulence.strength", label: "Strength", value: behavior.velocityMultiplier, step: 0.5, onChange: (v) => binding.apply(() => (behavior.velocityMultiplier = v)) }), _jsx(Vector3Row, { label: "Time scale", value: behavior.timeScale, step: 0.1, onChange: (v) => binding.apply(() => (behavior.timeScale = v)) })] })) }));
69
69
  }
70
70
  export function EmitDirectionModule({ binding }) {
71
71
  return (_jsx(BehaviorModule, { binding: binding, title: "Randomize Direction", type: "ChangeEmitDirection", create: () => new ChangeEmitDirection(new IntervalValue(0, Math.PI / 6)), children: (behavior) => (_jsx(ValueField, { label: "Angle (rad)", generator: behavior.angle, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.angle = g)) })) }));
@@ -87,7 +87,7 @@ export function SizeBySpeedModule({ binding }) {
87
87
  return (_jsx(BehaviorModule, { binding: binding, title: "Size by Speed", type: "SizeBySpeed", create: () => new SizeBySpeed(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [0.5, 0.8, 1.2, 1.5] }), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.size), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.size = buildCurve(pieces))) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
88
88
  }
89
89
  export function RotationBySpeedModule({ binding }) {
90
- return (_jsx(BehaviorModule, { binding: binding, title: "Rotation by Speed", type: "RotationBySpeed", create: () => new RotationBySpeed(new ConstantValue(Math.PI), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
90
+ return (_jsx(BehaviorModule, { binding: binding, title: "Rotation by Speed", type: "RotationBySpeed", create: () => new RotationBySpeed(new ConstantValue(Math.PI), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "rotationBySpeed.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
91
91
  }
92
92
  export function OrbitOverLifeModule({ binding }) {
93
93
  return (_jsx(BehaviorModule, { binding: binding, title: "Orbit over Lifetime", type: "OrbitOverLife", create: () => new OrbitOverLife(new ConstantValue(Math.PI), new Vector3(0, 1, 0)), children: (behavior) => (_jsx(ValueField, { label: "Orbit speed", generator: behavior.orbitSpeed, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitSpeed = g)) })) }));
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
2
  export declare function Row(props: {
3
3
  label: string;
4
+ hint?: string;
5
+ hintKey?: string;
4
6
  children: React.ReactNode;
5
7
  }): React.JSX.Element;
6
8
  /** Number input that keeps focus-friendly local text state and commits parsed values. */