babylon.quarks-editor 0.27.0 → 0.29.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 (149) hide show
  1. package/CHANGELOG.md +92 -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 +3 -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/emitterShapeWireframe.js +11 -4
  18. package/dist/core/galleryPlayback.js +2 -1
  19. package/dist/core/history.d.ts +6 -0
  20. package/dist/core/history.js +4 -0
  21. package/dist/core/indexedDbProjectStore.d.ts +13 -0
  22. package/dist/core/indexedDbProjectStore.js +13 -0
  23. package/dist/core/loadEffect.js +2 -0
  24. package/dist/core/material.d.ts +12 -2
  25. package/dist/core/playback.d.ts +6 -0
  26. package/dist/core/projectStore.d.ts +26 -0
  27. package/dist/core/projectStore.js +1 -0
  28. package/dist/core/sceneEntries.d.ts +3 -0
  29. package/dist/core/scrub.d.ts +1 -1
  30. package/dist/core/scrub.js +2 -2
  31. package/dist/core/shapes.d.ts +31 -1
  32. package/dist/core/shapes.js +18 -3
  33. package/dist/core/systems.d.ts +2 -0
  34. package/dist/core/systems.js +5 -1
  35. package/dist/core/timeline.d.ts +23 -3
  36. package/dist/core/values.d.ts +42 -8
  37. package/dist/core/values.js +52 -9
  38. package/dist/index.d.ts +5 -1
  39. package/dist/react/AssetsPanel.d.ts +7 -0
  40. package/dist/react/AxesCurveField.d.ts +21 -0
  41. package/dist/react/AxesCurveField.js +25 -0
  42. package/dist/react/ChoiceDialog.d.ts +7 -0
  43. package/dist/react/ContextMenu.d.ts +10 -1
  44. package/dist/react/EditorWorkspace.d.ts +7 -0
  45. package/dist/react/EditorWorkspace.js +6 -3
  46. package/dist/react/EffectEditor.d.ts +8 -2
  47. package/dist/react/EffectEditorHost.d.ts +32 -28
  48. package/dist/react/EffectEditorHost.js +241 -1665
  49. package/dist/react/EffectTreeView.d.ts +3 -0
  50. package/dist/react/LifeCurveField.d.ts +18 -0
  51. package/dist/react/LifeCurveField.js +26 -0
  52. package/dist/react/MessageDialog.d.ts +5 -0
  53. package/dist/react/ModuleSection.d.ts +10 -0
  54. package/dist/react/ModuleSection.js +4 -1
  55. package/dist/react/ProjectsPage.d.ts +6 -0
  56. package/dist/react/ProjectsPage.js +27 -11
  57. package/dist/react/PromptDialog.d.ts +7 -0
  58. package/dist/react/ScenePanel.d.ts +7 -0
  59. package/dist/react/ScenePanel.js +7 -5
  60. package/dist/react/TextureSelect.d.ts +5 -5
  61. package/dist/react/TimelinePanel.d.ts +13 -0
  62. package/dist/react/TimelinePanel.js +10 -6
  63. package/dist/react/TimelineTrackRow.d.ts +10 -0
  64. package/dist/react/TransportBar.d.ts +9 -0
  65. package/dist/react/ValueField.d.ts +33 -5
  66. package/dist/react/ValueField.js +34 -9
  67. package/dist/react/behaviorModules.d.ts +16 -5
  68. package/dist/react/behaviorModules.js +28 -9
  69. package/dist/react/fields.d.ts +8 -0
  70. package/dist/react/fields.js +3 -0
  71. package/dist/react/host/BottomDocks.d.ts +36 -0
  72. package/dist/react/host/BottomDocks.js +63 -0
  73. package/dist/react/host/HostDialogs.d.ts +50 -0
  74. package/dist/react/host/HostDialogs.js +113 -0
  75. package/dist/react/host/InspectorSidebar.d.ts +32 -0
  76. package/dist/react/host/InspectorSidebar.js +59 -0
  77. package/dist/react/host/SceneSidebar.d.ts +53 -0
  78. package/dist/react/host/SceneSidebar.js +62 -0
  79. package/dist/react/host/ViewportPane.d.ts +56 -0
  80. package/dist/react/host/ViewportPane.js +91 -0
  81. package/dist/react/host/hostHooks.d.ts +44 -0
  82. package/dist/react/host/hostHooks.js +164 -0
  83. package/dist/react/host/hostStyles.d.ts +21 -0
  84. package/dist/react/host/hostStyles.js +57 -0
  85. package/dist/react/index.d.ts +3 -1
  86. package/dist/react/inspectorHints.js +1 -1
  87. package/dist/react/modules.d.ts +17 -6
  88. package/dist/react/modules.js +77 -34
  89. package/dist/react/theme.d.ts +15 -0
  90. package/dist/react/theme.js +15 -0
  91. package/dist/session.d.ts +9 -0
  92. package/dist/session.js +8 -0
  93. package/package.json +17 -11
  94. package/src/core/assetTree.ts +14 -1
  95. package/src/core/assets.ts +25 -10
  96. package/src/core/binding.ts +9 -0
  97. package/src/core/collision.ts +4 -0
  98. package/src/core/colors.ts +21 -1
  99. package/src/core/defaultEffect.ts +6 -3
  100. package/src/core/editorSession.ts +1299 -0
  101. package/src/core/effectTree.ts +5 -0
  102. package/src/core/emitterShapeWireframe.ts +12 -4
  103. package/src/core/galleryPlayback.ts +2 -1
  104. package/src/core/history.ts +6 -0
  105. package/src/core/indexedDbProjectStore.ts +13 -0
  106. package/src/core/loadEffect.ts +3 -1
  107. package/src/core/material.ts +14 -4
  108. package/src/core/playback.ts +6 -0
  109. package/src/core/projectStore.ts +26 -0
  110. package/src/core/sceneEntries.ts +3 -0
  111. package/src/core/scrub.ts +2 -2
  112. package/src/core/shapes.ts +48 -3
  113. package/src/core/systems.ts +11 -2
  114. package/src/core/timeline.ts +40 -15
  115. package/src/core/values.ts +91 -14
  116. package/src/index.ts +5 -1
  117. package/src/react/AssetsPanel.tsx +7 -0
  118. package/src/react/AxesCurveField.tsx +72 -0
  119. package/src/react/ChoiceDialog.tsx +7 -0
  120. package/src/react/ContextMenu.tsx +10 -1
  121. package/src/react/EditorWorkspace.tsx +11 -5
  122. package/src/react/EffectEditor.tsx +7 -1
  123. package/src/react/EffectEditorHost.tsx +406 -2340
  124. package/src/react/EffectTreeView.tsx +3 -0
  125. package/src/react/LifeCurveField.tsx +57 -0
  126. package/src/react/MessageDialog.tsx +5 -0
  127. package/src/react/ModuleSection.tsx +11 -0
  128. package/src/react/ProjectsPage.tsx +38 -11
  129. package/src/react/PromptDialog.tsx +7 -0
  130. package/src/react/ScenePanel.tsx +14 -5
  131. package/src/react/TextureSelect.tsx +6 -6
  132. package/src/react/TimelinePanel.tsx +23 -6
  133. package/src/react/TimelineTrackRow.tsx +10 -0
  134. package/src/react/TransportBar.tsx +9 -0
  135. package/src/react/ValueField.tsx +99 -15
  136. package/src/react/behaviorModules.tsx +58 -42
  137. package/src/react/fields.tsx +12 -1
  138. package/src/react/host/BottomDocks.tsx +178 -0
  139. package/src/react/host/HostDialogs.tsx +228 -0
  140. package/src/react/host/InspectorSidebar.tsx +147 -0
  141. package/src/react/host/SceneSidebar.tsx +227 -0
  142. package/src/react/host/ViewportPane.tsx +300 -0
  143. package/src/react/host/hostHooks.ts +198 -0
  144. package/src/react/host/hostStyles.ts +65 -0
  145. package/src/react/index.ts +3 -1
  146. package/src/react/inspectorHints.ts +1 -1
  147. package/src/react/modules.tsx +192 -71
  148. package/src/react/theme.ts +15 -0
  149. package/src/session.ts +17 -0
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
3
- import { ApplyCollision, ApplyForce, ApplySequences, AxisAngleGenerator, Bezier, ChangeEmitDirection, ColorBySpeed, ConstantValue, EulerGenerator, ForceOverLife, FrameOverLife, Gradient, GravityForce, InheritVelocity, IntervalValue, LimitSpeedOverLife, Noise, OrbitOverLife, PiecewiseBezier, RandomQuatGenerator, Rotation3DOverLife, RotationBySpeed, RotationOverLife, SizeBySpeed, SpeedOverLife, TextureSequencer, TurbulenceField, Vector3, VelocityOverLife, WidthOverLength, } from 'babylon.quarks';
3
+ import { ApplyCollision, ApplyForce, ApplySequences, AxisAngleGenerator, Bezier, ChangeEmitDirection, ColorBySpeed, ConstantValue, EulerGenerator, ForceOverLife, FrameOverLife, Gradient, GravityForce, InheritVelocity, IntervalValue, LimitSpeedOverLife, Noise, OrbitOverLife, PiecewiseBezier, RandomQuatGenerator, Rotation3DOverLife, RotationBySpeed, RotationOverLife, SizeBySpeed, SpeedOverLife, TextureSequencer, TurbulenceField, Vector3, Vector3Function, VelocityOverLife, WidthOverLength, } from 'babylon.quarks';
4
4
  import React from 'react';
