babylon.quarks-editor 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +31 -4
  3. package/dist/core/assetTree.d.ts +5 -0
  4. package/dist/core/assets.d.ts +7 -0
  5. package/dist/core/assets.js +11 -7
  6. package/dist/core/binding.d.ts +9 -0
  7. package/dist/core/binding.js +8 -0
  8. package/dist/core/collision.d.ts +4 -0
  9. package/dist/core/collision.js +4 -0
  10. package/dist/core/colors.d.ts +16 -0
  11. package/dist/core/colors.js +8 -0
  12. package/dist/core/defaultEffect.d.ts +3 -2
  13. package/dist/core/defaultEffect.js +1 -1
  14. package/dist/core/editorSession.d.ts +229 -0
  15. package/dist/core/editorSession.js +983 -0
  16. package/dist/core/effectTree.d.ts +5 -0
  17. package/dist/core/galleryPlayback.js +2 -1
  18. package/dist/core/history.d.ts +6 -0
  19. package/dist/core/history.js +4 -0
  20. package/dist/core/indexedDbProjectStore.d.ts +13 -0
  21. package/dist/core/indexedDbProjectStore.js +13 -0
  22. package/dist/core/material.d.ts +12 -2
  23. package/dist/core/playback.d.ts +6 -0
  24. package/dist/core/projectStore.d.ts +26 -0
  25. package/dist/core/projectStore.js +1 -0
  26. package/dist/core/sceneEntries.d.ts +3 -0
  27. package/dist/core/scrub.d.ts +1 -1
  28. package/dist/core/scrub.js +2 -2
  29. package/dist/core/shapes.d.ts +24 -0
  30. package/dist/core/shapes.js +5 -0
  31. package/dist/core/systems.d.ts +2 -0
  32. package/dist/core/systems.js +3 -1
  33. package/dist/core/timeline.d.ts +23 -3
  34. package/dist/core/values.d.ts +21 -4
  35. package/dist/core/values.js +5 -0
  36. package/dist/index.d.ts +5 -1
  37. package/dist/react/AssetsPanel.d.ts +7 -0
  38. package/dist/react/ChoiceDialog.d.ts +7 -0
  39. package/dist/react/ContextMenu.d.ts +10 -1
  40. package/dist/react/EditorWorkspace.d.ts +7 -0
  41. package/dist/react/EditorWorkspace.js +6 -3
  42. package/dist/react/EffectEditor.d.ts +8 -2
  43. package/dist/react/EffectEditorHost.d.ts +32 -28
  44. package/dist/react/EffectEditorHost.js +241 -1665
  45. package/dist/react/EffectTreeView.d.ts +3 -0
  46. package/dist/react/MessageDialog.d.ts +5 -0
  47. package/dist/react/ModuleSection.d.ts +10 -0
  48. package/dist/react/ModuleSection.js +4 -1
  49. package/dist/react/ProjectsPage.d.ts +6 -0
  50. package/dist/react/ProjectsPage.js +27 -11
  51. package/dist/react/PromptDialog.d.ts +7 -0
  52. package/dist/react/ScenePanel.d.ts +7 -0
  53. package/dist/react/ScenePanel.js +7 -5
  54. package/dist/react/TextureSelect.d.ts +5 -5
  55. package/dist/react/TimelinePanel.d.ts +13 -0
  56. package/dist/react/TimelinePanel.js +10 -6
  57. package/dist/react/TimelineTrackRow.d.ts +10 -0
  58. package/dist/react/TransportBar.d.ts +9 -0
  59. package/dist/react/ValueField.d.ts +32 -5
  60. package/dist/react/ValueField.js +20 -9
  61. package/dist/react/behaviorModules.d.ts +16 -5
  62. package/dist/react/behaviorModules.js +16 -1
  63. package/dist/react/fields.d.ts +8 -0
  64. package/dist/react/fields.js +3 -0
  65. package/dist/react/host/BottomDocks.d.ts +36 -0
  66. package/dist/react/host/BottomDocks.js +63 -0
  67. package/dist/react/host/HostDialogs.d.ts +50 -0
  68. package/dist/react/host/HostDialogs.js +113 -0
  69. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  70. package/dist/react/host/InspectorSidebar.js +59 -0
  71. package/dist/react/host/SceneSidebar.d.ts +53 -0
  72. package/dist/react/host/SceneSidebar.js +62 -0
  73. package/dist/react/host/ViewportPane.d.ts +56 -0
  74. package/dist/react/host/ViewportPane.js +91 -0
  75. package/dist/react/host/hostHooks.d.ts +44 -0
  76. package/dist/react/host/hostHooks.js +164 -0
  77. package/dist/react/host/hostStyles.d.ts +21 -0
  78. package/dist/react/host/hostStyles.js +57 -0
  79. package/dist/react/index.d.ts +3 -1
  80. package/dist/react/modules.d.ts +17 -6
  81. package/dist/react/modules.js +38 -20
  82. package/dist/react/theme.d.ts +15 -0
  83. package/dist/react/theme.js +15 -0
  84. package/dist/session.d.ts +9 -0
  85. package/dist/session.js +8 -0
  86. package/package.json +17 -11
  87. package/src/core/assetTree.ts +14 -1
  88. package/src/core/assets.ts +25 -10
  89. package/src/core/binding.ts +9 -0
  90. package/src/core/collision.ts +4 -0
  91. package/src/core/colors.ts +21 -1
  92. package/src/core/defaultEffect.ts +4 -3
  93. package/src/core/editorSession.ts +1299 -0
  94. package/src/core/effectTree.ts +5 -0
  95. package/src/core/galleryPlayback.ts +2 -1
  96. package/src/core/history.ts +6 -0
  97. package/src/core/indexedDbProjectStore.ts +13 -0
  98. package/src/core/loadEffect.ts +1 -1
  99. package/src/core/material.ts +14 -4
  100. package/src/core/playback.ts +6 -0
  101. package/src/core/projectStore.ts +26 -0
  102. package/src/core/sceneEntries.ts +3 -0
  103. package/src/core/scrub.ts +2 -2
  104. package/src/core/shapes.ts +28 -1
  105. package/src/core/systems.ts +9 -2
  106. package/src/core/timeline.ts +40 -15
  107. package/src/core/values.ts +20 -4
  108. package/src/index.ts +5 -1
  109. package/src/react/AssetsPanel.tsx +7 -0
  110. package/src/react/ChoiceDialog.tsx +7 -0
  111. package/src/react/ContextMenu.tsx +10 -1
  112. package/src/react/EditorWorkspace.tsx +11 -5
  113. package/src/react/EffectEditor.tsx +7 -1
  114. package/src/react/EffectEditorHost.tsx +406 -2340
  115. package/src/react/EffectTreeView.tsx +3 -0
  116. package/src/react/MessageDialog.tsx +5 -0
  117. package/src/react/ModuleSection.tsx +11 -0
  118. package/src/react/ProjectsPage.tsx +38 -11
  119. package/src/react/PromptDialog.tsx +7 -0
  120. package/src/react/ScenePanel.tsx +14 -5
  121. package/src/react/TextureSelect.tsx +6 -6
  122. package/src/react/TimelinePanel.tsx +23 -6
  123. package/src/react/TimelineTrackRow.tsx +10 -0
  124. package/src/react/TransportBar.tsx +9 -0
  125. package/src/react/ValueField.tsx +58 -14
  126. package/src/react/behaviorModules.tsx +34 -32
  127. package/src/react/fields.tsx +12 -1
  128. package/src/react/host/BottomDocks.tsx +178 -0
  129. package/src/react/host/HostDialogs.tsx +228 -0
  130. package/src/react/host/InspectorSidebar.tsx +147 -0
  131. package/src/react/host/SceneSidebar.tsx +227 -0
  132. package/src/react/host/ViewportPane.tsx +300 -0
  133. package/src/react/host/hostHooks.ts +198 -0
  134. package/src/react/host/hostStyles.ts +65 -0
  135. package/src/react/index.ts +3 -1
  136. package/src/react/modules.tsx +70 -44
  137. package/src/react/theme.ts +15 -0
  138. package/src/session.ts +17 -0