5
5
  import { buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops } from '../core/colors.js';
6
- import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values.js';
7
- import { CurveEditor } from './CurveEditor.js';
6
+ import { buildScalar, readScalar } from '../core/values.js';
7
+ import { AxesCurveField } from './AxesCurveField.js';
8
8
  import { CheckboxField, NumberField, Row, SelectField } from './fields.js';
9
9
  import { GradientEditor } from './GradientEditor.js';
10
10
  import { iconStyle } from './icons.js';
11
+ import { LifeCurveField } from './LifeCurveField.js';
11
12
  import { ModuleSection } from './ModuleSection.js';
12
13
  import { theme } from './theme.js';
13
14
  import { ValueField } from './ValueField.js';
@@ -28,12 +29,18 @@ function BehaviorModule(props) {
28
29
  }
29
30
  }), children: behavior ? props.children(behavior) : null }));
30
31
  }
32
+ /** The Speed over Lifetime module (Unity's Speed Modifier): scales speed by a curve over the particle's life. */
31
33
  export function SpeedOverLifeModule({ binding }) {
32
- return (_jsx(BehaviorModule, { binding: binding, title: "Speed over Lifetime", type: "SpeedOverLife", create: () => new SpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0] })), children: (behavior) => (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.speed), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.speed = buildCurve(pieces))) }) })) }));
34
+ return (_jsx(BehaviorModule, { binding: binding, title: "Speed over Lifetime", type: "SpeedOverLife", create: () => new SpeedOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.5, 0] })), children: (behavior) => (_jsx("div", { style: { marginTop: 6 }, children: _jsx(LifeCurveField, { generator: behavior.speed, maxValue: 2, onChange: (curve) => binding.apply(() => (behavior.speed = curve)) }) })) }));
33
35
  }
36
+ /** The Limit Speed over Lifetime module (Unity's Limit Velocity over Lifetime): caps speed by a curve, with dampening. */
34
37
  export function LimitSpeedOverLifeModule({ binding }) {
35
- 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))) }) })] })) }));
38
+ 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(AxesCurveField, { generator: behavior.speed, maxValue: 8, onChange: (speed) => binding.apply(() => (behavior.speed = speed)) }) }), behavior.speed instanceof Vector3Function && (_jsx(Row, { label: "Space", hint: "The axes each limit is measured along: the emitter's or the world's.", children: _jsx(SelectField, { value: behavior.space, options: [
39
+ { value: 'local', label: 'Local' },
40
+ { value: 'world', label: 'World' },
41
+ ], onChange: (v) => binding.apply(() => (behavior.space = v)) }) })), _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))) }) })] })) }));
36
42
  }
43
+ /** The Force over Lifetime module (Unity's Force over Lifetime): a constant push per axis. */
37
44
  export function ForceOverLifeModule({ binding }) {
38
45
  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(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
39
46
  { value: 'world', label: 'World' },
@@ -56,12 +63,14 @@ export function InheritVelocityModule({ binding }) {
56
63
  export function AttractorModule({ binding }) {
57
64
  return (_jsx(BehaviorModule, { binding: binding, title: "Attractor", type: "GravityForce", create: () => new GravityForce(new Vector3(0, 0, 0), 1), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Center", hintKey: "attractor.center", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: ['x', 'y', 'z'].map((axis) => (_jsx(NumberField, { value: behavior.center[axis], step: 0.1, onChange: (v) => binding.apply(() => (behavior.center[axis] = v)) }, axis))) }) }), _jsx(Row, { label: "Strength", hintKey: "attractor.magnitude", children: _jsx(NumberField, { value: behavior.magnitude, step: 0.1, onChange: (v) => binding.apply(() => (behavior.magnitude = v)) }) })] })) }));
58
65
  }
66
+ /** The Gravity module: pulls particles towards a point. */
59
67
  export function GravityModule({ binding }) {
60
68
  return (_jsx(BehaviorModule, { binding: binding, title: "Gravity", type: "ApplyForce", create: () => new ApplyForce(new Vector3(0, -1, 0), new ConstantValue(9.81)), children: (behavior) => (_jsxs(_Fragment, { children: [_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))) }) }), _jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
61
69
  { value: 'world', label: 'World' },
62
70
  { value: 'local', label: 'Local' },
63
71
  ], onChange: (v) => binding.apply(() => (behavior.space = v)) }) })] })) }));
64
72
  }
73
+ /** The Rotation over Lifetime module (Unity's Rotation over Lifetime): spin rate over the particle's life. */
65
74
  export function RotationOverLifeModule({ binding }) {
66
75
  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)) })) }));
67
76
  }
@@ -137,6 +146,7 @@ export function SequencesModule({ binding, textureOptions }) {
137
146
  }, title: "Remove sequence", onClick: () => binding.apply(() => behavior.sequencers.splice(index, 1)), children: _jsx(XMarkIcon, { style: iconStyle(13) }) })] }), _jsx(Row, { label: "Time from / to (s)", children: pair(range.a, range.b, (v) => binding.apply(() => (range.a = v)), (v) => binding.apply(() => (range.b = v)), 0.1) }), texture.locations !== undefined && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Scale X / Y", children: pair(texture.scaleX, texture.scaleY, (v) => binding.apply(() => (texture.scaleX = v)), (v) => binding.apply(() => (texture.scaleY = v)), 0.01) }), _jsx(Vector3Row, { label: "Position", value: texture.position, step: 0.1, onChange: (v) => binding.apply(() => texture.position.copy(v)) })] }))] }, index));
138
147
  }), _jsx(Row, { label: "Image", children: textureOptions && textureOptions.length > 0 ? (_jsx(SelectField, { value: imageUrl, options: textureOptions.map((option) => ({ value: option.url, label: option.label })), onChange: (url) => setImageUrl(url) })) : (_jsx("input", { className: "qe-hover", style: { flex: 1, minWidth: 0, fontSize: 12 }, placeholder: "Image URL", value: imageUrl, onChange: (event) => setImageUrl(event.target.value) })) }), _jsx(Row, { label: "Alpha threshold", children: _jsx(NumberField, { value: threshold, min: 0, step: 0.05, onChange: (v) => setThreshold(Math.min(1, v)) }) }), _jsxs("button", { className: "qe-hover", style: { display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, fontSize: 12 }, onClick: () => void addFromImage(behavior), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add sequence from image"] }), status && _jsx("p", { style: { margin: '4px 0 0', color: theme.textDim, fontSize: 11.5 }, children: status })] })) }));
139
148
  }
149
+ /** The Noise module (Unity's Noise): moves and turns particles by a noise field. */
140
150
  export function NoiseModule({ binding }) {
141
151
  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)) }), _jsx(ValueField, { label: "Scroll speed", generator: behavior.scrollSpeed, curveMax: 5, onChange: (g) => binding.apply(() => (behavior.scrollSpeed = g)) }), _jsx(Row, { label: "Remap", children: _jsx(CheckboxField, { value: behavior.remap !== undefined, onChange: (v) => binding.apply(() => (behavior.remap = v
142
152
  ? new PiecewiseBezier([[new Bezier(-1, -1 / 3, 1 / 3, 1), 0]])
@@ -151,6 +161,7 @@ function Vector3Row(props) {
151
161
  };
152
162
  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) })] }) }));
153
163
  }
164
+ /** The Turbulence module: a curl noise force field. */
154
165
  export function TurbulenceModule({ binding }) {
155
166
  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)) }), _jsx(Row, { label: "Seed", children: _jsx(NumberField, { value: behavior.seed, step: 1, onChange: (v) => binding.apply(() => {
156
167
  const seed = Math.round(v);
@@ -161,9 +172,11 @@ export function TurbulenceModule({ binding }) {
161
172
  behavior.timeOffset.copy(next.initialOffset);
162
173
  }) }) })] })) }));
163
174
  }
175
+ /** The Randomize Direction module: tilts each particle's start direction by a random angle. */
164
176
  export function EmitDirectionModule({ binding }) {
165
- 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)) })) }));
177
+ 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, curve: false, min: 0, onChange: (g) => binding.apply(() => (behavior.angle = g)) })) }));
166
178
  }
179
+ /** The Collision module (Unity's Collision): particles bounce off or die on a ground plane. */
167
180
  export function CollisionModule({ binding }) {
168
181
  // Like Unity: Collision stores bounce only. Colliders are host-provided via
169
182
  // setPhysicsResolver, asked when a particle is tested — the editor never invents a ground plane.
@@ -173,28 +186,34 @@ export function CollisionModule({ binding }) {
173
186
  function SpeedRangeRows({ binding, behavior }) {
174
187
  return (_jsx(Row, { label: "Speed range", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: behavior.speedRange.a, min: 0, onChange: (v) => binding.apply(() => (behavior.speedRange = new IntervalValue(v, behavior.speedRange.b))) }), _jsx(NumberField, { value: behavior.speedRange.b, min: 0, onChange: (v) => binding.apply(() => (behavior.speedRange = new IntervalValue(behavior.speedRange.a, v))) })] }) }));