@@ -0,0 +1,300 @@
1
+ import {
2
+ ArrowLeftIcon,
3
+ ArrowPathIcon,
4
+ ArrowsPointingOutIcon,
5
+ ArrowUturnLeftIcon,
6
+ ArrowUturnRightIcon,
7
+ CubeTransparentIcon,
8
+ CursorArrowRaysIcon,
9
+ Square2StackIcon,
10
+ } from '@heroicons/react/24/solid';
11
+ import React from 'react';
12
+ import type {AssetLoadProgress} from '../../core/assets.js';
13
+ import type {PrefabInfo} from '../../core/editorSession.js';
14
+ import {iconStyle, type HeroIcon} from '../icons.js';
15
+ import {buttonStyle, theme} from '../theme.js';
16
+ import {activeSquareButtonStyle, floatingPanelStyle, saveButtonStyle, squareButtonStyle} from './hostStyles.js';
17
+
18
+ /** What the stage gizmo does: nothing, or move, rotate or scale the selected node. */
19
+ export type GizmoMode = 'none' | 'position' | 'rotation' | 'scale';
20
+
21
+ const GIZMO_MODE_BUTTONS: Array<{value: GizmoMode; Icon: HeroIcon; title: string}> = [
22
+ {value: 'none', Icon: CursorArrowRaysIcon, title: 'Select (hide stage gizmo)'},
23
+ {value: 'position', Icon: ArrowsPointingOutIcon, title: 'Move the stage node (position)'},
24
+ {value: 'rotation', Icon: ArrowPathIcon, title: 'Rotate the stage node'},
25
+ {value: 'scale', Icon: Square2StackIcon, title: 'Scale the stage node'},
26
+ ];
27
+
28
+ /** Props of {@link ViewportPane}. */
29
+ export interface ViewportPaneProps {
30
+ /** The canvas the engine draws into. */
31
+ canvasRef: React.RefObject<HTMLCanvasElement | null>;
32
+ /** The render loop writes the frame rate here. */
33
+ fpsRef: React.RefObject<HTMLSpanElement | null>;
34
+ /** Pixels at the bottom covered by the floating Assets drawer. */
35
+ inset: number;
36
+ /** The transport bar, once the scene exists. */
37
+ transport: React.ReactNode;
38
+ /** The asset drag hovering the viewport, if any. */
39
+ dragHover: {label: string; count: number} | null;
40
+ /** Asset import progress, while importing. */
41
+ assetsLoading: AssetLoadProgress | null;
42
+ /** The open prefab, if any. */
43
+ prefabInfo: PrefabInfo | null;
44
+ /** The open prefab has unsaved edits. */
45
+ prefabDirty: boolean;
46
+ /** Link shown in the top-left corner, outside prefab mode. */
47
+ backLink?: {href: string; label: string};
48
+ /** Undo is possible. */
49
+ canUndo: boolean;
50
+ /** Redo is possible. */
51
+ canRedo: boolean;
52
+ /** An effect is focused. */
53
+ hasFocus: boolean;
54
+ /** The stage gizmo's mode. */
55
+ gizmoMode: GizmoMode;
56
+ /** Emitter shape wireframes are shown. */
57
+ showWireframes: boolean;
58
+ /** Drag handlers of the viewport, for assets dropped from the drawer. */
59
+ dragHandlers: Pick<React.HTMLAttributes<HTMLDivElement>, 'onDragEnter' | 'onDragLeave' | 'onDragOver' | 'onDrop'>;
60
+ /** Undo (`false`) or redo. */
61
+ onHistoryStep(redo: boolean): void;
62
+ /** Pick the stage gizmo's mode. */
63
+ onGizmoMode(mode: GizmoMode): void;
64
+ /** Show or hide the emitter wireframes. */
65
+ onToggleWireframes(): void;
66
+ /** Back from the prefab to the scene. */
67
+ onExitPrefab(): void;
68
+ /** Save the open prefab. */
69
+ onSavePrefab(): void;
70
+ }
71
+
72
+ /** The 3D viewport and what floats over it: toolbars, transport, drag and import feedback. */
73
+ export function ViewportPane({
74
+ canvasRef,
75
+ fpsRef,
76
+ inset,
77
+ transport,
78
+ dragHover,
79
+ assetsLoading,
80
+ prefabInfo,
81
+ prefabDirty,
82
+ backLink,
83
+ canUndo,
84
+ canRedo,
85
+ hasFocus,
86
+ gizmoMode,
87
+ showWireframes,
88
+ dragHandlers,
89
+ onHistoryStep,
90
+ onGizmoMode,
91
+ onToggleWireframes,
92
+ onExitPrefab,
93
+ onSavePrefab,
94
+ }: ViewportPaneProps): React.ReactElement {
95
+ return (
96
+ <div style={{position: 'relative', flex: 1, minWidth: 0}} {...dragHandlers}>
97
+ <canvas
98
+ ref={canvasRef}
99
+ style={{width: '100%', height: '100%', display: 'block', touchAction: 'none', outline: 'none'}}
100
+ />
101
+ <div
102
+ style={{
103
+ ...floatingPanelStyle,
104
+ position: 'absolute',
105
+ bottom: 12 + inset,
106
+ left: 12,
107
+ padding: '4px 10px',
108
+ fontSize: 12,
109
+ color: theme.textDim,
110
+ fontVariantNumeric: 'tabular-nums',
111
+ pointerEvents: 'none',
112
+ fontFamily: theme.font,
113
+ }}
114
+ >
115
+ <span ref={fpsRef}>— FPS</span>
116
+ </div>
117
+ {transport && (
118
+ <div style={{position: 'absolute', bottom: 12 + inset, left: '50%', transform: 'translateX(-50%)'}}>
119
+ {transport}
120
+ </div>
121
+ )}
122
+ {dragHover && (
123
+ <div
124
+ style={{
125
+ position: 'absolute',
126
+ inset: 10,
127
+ border: '2px dashed rgba(120, 165, 255, 0.75)',
128
+ borderRadius: 12,
129
+ pointerEvents: 'none',
130
+ boxShadow: 'inset 0 0 0 1px rgba(120, 165, 255, 0.15)',
131
+ }}
132
+ >
133
+ <div
134
+ style={{
135
+ position: 'absolute',
136
+ left: '50%',
137
+ top: 18,
138
+ transform: 'translateX(-50%)',
139
+ background: 'rgba(7, 11, 22, 0.88)',
140
+ border: `1px solid ${theme.border}`,
141
+ borderRadius: 8,
142
+ padding: '8px 14px',
143
+ fontSize: 13,
144
+ color: theme.text,
145
+ whiteSpace: 'nowrap',
146
+ }}
147
+ >
148
+ Drop {dragHover.count === 1 ? 'effect' : `${dragHover.count} effects`} — {dragHover.label}
149
+ </div>
150
+ </div>
151
+ )}
152
+ {assetsLoading && (
153
+ <div
154
+ style={{
155
+ position: 'absolute',
156
+ inset: 0,
157
+ display: 'flex',
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ background: 'rgba(7, 11, 22, 0.72)',
161
+ pointerEvents: 'none',
162
+ }}
163
+ >
164
+ <div style={{...floatingPanelStyle, borderRadius: 10, padding: '16px 22px', textAlign: 'center'}}>
165
+ <div style={{fontWeight: 600, marginBottom: 6}}>Importing assets…</div>
166
+ <div style={{fontSize: 13, color: theme.textDim}}>
167
+ {assetsLoading.done} / {assetsLoading.total || '…'} files
168
+ {assetsLoading.failed > 0 ? ` · ${assetsLoading.failed} skipped` : ''}
169
+ </div>
170
+ </div>
171
+ </div>
172
+ )}
173
+ {prefabInfo && (
174
+ <div
175
+ style={{
176
+ ...floatingPanelStyle,
177
+ position: 'absolute',
178
+ top: 12,
179
+ left: '50%',
180
+ transform: 'translateX(-50%)',
181
+ display: 'flex',
182
+ alignItems: 'center',
183
+ gap: 8,
184
+ padding: '4px 4px 4px 6px',
185
+ border: `1px solid ${theme.borderActive}`,
186
+ fontSize: 12.5,
187
+ zIndex: 5,
188
+ }}
189
+ >
190
+ <button
191
+ type="button"
192
+ className="qe-hover"
193
+ title="Back to the scene"
194
+ style={{...buttonStyle, padding: '3px 10px'}}
195
+ onClick={onExitPrefab}
196
+ >
197
+ ← Scene
198
+ </button>
199
+ <span style={{color: theme.textDim}}>›</span>
200
+ <span style={{fontWeight: 600, whiteSpace: 'nowrap'}}>
201
+ {prefabInfo.name}
202
+ {prefabDirty ? ' *' : ''}
203
+ </span>
204
+ <button
205
+ type="button"
206
+ className="qe-hover"
207
+ title="Overwrite the prefab asset (Ctrl+S)"
208
+ style={saveButtonStyle(prefabDirty, '3px 12px')}
209
+ onClick={onSavePrefab}
210
+ >
211
+ Save
212
+ </button>
213
+ </div>
214
+ )}
215
+ {backLink && !prefabInfo && (
216
+ <a
217
+ className="qe-hover"
218
+ href={backLink.href}
219
+ style={{
220
+ ...floatingPanelStyle,
221
+ position: 'absolute',
222
+ top: 12,
223
+ left: 12,
224
+ display: 'inline-flex',
225
+ alignItems: 'center',
226
+ gap: 6,
227
+ padding: '6px 12px',
228
+ fontSize: 13,
229
+ fontWeight: 500,
230
+ color: theme.text,
231
+ textDecoration: 'none',
232
+ fontFamily: theme.font,
233
+ }}
234
+ >
235
+ <ArrowLeftIcon style={iconStyle(14)} />
236
+ {backLink.label.replace(/^←\s*/, '')}
237
+ </a>
238
+ )}
239
+ <div
240
+ style={{
241
+ position: 'absolute',
242
+ top: 12,
243
+ right: 12,
244
+ display: 'flex',
245
+ flexDirection: 'column',
246
+ alignItems: 'flex-end',
247
+ gap: 6,
248
+ }}
249
+ >
250
+ <div style={{...floatingPanelStyle, display: 'flex', gap: 4, padding: 4}}>
251
+ <button
252
+ className="qe-hover"
253
+ style={squareButtonStyle}
254
+ title="Undo (Ctrl+Z)"
255
+ disabled={!canUndo || !hasFocus}
256
+ onClick={() => onHistoryStep(false)}
257
+ >
258
+ <ArrowUturnLeftIcon style={iconStyle(14)} />
259
+ </button>
260
+ <button
261
+ className="qe-hover"
262
+ style={squareButtonStyle}
263
+ title="Redo (Ctrl+Shift+Z)"
264
+ disabled={!canRedo || !hasFocus}
265
+ onClick={() => onHistoryStep(true)}
266
+ >
267
+ <ArrowUturnRightIcon style={iconStyle(14)} />
268
+ </button>
269
+ </div>
270
+ <div style={{...floatingPanelStyle, display: 'flex', flexDirection: 'column', gap: 4, padding: 4}}>
271
+ {GIZMO_MODE_BUTTONS.map((m) => (
272
+ <button
273
+ key={m.value}
274
+ className="qe-hover"
275
+ style={{
276
+ ...squareButtonStyle,
277
+ ...(gizmoMode === m.value ? activeSquareButtonStyle : {}),
278
+ }}
279
+ title={m.title}
280
+ onClick={() => onGizmoMode(m.value)}
281
+ >
282
+ <m.Icon style={iconStyle(14)} />
283
+ </button>
284
+ ))}
285
+ </div>
286
+ <div style={{...floatingPanelStyle, display: 'flex', flexDirection: 'column', gap: 4, padding: 4}}>
287
+ <button
288
+ className="qe-hover"
289
+ style={{...squareButtonStyle, ...(showWireframes ? activeSquareButtonStyle : {})}}
290
+ title="Show emitter shape wireframes"
291
+ disabled={!hasFocus}
292
+ onClick={onToggleWireframes}
293
+ >
294
+ <CubeTransparentIcon style={iconStyle(14)} />
295
+ </button>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ }
@@ -0,0 +1,198 @@
1
+ import {Viewport} from '@babylonjs/core/Maths/math.viewport.js';
2
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
3
+ import {useEffect, useRef, useState} from 'react';
4
+ import type {EditorSession} from '../../core/editorSession.js';
5
+ import {notifyTransformChanged} from '../../core/transformStore.js';
6
+ import {DRAWER_HEIGHT} from './hostStyles.js';
7
+ import type {GizmoMode} from './ViewportPane.js';
8
+
9
+ /** True for a field that takes typed text, where editor shortcuts stay out of the way. */
10
+ export function isTextInput(target: EventTarget | null): boolean {
11
+ const el = target as HTMLElement | null;
12
+ return !!el && (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName));
13
+ }
14
+
15
+ /**
16
+ * Inspector value controls (numbers, checkboxes, selects): Ctrl+Z there means the effect's undo,
17
+ * not the browser's per-field text undo. Free-text inputs (names, filters) keep the native one.
18
+ */
19
+ export function isInspectorValueControl(target: EventTarget | null): boolean {
20
+ const el = target as HTMLElement | null;
21
+ if (!el) return false;
22
+ if (el.tagName === 'SELECT') return true;
23
+ return el.tagName === 'INPUT' && ['number', 'checkbox', 'range', 'color'].includes((el as HTMLInputElement).type);
24
+ }
25
+
26
+ /** What the editor's keyboard shortcuts do. */
27
+ export interface HostShortcuts {
28
+ /** Ctrl+S: save the scene, or ask about the open prefab. */
29
+ save(): void;
30
+ /** Ctrl+Space: open or close the Assets drawer. */
31
+ toggleAssets(): void;
32
+ /** Space: play or pause the stage. */
33
+ togglePlayback(): void;
34
+ /** Escape: close the floating Assets drawer. */
35
+ closeDrawer(): void;
36
+ /** Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z. */
37
+ historyStep(redo: boolean): void;
38
+ /** Delete: remove the focused effect; returns whether it did. */
39
+ deleteFocused(): boolean;
40
+ }
41
+
42
+ /** Handles a keydown on the editor's window; exported for tests. */
43
+ export function handleHostShortcut(e: KeyboardEvent, shortcuts: HostShortcuts): void {
44
+ const key = e.key.toLowerCase();
45
+ const command = e.ctrlKey || e.metaKey;
46
+ if (command && key === 's') {
47
+ e.preventDefault();
48
+ shortcuts.save();
49
+ return;
50
+ }
51
+ if (e.code === 'Space' && command) {
52
+ e.preventDefault();
53
+ shortcuts.toggleAssets();
54
+ return;
55
+ }
56
+ const target = e.target as HTMLElement | null;
57
+ if (e.code === 'Space' && !isTextInput(target) && target?.tagName !== 'BUTTON') {
58
+ e.preventDefault();
59
+ shortcuts.togglePlayback();
60
+ return;
61
+ }
62
+ if (e.key === 'Escape') {
63
+ shortcuts.closeDrawer();
64
+ }
65
+ if (command && (key === 'z' || key === 'y')) {
66
+ if (isTextInput(e.target) && !isInspectorValueControl(e.target)) {
67
+ return;
68
+ }
69
+ e.preventDefault();
70
+ // Leave the field first so it shows the restored value instead of its draft text.
71
+ (e.target as HTMLElement | null)?.blur?.();
72
+ shortcuts.historyStep(key === 'y' || e.shiftKey);
73
+ return;
74
+ }
75
+ if (e.key === 'Delete' && !isTextInput(e.target) && shortcuts.deleteFocused()) {
76
+ e.preventDefault();
77
+ }
78
+ }
79
+
80
+ /** Installs the editor's keyboard shortcuts on the window; the latest `shortcuts` are always used. */
81
+ export function useHostShortcuts(shortcuts: HostShortcuts): void {
82
+ const ref = useRef(shortcuts);
83
+ ref.current = shortcuts;
84
+ useEffect(() => {
85
+ const onKey = (e: KeyboardEvent) => handleHostShortcut(e, ref.current);
86
+ window.addEventListener('keydown', onKey);
87
+ return () => window.removeEventListener('keydown', onKey);
88
+ }, []);
89
+ }
90
+
91
+ /**
92
+ * Puts the stage gizmo in `mode` and attaches it to `target`, leaving it where it is while
93
+ * `paused`. One gizmo drag is one undo step: the binding snapshots at drag start, and the UI
94
+ * refreshes at most once a frame while dragging.
95
+ */
96
+ export function useStageGizmo(
97
+ session: EditorSession | null,
98
+ mode: GizmoMode,
99
+ target: TransformNode | null,
100
+ paused: boolean
101
+ ): void {
102
+ useEffect(() => {
103
+ const gm = session?.gizmoManager;
104
+ if (!gm) return;
105
+ gm.positionGizmoEnabled = mode === 'position';
106
+ gm.rotationGizmoEnabled = mode === 'rotation';
107
+ gm.scaleGizmoEnabled = mode === 'scale';
108
+ const activeGizmo =
109
+ mode === 'position'
110
+ ? gm.gizmos.positionGizmo
111
+ : mode === 'rotation'
112
+ ? gm.gizmos.rotationGizmo
113
+ : mode === 'scale'
114
+ ? gm.gizmos.scaleGizmo
115
+ : null;
116
+ let dragRaf = 0;
117
+ const scheduleTransformRefresh = () => {
118
+ if (dragRaf !== 0) {
119
+ return;
120
+ }
121
+ dragRaf = requestAnimationFrame(() => {
122
+ dragRaf = 0;
123
+ notifyTransformChanged();
124
+ });
125
+ };
126
+ const dragStartObserver = activeGizmo?.onDragStartObservable.add(() => session.binding?.onBeforeChange?.());
127
+ const dragObserver = activeGizmo?.onDragObservable.add(scheduleTransformRefresh);
128
+ const dragEndObserver = activeGizmo?.onDragEndObservable.add(() => {
129
+ notifyTransformChanged();
130
+ session.binding?.markChanged();
131
+ });
132
+ return () => {
133
+ if (dragRaf !== 0) {
134
+ cancelAnimationFrame(dragRaf);
135
+ }
136
+ if (activeGizmo) {
137
+ activeGizmo.onDragStartObservable.remove(dragStartObserver ?? null);
138
+ activeGizmo.onDragObservable.remove(dragObserver ?? null);
139
+ activeGizmo.onDragEndObservable.remove(dragEndObserver ?? null);
140
+ }
141
+ };
142
+ }, [session, mode]);
143
+
144
+ useEffect(() => {
145
+ if (!paused) {
146
+ session?.gizmoManager.attachToNode(target);
147
+ }
148
+ }, [session, target, paused]);
149
+ }
150
+
151
+ /**
152
+ * The floating drawer is opaque, so the scene renders only in the part of the viewport above it:
153
+ * the view, and every "frame this effect" camera move, re-centres on what is visible. Measured
154
+ * again when the drawer floats or docks and when the timeline collapses. Returns the pixels the
155
+ * drawer covers.
156
+ */
157
+ export function useViewportInset(
158
+ session: EditorSession | null,
159
+ canvasRef: React.RefObject<HTMLCanvasElement | null>,
160
+ areaRef: React.RefObject<HTMLDivElement | null>,
161
+ drawerFloating: boolean,
162
+ timelineCollapsed: boolean
163
+ ): number {
164
+ const [inset, setInset] = useState(0);
165
+ useEffect(() => {
166
+ const update = () => {
167
+ const camera = session?.camera;
168
+ const canvas = canvasRef.current;
169
+ const area = areaRef.current;
170
+ if (!camera || !canvas || !area) return;
171
+ const canvasRect = canvas.getBoundingClientRect();
172
+ const drawerTop = area.getBoundingClientRect().bottom - DRAWER_HEIGHT;
173
+ const covered = drawerFloating
174
+ ? Math.max(0, Math.min(canvasRect.height * 0.8, canvasRect.bottom - drawerTop))
175
+ : 0;
176
+ const fraction = canvasRect.height > 0 ? covered / canvasRect.height : 0;
177
+ camera.viewport = new Viewport(0, fraction, 1, 1 - fraction);
178
+ setInset(covered);
179
+ };
180
+ update();
181
+ window.addEventListener('resize', update);
182
+ return () => window.removeEventListener('resize', update);
183
+ }, [session, canvasRef, areaRef, drawerFloating, timelineCollapsed]);
184
+ return inset;
185
+ }
186
+
187
+ /** Asks before the page unloads while `dirty`. */
188
+ export function useUnsavedChangesGuard(dirty: boolean): void {
189
+ useEffect(() => {
190
+ if (!dirty) return;
191
+ const onBeforeUnload = (e: BeforeUnloadEvent) => {
192
+ e.preventDefault();
193
+ e.returnValue = '';
194
+ };
195
+ window.addEventListener('beforeunload', onBeforeUnload);
196
+ return () => window.removeEventListener('beforeunload', onBeforeUnload);
197
+ }, [dirty]);
198
+ }
@@ -0,0 +1,65 @@
1
+ import type React from 'react';
2
+ import {buttonStyle, theme} from '../theme.js';
3
+
4
+ /** Width of the Scene list on the left. */
5
+ export const SCENE_PANEL_WIDTH = 280;
6
+ /** Width of the inspector on the right. */
7
+ export const INSPECTOR_WIDTH = 340;
8
+ /** Height of the timeline dock when expanded. */
9
+ export const TIMELINE_HEIGHT = 200;
10
+ /** Height of the Assets drawer. */
11
+ export const DRAWER_HEIGHT = 280;
12
+
13
+ /** A square icon button of the viewport toolbars. */
14
+ export const squareButtonStyle: React.CSSProperties = {
15
+ ...buttonStyle,
16
+ width: 28,
17
+ height: 28,
18
+ padding: 0,
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ justifyContent: 'center',
22
+ fontWeight: 700,
23
+ };
24
+
25
+ /** Added to a toolbar button that is switched on. */
26
+ export const activeSquareButtonStyle: React.CSSProperties = {
27
+ border: `1px solid ${theme.borderActive}`,
28
+ background: 'rgba(120, 165, 255, 0.35)',
29
+ };
30
+
31
+ /** Title of a dock (Assets, Timeline). */
32
+ export const dockTitleStyle: React.CSSProperties = {
33
+ fontSize: 11,
34
+ fontWeight: 600,
35
+ letterSpacing: 0.4,
36
+ textTransform: 'uppercase',
37
+ color: theme.text,
38
+ };
39
+
40
+ /** A small icon button in a dock's header. */
41
+ export const dockIconButtonStyle: React.CSSProperties = {
42
+ ...buttonStyle,
43
+ width: 24,
44
+ height: 22,
45
+ padding: 0,
46
+ display: 'inline-flex',
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ };
50
+
51
+ /** A floating panel over the viewport. */
52
+ export const floatingPanelStyle: React.CSSProperties = {
53
+ background: theme.panelBg,
54
+ border: `1px solid ${theme.border}`,
55
+ borderRadius: 8,
56
+ };
57
+
58
+ /** A Save button, highlighted while there is something to save. */
59
+ export function saveButtonStyle(pending: boolean, padding: string): React.CSSProperties {
60
+ return {
61
+ ...buttonStyle,
62
+ padding,
63
+ ...(pending ? {background: theme.accent, color: '#0a1128', border: `1px solid ${theme.accent}`} : {}),
64
+ };
65
+ }
@@ -28,7 +28,7 @@ export type {EditorWorkspaceProps} from './EditorWorkspace.js';
28
28
  export {EffectEditor} from './EffectEditor.js';