175
188
  }
189
+ /** The Color by Speed module (Unity's Color by Speed): a gradient over a speed range. */
176
190
  export function ColorBySpeedModule({ binding }) {
177
191
  return (_jsx(BehaviorModule, { binding: binding, title: "Color by Speed", type: "ColorBySpeed", create: () => new ColorBySpeed(buildGradient(DEFAULT_GRADIENT_STOPS), new IntervalValue(0, 5)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(GradientEditor, { stops: behavior.color instanceof Gradient
178
192
  ? readGradientStops(behavior.color)
179
193
  : DEFAULT_GRADIENT_STOPS, onChange: (stops) => binding.apply(() => (behavior.color = buildGradient(stops))) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
180
194
  }
195
+ /** The Size by Speed module (Unity's Size by Speed): size over a speed range. */
181
196
  export function SizeBySpeedModule({ binding }) {
182
- 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 })] })) }));
197
+ 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(AxesCurveField, { generator: behavior.size, maxValue: 2, onChange: (size) => binding.apply(() => (behavior.size = size)) }) }), _jsx(SpeedRangeRows, { binding: binding, behavior: behavior })] })) }));
183
198
  }
199
+ /** The Rotation by Speed module (Unity's Rotation by Speed): spin rate over a speed range. */
184
200
  export function RotationBySpeedModule({ binding }) {
185
201
  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 })] })) }));
186
202
  }
203
+ /** The Orbit over Lifetime module (Unity's Velocity over Lifetime orbital): turns particles about an axis. */
187
204
  export function OrbitOverLifeModule({ binding }) {
188
205
  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)) })) }));
189
206
  }
207
+ /** The Width over Trail module (Unity's Trails width over trail): a trail's width along its length. */
190
208
  export function WidthOverTrailModule({ binding }) {
191
- return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(behavior.width), maxValue: 2, onChange: (pieces) => binding.apply(() => (behavior.width = buildCurve(pieces))) }) }), _jsx(Row, { label: "Absolute width", children: _jsx(CheckboxField, { value: behavior.absolute, onChange: (v) => binding.apply(() => (behavior.absolute = v)) }) })] })) }));
209
+ return (_jsx(BehaviorModule, { binding: binding, title: "Width over Trail", type: "WidthOverLength", create: () => new WidthOverLength(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve: [1, 0.75, 0.4, 0] })), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("div", { style: { marginTop: 6 }, children: _jsx(LifeCurveField, { generator: behavior.width, maxValue: 2, onChange: (curve) => binding.apply(() => (behavior.width = curve)) }) }), _jsx(Row, { label: "Absolute width", children: _jsx(CheckboxField, { value: behavior.absolute, onChange: (v) => binding.apply(() => (behavior.absolute = v)) }) })] })) }));
192
210
  }
193
211
  /** How many of the first frames a start-frame generator picks from: its exclusive upper bound. */
194
212
  function startFrameCount(startFrame) {
195
213
  const state = readScalar(startFrame);
196
214
  return Math.round(state.mode === 'random' ? state.max : state.value);
197
215
  }
216
+ /** The Texture Sheet Animation module (Unity's Texture Sheet Animation): plays the sheet's frames by life or by frame rate. */
198
217
  export function TextureSheetModule({ binding }) {
199
218
  const system = binding.system;
200
219
  const frameBehavior = findBehavior(system.behaviors, 'FrameOverLife');
@@ -261,5 +280,5 @@ export function TextureSheetModule({ binding }) {
261
280
  if (v && frameBehavior.frameCount <= 0) {
262
281
  frameBehavior.frameCount = tiles;
263
282
  }
264
- }) }), frameBehavior.startFrame && (_jsx(NumberField, { value: startFrameCount(frameBehavior.startFrame), min: 1, max: tiles, step: 1, onChange: (v) => binding.apply(() => (frameBehavior.startFrame = new IntervalValue(0, Math.max(1, Math.round(v))))) }))] }), _jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(frameBehavior.frame), maxValue: Math.max(1, tiles - 1), onChange: (pieces) => binding.apply(() => (frameBehavior.frame = buildCurve(pieces))) }) })] }))] }));
283
+ }) }), frameBehavior.startFrame && (_jsx(NumberField, { value: startFrameCount(frameBehavior.startFrame), min: 1, max: tiles, step: 1, onChange: (v) => binding.apply(() => (frameBehavior.startFrame = new IntervalValue(0, Math.max(1, Math.round(v))))) }))] }), _jsx("div", { style: { marginTop: 6 }, children: _jsx(LifeCurveField, { generator: frameBehavior.frame, maxValue: Math.max(1, tiles - 1), onChange: (curve) => binding.apply(() => (frameBehavior.frame = curve)) }) })] }))] }));
265
284
  }
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ /** An inspector row: a label, with an optional hint, and its controls. */
2
3
  export declare function Row(props: {
3
4
  label: string;
4
5
  hint?: string;
@@ -13,15 +14,22 @@ export declare function NumberField(props: {
13
14
  min?: number;
14
15
  max?: number;
15
16
  }): React.JSX.Element;
17
+ /** A checkbox bound to a boolean. */
16
18
  export declare function CheckboxField(props: {
17
19
  value: boolean;
18
20
  onChange: (value: boolean) => void;
19
21
  }): React.JSX.Element;
22
+ /** A select bound to a value, from a list of options. */
20
23
  export declare function SelectField<T extends string | number>(props: {
24
+ /** The selected value. */
21
25
  value: T;
26
+ /** The options, in menu order. */
22
27
  options: Array<{
28
+ /** The option's value. */
23
29
  value: T;
30
+ /** Its text. */
24
31
  label: string;
25
32
  }>;
33
+ /** Receives the picked value, a number when the values are numbers. */
26
34
  onChange: (value: T) => void;
27
35
  }): React.JSX.Element;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState } from 'react';
3
3
  import { InspectorLabel } from './InspectorLabel.js';
4
4
  import { inputStyle, rowStyle } from './theme.js';
5
+ /** An inspector row: a label, with an optional hint, and its controls. */
5
6
  export function Row(props) {
6
7
  return (_jsxs("div", { style: rowStyle, children: [_jsx(InspectorLabel, { label: props.label, hint: props.hint, hintKey: props.hintKey }), props.children] }));
7
8
  }
@@ -27,9 +28,11 @@ export function NumberField(props) {
27
28
  }
28
29
  } }));
29
30
  }
31
+ /** A checkbox bound to a boolean. */
30
32
  export function CheckboxField(props) {
31
33
  return (_jsx("input", { type: "checkbox", className: "qe-hover", checked: props.value, onChange: (e) => props.onChange(e.target.checked), style: { justifySelf: 'start', width: 15, height: 15, accentColor: '#78a5ff', cursor: 'pointer' } }));
32
34
  }
35
+ /** A select bound to a value, from a list of options. */
33
36
  export function SelectField(props) {
34
37
  const numeric = typeof props.value === 'number';
35
38
  return (_jsx("select", { className: "qe-hover", style: { ...inputStyle, cursor: 'pointer' }, value: String(props.value), onChange: (e) => props.onChange((numeric ? Number(e.target.value) : e.target.value)), children: props.options.map((o) => (_jsx("option", { value: String(o.value), children: o.label }, String(o.value)))) }));
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { type AssetsPanelProps } from '../AssetsPanel.js';
3
+ /** Props of {@link AssetsDrawer}. */
4
+ export interface AssetsDrawerProps {
5
+ /** The drawer is shown. */
6
+ open: boolean;
7
+ /** The drawer is docked above the timeline instead of floating over the viewport. */
8
+ pinned: boolean;
9
+ /** Pin or unpin it (it stays open). */
10
+ onTogglePin(): void;
11
+ /** Close and unpin it. */
12
+ onClose(): void;
13
+ /** The Assets browser inside. */
14
+ panel: AssetsPanelProps;
15
+ }
16
+ /** The Assets browser, floating over the viewport or pinned above the timeline. */
17
+ export declare function AssetsDrawer(props: AssetsDrawerProps): React.ReactElement;
18
+ /** Props of {@link TimelineDock}. */
19
+ export interface TimelineDockProps {
20
+ /** The timeline is collapsed to its header. */
21
+ collapsed: boolean;
22
+ /** Collapse or expand it. */
23
+ onToggleCollapsed(): void;
24
+ /** Name of the focused effect, if any. */
25
+ effectName?: string;
26
+ /** Assets in the project, counted on the Assets button. */
27
+ assetCount: number;
28
+ /** The Assets button shows as switched on. */
29
+ assetsActive: boolean;
30
+ /** The Assets button. */
31
+ onToggleAssets(): void;
32
+ /** The timeline, or `null` when nothing is focused. */
33
+ children: React.ReactNode;
34
+ }
35
+ /** The bottom dock: the focused effect's timeline, and the Assets button. */
36
+ export declare function TimelineDock(props: TimelineDockProps): React.ReactElement;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDownIcon, ChevronUpIcon, MapPinIcon, XMarkIcon } from '@heroicons/react/24/solid';
3
+ import { AssetsPanel } from '../AssetsPanel.js';
4
+ import { iconStyle } from '../icons.js';
5
+ import { buttonStyle, theme } from '../theme.js';
6
+ import { activeSquareButtonStyle, dockIconButtonStyle, dockTitleStyle, DRAWER_HEIGHT, TIMELINE_HEIGHT, } from './hostStyles.js';
7
+ /** The Assets browser, floating over the viewport or pinned above the timeline. */
8
+ export function AssetsDrawer(props) {
9
+ const { open, pinned } = props;
10
+ return (_jsxs("div", { "aria-hidden": !open && !pinned, style: {
11
+ height: DRAWER_HEIGHT,
12
+ display: 'flex',
13
+ flexDirection: 'column',
14
+ background: theme.panelBg,
15
+ borderTop: `1px solid ${theme.border}`,
16
+ ...(pinned
17
+ ? { flexShrink: 0 }
18
+ : {
19
+ position: 'absolute',
20
+ left: 0,
21
+ right: 0,
22
+ bottom: 0,
23
+ zIndex: 20,
24
+ background: '#0a0f20',
25
+ boxShadow: '0 -10px 30px rgba(0, 0, 0, 0.5)',
26
+ transform: open ? 'none' : 'translateY(100%)',
27
+ opacity: open ? 1 : 0,
28
+ pointerEvents: open ? 'auto' : 'none',
29
+ transition: 'transform 0.18s ease, opacity 0.18s ease',
30
+ }),
31
+ }, children: [_jsxs("div", { style: {
32
+ display: 'flex',
33
+ alignItems: 'center',
34
+ gap: 6,
35
+ padding: '4px 6px 4px 12px',
36
+ borderBottom: `1px solid ${theme.border}`,
37
+ }, children: [_jsx("span", { style: dockTitleStyle, children: "Assets" }), _jsx("span", { style: { fontSize: 11, color: theme.textDim }, children: "Ctrl+Space" }), _jsx("span", { style: { flex: 1 } }), _jsx("button", { type: "button", className: "qe-hover", title: pinned ? 'Unpin — float over the viewport again' : 'Pin — keep it docked above the timeline', style: { ...dockIconButtonStyle, ...(pinned ? activeSquareButtonStyle : {}) }, onClick: props.onTogglePin, children: _jsx(MapPinIcon, { style: iconStyle(13) }) }), _jsx("button", { type: "button", className: "qe-hover", title: "Close (Esc)", style: dockIconButtonStyle, onClick: props.onClose, children: _jsx(XMarkIcon, { style: iconStyle(13) }) })] }), _jsx(AssetsPanel, { ...props.panel })] }));
38
+ }
39
+ /** The bottom dock: the focused effect's timeline, and the Assets button. */
40
+ export function TimelineDock(props) {
41
+ const { collapsed } = props;
42
+ return (_jsxs("div", { style: {
43
+ flexShrink: 0,
44
+ borderTop: `1px solid ${theme.border}`,
45
+ background: theme.panelBg,
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '3px 6px' }, children: [_jsx("button", { type: "button", className: "qe-hover-bg", title: collapsed ? 'Expand timeline' : 'Collapse timeline', style: { ...dockIconButtonStyle, border: 'none', background: 'transparent' }, onClick: props.onToggleCollapsed, children: collapsed ? _jsx(ChevronUpIcon, { style: iconStyle(13) }) : _jsx(ChevronDownIcon, { style: iconStyle(13) }) }), _jsx("span", { style: dockTitleStyle, children: "Timeline" }), props.effectName !== undefined && (_jsx("span", { style: {
49
+ fontSize: 12,
50
+ color: theme.textDim,
51
+ overflow: 'hidden',
52
+ textOverflow: 'ellipsis',
53
+ whiteSpace: 'nowrap',
54
+ }, children: props.effectName })), _jsx("span", { style: { flex: 1 } }), _jsxs("button", { type: "button", className: "qe-hover", title: "Assets (Ctrl+Space)", style: {
55
+ ...buttonStyle,
56
+ padding: '3px 10px',
57
+ ...(props.assetsActive ? activeSquareButtonStyle : {}),
58
+ }, onClick: props.onToggleAssets, children: ["Assets", props.assetCount > 0 ? ` · ${props.assetCount}` : ''] })] }), _jsx("div", { style: {
59
+ height: TIMELINE_HEIGHT,
60
+ display: collapsed ? 'none' : 'flex',
61
+ borderTop: `1px solid ${theme.border}`,
62
+ }, children: props.children ?? (_jsx("div", { style: { padding: '14px', fontSize: 12, color: theme.textDim }, children: "Select an effect in the Scene to see its timeline." })) })] }));
63
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import type { EditorSession, PrefabInfo } from '../../core/editorSession.js';
3
+ import type { SceneEntry } from '../../core/sceneEntries.js';
4
+ /** Which prefab question is open: overwrite on save, save before leaving, or a new asset path. */
5
+ export type PrefabDialog = 'overwrite' | 'exit' | 'saveAs' | null;
6
+ /** The host's dialog and notice state, with its setters. */
7
+ export interface HostDialogState {
8
+ /** Name of the project being opened, while it loads. */
9
+ loadingProject: string | null;
10
+ /** The short notice shown, if any. */
11
+ toast: string | null;
12
+ /** Entry being saved as a new asset. */
13
+ saveAsTarget: SceneEntry | null;
14
+ setSaveAsTarget(entry: SceneEntry | null): void;
15
+ /** The open prefab, if any. */
16
+ prefabInfo: PrefabInfo | null;
17
+ /** The open prefab question. */
18
+ prefabDialog: PrefabDialog;
19
+ setPrefabDialog(dialog: PrefabDialog): void;
20
+ /** Asset waiting to replace an open prefab that has unsaved edits. */
21
+ prefabSwitch: {
22
+ id: number;
23
+ name: string;
24
+ } | null;
25
+ setPrefabSwitch(asset: {
26
+ id: number;
27
+ name: string;
28
+ } | null): void;
29
+ /** "Save changes to the scene?" before leaving the project. */
30
+ exitPrompt: boolean;
31
+ setExitPrompt(open: boolean): void;
32
+ /** The notice the user dismisses, if any. */
33
+ message: {
34
+ title: string;
35
+ message: string;
36
+ } | null;
37
+ setMessage(message: {
38
+ title: string;
39
+ message: string;
40
+ } | null): void;
41
+ }
42
+ /** Props of {@link HostDialogs}. */
43
+ export interface HostDialogsProps extends HostDialogState {
44
+ /** The session the dialogs act on. */
45
+ session: EditorSession | null;
46
+ /** Leaves the project, once the scene is dealt with. */
47
+ onExitProject?: () => void;
48
+ }
49
+ /** The host's modal dialogs, the project loading screen and the toast. */
50
+ export declare function HostDialogs(props: HostDialogsProps): React.ReactElement;
@@ -0,0 +1,113 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ChoiceDialog } from '../ChoiceDialog.js';
3
+ import { MessageDialog } from '../MessageDialog.js';
4
+ import { PromptDialog } from '../PromptDialog.js';
5
+ import { theme } from '../theme.js';
6
+ /** The host's modal dialogs, the project loading screen and the toast. */
7
+ export function HostDialogs(props) {
8
+ const { session, prefabInfo, prefabDialog, setPrefabDialog, prefabSwitch, setPrefabSwitch } = props;
9
+ return (_jsxs(_Fragment, { children: [props.loadingProject !== null && (_jsxs("div", { style: {
10
+ position: 'fixed',
11
+ inset: 0,
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ background: 'rgba(7, 11, 22, 0.8)',
16
+ zIndex: 900,
17
+ color: theme.text,
18
+ fontSize: 14,
19
+ }, children: ["Opening ", props.loadingProject, "\u2026"] })), props.toast && (_jsx("div", { role: "status", style: {
20
+ position: 'fixed',
21
+ top: 16,
22
+ left: '50%',
23
+ transform: 'translateX(-50%)',
24
+ zIndex: 950,
25
+ background: '#13204a',
26
+ border: `1px solid ${theme.borderActive}`,
27
+ borderRadius: 8,
28
+ padding: '8px 14px',
29
+ fontSize: 12.5,
30
+ color: theme.text,
31
+ boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)',
32
+ pointerEvents: 'none',
33
+ }, children: props.toast })), props.saveAsTarget && (_jsx(PromptDialog, { title: "Save as new asset \u2014 Folder/Name", defaultValue: props.saveAsTarget.name, placeholder: "Fire/Big explosion", confirmLabel: "Save", onSubmit: (path) => {
34
+ const entry = props.saveAsTarget;
35
+ props.setSaveAsTarget(null);
36
+ session?.saveEntryAsAsset(entry, path);
37
+ }, onCancel: () => props.setSaveAsTarget(null) })), prefabDialog === 'overwrite' && prefabInfo && (_jsx(ChoiceDialog, { title: `Overwrite prefab “${prefabInfo.name}”?`, message: (() => {
38
+ const count = session?.prefabInstanceCount() ?? 0;
39
+ return count > 0
40
+ ? `The asset is replaced with your edits. The ${count} “${prefabInfo.name}” effect(s) already in the scene will not change.`
41
+ : 'The asset is replaced with your edits. Effects already in the scene will not change.';
42
+ })(), actions: [
43
+ { label: 'Cancel', onSelect: () => setPrefabDialog(null) },
44
+ { label: 'Save as new…', onSelect: () => setPrefabDialog('saveAs') },
45
+ {
46
+ label: 'Overwrite',
47
+ primary: true,
48
+ onSelect: () => {
49
+ setPrefabDialog(null);
50
+ session?.overwritePrefab();
51
+ },
52
+ },
53
+ ], onCancel: () => setPrefabDialog(null) })), prefabSwitch && prefabInfo && (_jsx(ChoiceDialog, { title: `Save changes to prefab “${prefabInfo.name}”?`, message: `Before opening “${prefabSwitch.name}”. Saving overwrites the prefab asset; effects already in the scene will not change.`, actions: [
54
+ { label: 'Cancel', onSelect: () => setPrefabSwitch(null) },
55
+ {
56
+ label: "Don't save",
57
+ danger: true,
58
+ onSelect: () => {
59
+ setPrefabSwitch(null);
60
+ session?.openPrefab(prefabSwitch.id, true);
61
+ },
62
+ },
63
+ {
64
+ label: 'Save',
65
+ primary: true,
66
+ onSelect: () => {
67
+ setPrefabSwitch(null);
68
+ session?.overwritePrefab();
69
+ session?.openPrefab(prefabSwitch.id, true);
70
+ },
71
+ },
72
+ ], onCancel: () => setPrefabSwitch(null) })), prefabDialog === 'exit' && prefabInfo && (_jsx(ChoiceDialog, { title: `Save changes to prefab “${prefabInfo.name}”?`, message: "Saving overwrites the prefab asset. Effects already in the scene will not change.", actions: [
73
+ { label: 'Cancel', onSelect: () => setPrefabDialog(null) },
74
+ {
75
+ label: "Don't save",
76
+ danger: true,
77
+ onSelect: () => {
78
+ setPrefabDialog(null);
79
+ session?.closePrefab();
80
+ },
81
+ },
82
+ {
83
+ label: 'Save',
84
+ primary: true,
85
+ onSelect: () => {
86
+ setPrefabDialog(null);
87
+ session?.overwritePrefab();
88
+ session?.closePrefab();
89
+ },
90
+ },
91
+ ], onCancel: () => setPrefabDialog(null) })), prefabDialog === 'saveAs' && prefabInfo && (_jsx(PromptDialog, { title: "Save prefab as new asset \u2014 Folder/Name", defaultValue: prefabInfo.path.replace(/\.json$/i, ''), confirmLabel: "Save", onSubmit: (path) => {
92
+ setPrefabDialog(null);
93
+ session?.savePrefabAsNew(path);
94
+ }, onCancel: () => setPrefabDialog(null) })), props.exitPrompt && (_jsx(ChoiceDialog, { title: "Save changes to the scene?", message: "The scene has changes that are not saved. Assets are already saved.", actions: [
95
+ { label: 'Cancel', onSelect: () => props.setExitPrompt(false) },
96
+ {
97
+ label: "Don't save",
98
+ danger: true,
99
+ onSelect: () => {
100
+ props.setExitPrompt(false);
101
+ props.onExitProject?.();
102
+ },
103
+ },
104
+ {
105
+ label: 'Save',
106
+ primary: true,
107
+ onSelect: () => {
108
+ props.setExitPrompt(false);
109
+ void session?.saveScene().then((saved) => saved && props.onExitProject?.());
110
+ },
111
+ },
112
+ ], onCancel: () => props.setExitPrompt(false) })), props.message && (_jsx(MessageDialog, { title: props.message.title, message: props.message.message, onClose: () => props.setMessage(null) }))] }));
113
+ }
@@ -0,0 +1,32 @@
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
+ import type { ParticleSystem } from 'babylon.quarks';
3
+ import React from 'react';
4
+ import type { EffectBinding } from '../../core/binding.js';
5
+ import { type EffectEditorProps } from '../EffectEditor.js';
6
+ /**
7
+ * The effect's JSON, with a warning when it points at object URLs: a texture
8
+ * dropped in from disk lives at a `blob:` URL that dies with the page, so the
9
+ * exported effect would load without it.
10
+ */
11
+ export declare function exportEffectJSON(binding: EffectBinding, warn: (message: string) => void): string;
12
+ /** Props of {@link InspectorSidebar}. */
13
+ export interface InspectorSidebarProps extends Pick<EffectEditorProps, 'textureOptions' | 'geometryOptions' | 'resolveTexture' | 'resolveGeometry'> {
14
+ /** The focused effect, for the header and its actions. */
15
+ binding: EffectBinding | null;
16
+ /** The binding, system and node the inspector shows (they may lag a selection change). */
17
+ inspector: {
18
+ binding: EffectBinding;
19
+ system: ParticleSystem;
20
+ node: TransformNode;
21
+ } | null;
22
+ /** Header shown when the effect has no name. */
23
+ title?: string;
24
+ /** Called after the effect is renamed. */
25
+ onRenamed(name: string): void;
26
+ /** Shows a Save button that receives the effect's JSON. */
27
+ onSave?: (json: string) => void;
28
+ /** A short notice. */
29
+ toast(text: string): void;
30
+ }
31
+ /** The editor's right column: the effect's name, Export and Save, and the module inspector. */
32
+ export declare function InspectorSidebar(props: InspectorSidebarProps): React.ReactElement;
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { EffectEditor } from '../EffectEditor.js';
4
+ import { PromptDialog } from '../PromptDialog.js';
5
+ import { buttonStyle, theme } from '../theme.js';
6
+ import { INSPECTOR_WIDTH } from './hostStyles.js';
7
+ /**
8
+ * The effect's JSON, with a warning when it points at object URLs: a texture
9
+ * dropped in from disk lives at a `blob:` URL that dies with the page, so the
10
+ * exported effect would load without it.
11
+ */
12
+ export function exportEffectJSON(binding, warn) {
13
+ const json = binding.exportJSON('EditorEffect');
14
+ if (json.includes('"blob:')) {
15
+ warn('This effect references blob: textures, which will not load after a reload. Re-pick them from a hosted URL.');
16
+ }
17
+ return json;
18
+ }
19
+ /** The editor's right column: the effect's name, Export and Save, and the module inspector. */
20
+ export function InspectorSidebar(props) {
21
+ const { binding, inspector, onSave } = props;
22
+ const [renaming, setRenaming] = useState(false);
23
+ return (_jsxs("aside", { style: {
24
+ width: INSPECTOR_WIDTH,
25
+ flexShrink: 0,
26
+ borderLeft: `1px solid ${theme.border}`,
27
+ background: theme.panelBg,
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ minHeight: 0,
31
+ }, children: [_jsxs("div", { style: { padding: '12px 14px 10px', borderBottom: `1px solid ${theme.border}` }, children: [_jsx("div", { style: {
32
+ fontSize: 11,
33
+ fontWeight: 600,
34
+ color: theme.textDim,
35
+ textTransform: 'uppercase',
36
+ letterSpacing: 0.4,
37
+ marginBottom: 6,
38
+ }, children: "Inspector" }), _jsx("div", { className: "qe-hover-bg", title: binding ? 'Rename effect' : undefined, style: {
39
+ fontSize: 16,
40
+ fontWeight: 600,
41
+ cursor: binding ? 'pointer' : 'default',
42
+ borderRadius: 6,
43
+ padding: '2px 4px',
44
+ margin: '-2px -4px',
45
+ }, onClick: () => binding && setRenaming(true), children: (binding?.root.name || props.title) ?? 'Effect editor' }), renaming && binding && (_jsx(PromptDialog, { title: "Rename effect", defaultValue: binding.root.name, onSubmit: (name) => {
46
+ binding.apply(() => (binding.root.name = name));
47
+ props.onRenamed(name);
48
+ setRenaming(false);
49
+ }, onCancel: () => setRenaming(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => {
50
+ if (!binding)
51
+ return;
52
+ const blob = new Blob([exportEffectJSON(binding, props.toast)], { type: 'application/json' });
53
+ const link = document.createElement('a');
54
+ link.href = URL.createObjectURL(blob);
55
+ link.download = `${binding.root.name || 'effect'}.json`;
56
+ link.click();
57
+ URL.revokeObjectURL(link.href);
58
+ }, children: "Export" }), onSave && (_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => binding && onSave(exportEffectJSON(binding, props.toast)), children: "Save" }))] })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '6px 14px 20px' }, children: inspector ? (_jsx(EffectEditor, { binding: inspector.binding, selectedSystem: inspector.system, gizmoTargetNode: inspector.node, gizmoTargetLabel: inspector.node.name || 'Node', textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })) : (_jsx("div", { style: { fontSize: 13, color: theme.textDim, lineHeight: 1.5 }, children: "No effect selected." })) })] }));
59
+ }
@@ -0,0 +1,53 @@
1
+ import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
2
+ import React from 'react';
3
+ import type { PrefabInfo } from '../../core/editorSession.js';
4
+ import type { SceneEntry } from '../../core/sceneEntries.js';
5
+ /** Props of {@link SceneSidebar}. */
6
+ export interface SceneSidebarProps {
7
+ /** The open project, for its header; none in memory-only mode. */
8
+ projectName?: string;
9
+ /** Shows the back button in the project header. */
10
+ canExitProject: boolean;
11
+ /** The scene has unsaved changes. */
12
+ sceneDirty: boolean;
13
+ /** The project's scene is still loading. */
14
+ projectLoading: boolean;
15
+ /** The open prefab, if any. */
16
+ prefabInfo: PrefabInfo | null;
17
+ /** The open prefab has unsaved edits. */
18
+ prefabDirty: boolean;
19
+ /** The scene's entries. */
20
+ entries: SceneEntry[];
21
+ /** The focused entry's root. */
22
+ focusedRoot: TransformNode | null;
23
+ /** Shown entries, by root `uniqueId`. */
24
+ visibleRootIds: ReadonlySet<number>;
25
+ /** The entry shown alone, if any. */
26
+ soloRootId: number | null;
27
+ /** The node selected in the focused effect. */
28
+ selectedNode: TransformNode | null;
29
+ /** Back to the projects list. */
30
+ onExitProject(): void;
31
+ /** Save (the scene, or the open prefab). */
32
+ onSave(): void;
33
+ /** Back from the prefab to the scene. */
34
+ onExitPrefab(): void;
35
+ /** Save the open prefab. */
36
+ onSavePrefab(): void;
37
+ /** Focus an entry. */
38
+ onFocus(entry: SceneEntry): void;
39
+ /** Show or hide an entry. */
40
+ onToggleVisible(entry: SceneEntry): void;
41
+ /** Show an entry alone. */
42
+ onToggleSolo(entry: SceneEntry): void;
43
+ /** Save an entry as a new asset. */
44
+ onSaveAsAsset(entry: SceneEntry): void;
45
+ /** Remove an entry. */
46
+ onDelete(entry: SceneEntry): void;
47
+ /** Add a default effect. */
48
+ onCreateDefault(): void;
49
+ /** Select a node, focusing its entry first when needed. */
50
+ onSelectNode(entry: SceneEntry | null, node: TransformNode): void;
51
+ }
52
+ /** The editor's left column: the project header, then the Scene list or the open prefab. */
53
+ export declare function SceneSidebar(props: SceneSidebarProps): React.ReactElement;