29
29
  export type {EffectEditorProps} from './EffectEditor.js';
30
30
  export {EffectEditorHost, QuarksEffectEditor} from './EffectEditorHost.js';
31
- export type {EffectEditorHostHandle, EffectEditorHostProps} from './EffectEditorHost.js';
31
+ export type {EditorProject, EffectEditorHostHandle, EffectEditorHostProps} from './EffectEditorHost.js';
32
32
  export {EffectTreeView} from './EffectTreeView.js';
33
33
  export type {EffectTreeViewProps} from './EffectTreeView.js';
34
34
  export {CheckboxField, NumberField, Row, SelectField} from './fields.js';
@@ -45,6 +45,7 @@ export {
45
45
  } from './modules.js';
46
46
  export type {GeometryData, GeometryOption, TextureOption} from './modules.js';
47
47
  export {ModuleSection} from './ModuleSection.js';
48
+ export type {ModuleProps} from './ModuleSection.js';
48
49
  export {ProjectsPage} from './ProjectsPage.js';
49
50
  export type {ProjectsPageProps} from './ProjectsPage.js';
50
51
  export {PromptDialog} from './PromptDialog.js';
@@ -62,3 +63,4 @@ export type {TimelineTrackRowProps} from './TimelineTrackRow.js';
62
63
  export {TransportBar} from './TransportBar.js';
63
64
  export type {TransportBarProps} from './TransportBar.js';
64
65
  export {ValueField} from './ValueField.js';
66
+ export type {ConstantValueFieldProps, CurveValueFieldProps} from './ValueField.js';