babylon.quarks-editor 0.18.1 → 0.20.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 (157) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +6 -6
  3. package/dist/core/assetDragSession.d.ts +4 -0
  4. package/dist/core/assetDragSession.js +8 -0
  5. package/dist/core/assetTree.d.ts +39 -0
  6. package/dist/core/assetTree.js +112 -0
  7. package/dist/core/assets.d.ts +56 -0
  8. package/dist/core/assets.js +207 -0
  9. package/dist/core/binding.d.ts +6 -1
  10. package/dist/core/binding.js +7 -0
  11. package/dist/core/collision.d.ts +4 -5
  12. package/dist/core/collision.js +4 -5
  13. package/dist/core/colors.d.ts +1 -1
  14. package/dist/core/defaultEffect.d.ts +4 -5
  15. package/dist/core/defaultEffect.js +5 -14
  16. package/dist/core/effectTree.d.ts +1 -1
  17. package/dist/core/effectTree.js +18 -4
  18. package/dist/core/emitterShapeWireframe.d.ts +2 -1
  19. package/dist/core/emitterShapeWireframe.js +2 -1
  20. package/dist/core/galleryDropMarker.js +1 -1
  21. package/dist/core/galleryPlayback.d.ts +7 -7
  22. package/dist/core/galleryPlayback.js +2 -2
  23. package/dist/core/galleryScene.d.ts +8 -13
  24. package/dist/core/galleryScene.js +14 -42
  25. package/dist/core/gallerySelectionMarker.d.ts +1 -0
  26. package/dist/core/gallerySelectionMarker.js +1 -0
  27. package/dist/core/history.d.ts +12 -1
  28. package/dist/core/history.js +16 -3
  29. package/dist/core/indexedDbProjectStore.d.ts +35 -0
  30. package/dist/core/indexedDbProjectStore.js +299 -0
  31. package/dist/core/lineShaders.d.ts +2 -0
  32. package/dist/core/lineShaders.js +7 -0
  33. package/dist/core/loadEffect.d.ts +2 -2
  34. package/dist/core/loadEffect.js +1 -1
  35. package/dist/core/pickAssetFiles.d.ts +6 -0
  36. package/dist/core/{pickGalleryFiles.js → pickAssetFiles.js} +13 -8
  37. package/dist/core/projectStore.d.ts +61 -0
  38. package/dist/core/projectStore.js +14 -0
  39. package/dist/core/sceneEntries.d.ts +35 -0
  40. package/dist/core/sceneEntries.js +62 -0
  41. package/dist/core/shapes.d.ts +1 -1
  42. package/dist/core/shapes.js +35 -8
  43. package/dist/core/systems.d.ts +1 -1
  44. package/dist/core/systems.js +3 -1
  45. package/dist/core/timeline.d.ts +1 -2
  46. package/dist/core/transport.d.ts +12 -0
  47. package/dist/core/transport.js +36 -0
  48. package/dist/core/values.d.ts +1 -1
  49. package/dist/core/viewportPicker.d.ts +4 -4
  50. package/dist/core/viewportPicker.js +3 -3
  51. package/dist/index.d.ts +15 -11
  52. package/dist/index.js +8 -6
  53. package/dist/react/AssetsPanel.d.ts +22 -0
  54. package/dist/react/AssetsPanel.js +252 -0
  55. package/dist/react/ChoiceDialog.d.ts +18 -0
  56. package/dist/react/ChoiceDialog.js +48 -0
  57. package/dist/react/ContextMenu.d.ts +16 -0
  58. package/dist/react/ContextMenu.js +70 -0
  59. package/dist/react/CurveEditor.d.ts +1 -2
  60. package/dist/react/CurveEditor.js +14 -3
  61. package/dist/react/EditorWorkspace.d.ts +21 -0
  62. package/dist/react/EditorWorkspace.js +63 -0
  63. package/dist/react/EffectEditor.d.ts +3 -3
  64. package/dist/react/EffectEditor.js +9 -6
  65. package/dist/react/EffectEditorHost.d.ts +20 -3
  66. package/dist/react/EffectEditorHost.js +1426 -558
  67. package/dist/react/EffectTreeView.d.ts +17 -0
  68. package/dist/react/EffectTreeView.js +52 -0
  69. package/dist/react/GradientEditor.d.ts +1 -2
  70. package/dist/react/GradientEditor.js +8 -1
  71. package/dist/react/MessageDialog.js +7 -1
  72. package/dist/react/ModuleSection.js +10 -3
  73. package/dist/react/ObjectModule.d.ts +5 -2
  74. package/dist/react/ObjectModule.js +8 -11
  75. package/dist/react/ProjectsPage.d.ts +9 -0
  76. package/dist/react/ProjectsPage.js +140 -0
  77. package/dist/react/ScenePanel.d.ts +26 -0
  78. package/dist/react/ScenePanel.js +134 -0
  79. package/dist/react/SubEmittersModule.d.ts +1 -2
  80. package/dist/react/TimelinePanel.d.ts +11 -4
  81. package/dist/react/TimelinePanel.js +65 -40
  82. package/dist/react/TimelineTrackRow.d.ts +1 -1
  83. package/dist/react/TimelineTrackRow.js +20 -6
  84. package/dist/react/TransportBar.d.ts +24 -0
  85. package/dist/react/TransportBar.js +43 -0
  86. package/dist/react/ValueField.d.ts +1 -2
  87. package/dist/react/behaviorModules.js +24 -12
  88. package/dist/react/index.d.ts +31 -17
  89. package/dist/react/index.js +18 -11
  90. package/dist/react/inspectorHints.js +1 -2
  91. package/dist/react/modules.js +55 -25
  92. package/package.json +68 -68
  93. package/src/core/assetDragSession.ts +12 -0
  94. package/src/core/assetTree.ts +148 -0
  95. package/src/core/assets.ts +262 -0
  96. package/src/core/binding.ts +10 -2
  97. package/src/core/collision.ts +4 -5
  98. package/src/core/colors.ts +1 -1
  99. package/src/core/defaultEffect.ts +10 -23
  100. package/src/core/effectTree.ts +22 -7
  101. package/src/core/emitterShapeWireframe.ts +9 -4
  102. package/src/core/galleryDropMarker.ts +6 -2
  103. package/src/core/galleryPlayback.ts +9 -9
  104. package/src/core/galleryScene.ts +15 -63
  105. package/src/core/gallerySelectionMarker.ts +6 -1
  106. package/src/core/history.ts +24 -3
  107. package/src/core/indexedDbProjectStore.ts +357 -0
  108. package/src/core/lineShaders.ts +7 -0
  109. package/src/core/loadEffect.ts +3 -3
  110. package/src/core/{pickGalleryFiles.ts → pickAssetFiles.ts} +16 -8
  111. package/src/core/projectStore.ts +81 -0
  112. package/src/core/sceneEntries.ts +96 -0
  113. package/src/core/shapes.ts +48 -10
  114. package/src/core/systems.ts +9 -3
  115. package/src/core/timeline.ts +8 -8
  116. package/src/core/transport.ts +53 -0
  117. package/src/core/values.ts +1 -1
  118. package/src/core/viewportPicker.ts +5 -5
  119. package/src/index.ts +29 -18
  120. package/src/react/AssetsPanel.tsx +559 -0
  121. package/src/react/ChoiceDialog.tsx +93 -0
  122. package/src/react/ContextMenu.tsx +107 -0
  123. package/src/react/CurveEditor.tsx +40 -10
  124. package/src/react/EditorWorkspace.tsx +95 -0
  125. package/src/react/EffectEditor.tsx +28 -16
  126. package/src/react/EffectEditorHost.tsx +2005 -721
  127. package/src/react/EffectTreeView.tsx +117 -0
  128. package/src/react/GradientEditor.tsx +21 -4
  129. package/src/react/InspectorInfoIcon.tsx +6 -2
  130. package/src/react/MessageDialog.tsx +9 -1
  131. package/src/react/ModuleSection.tsx +12 -3
  132. package/src/react/ObjectModule.tsx +41 -16
  133. package/src/react/ProjectsPage.tsx +306 -0
  134. package/src/react/PromptDialog.tsx +5 -1
  135. package/src/react/ScenePanel.tsx +318 -0
  136. package/src/react/SubEmittersModule.tsx +1 -2
  137. package/src/react/TimelinePanel.tsx +171 -99
  138. package/src/react/TimelineTrackRow.tsx +71 -18
  139. package/src/react/TransportBar.tsx +114 -0
  140. package/src/react/ValueField.tsx +0 -1
  141. package/src/react/behaviorModules.tsx +148 -42
  142. package/src/react/index.ts +47 -33
  143. package/src/react/inspectorHints.ts +1 -2
  144. package/src/react/modules.tsx +173 -57
  145. package/dist/core/galleryDragSession.d.ts +0 -4
  146. package/dist/core/galleryDragSession.js +0 -8
  147. package/dist/core/galleryTree.d.ts +0 -49
  148. package/dist/core/galleryTree.js +0 -138
  149. package/dist/core/loadEffectGallery.d.ts +0 -36
  150. package/dist/core/loadEffectGallery.js +0 -97
  151. package/dist/core/pickGalleryFiles.d.ts +0 -5
  152. package/dist/react/GalleryPanel.d.ts +0 -20
  153. package/dist/react/GalleryPanel.js +0 -246
  154. package/src/core/galleryDragSession.ts +0 -12
  155. package/src/core/galleryTree.ts +0 -187
  156. package/src/core/loadEffectGallery.ts +0 -146
  157. package/src/react/GalleryPanel.tsx +0 -517
@@ -1,32 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { useMemo, useState } from 'react';
3
- import { Mesh } from '@babylonjs/core/Meshes/mesh';
4
2
  import { Texture } from '@babylonjs/core/Materials/Textures/texture';
3
+ import { Mesh } from '@babylonjs/core/Meshes/mesh';
4
+ import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
5
5
  import { ColorOverLife, ConstantColor, ConstantValue, EulerGenerator, Gradient, IntervalValue, RandomColor, RandomColorBetweenGradient, RandomQuatGenerator, RenderMode, SizeOverLife, Vector3Function, Vector4, } from 'babylon.quarks';
6
+ import React, { useMemo, useState } from 'react';
6
7
  import { DEFAULT_GRADIENT_STOPS, buildGradient, findBehavior, hexToRgb, readGradientStops, rgbToHex, } from '../core/colors';
7
- import { DEFAULT_SHAPE_PARAMS, SHAPE_PARAM_KEYS, SHAPE_TYPES, createShape, getShapeType, readShapeParams } from '../core/shapes';
8
- import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values';
9
8
  import { applyRendererMaterial, getMaterialLabel } from '../core/material';
9
+ import { DEFAULT_SHAPE_PARAMS, SHAPE_PARAM_KEYS, SHAPE_TYPES, createShape, getShapeType, readShapeParams, } from '../core/shapes';
10
+ import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values';
10
11
  import { CurveEditor } from './CurveEditor';
11
12
  import { GradientEditor } from './GradientEditor';
13
+ import { InspectorInfoIcon } from './InspectorInfoIcon';
12
14
  import { ModuleSection } from './ModuleSection';
13
15
  import { PromptDialog } from './PromptDialog';
14
- import { CheckboxField, NumberField, Row, SelectField } from './fields';
15
16
  import { ValueField } from './ValueField';
17
+ import { CheckboxField, NumberField, Row, SelectField } from './fields';
16
18
  import { iconStyle } from './icons';
17
19
  import { theme } from './theme';
18
- import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
19
- import { InspectorInfoIcon } from './InspectorInfoIcon';
20
20
  const MESH_QUAD_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]);
21
21
  const MESH_QUAD_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
22
22
  const MESH_QUAD_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
23
23
  const MESH_CUBE_POSITIONS = new Float32Array([
24
- -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5,
25
- -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5,
24
+ -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 0.5,
25
+ -0.5, 0.5, 0.5,
26
26
  ]);
27
27
  const MESH_CUBE_INDICES = new Uint32Array([
28
- 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1,
29
- 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2,
28
+ 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2,
30
29
  ]);
31
30
  function arraysEqual(a, b) {
32
31
  if (a.length !== b.length) {
@@ -65,21 +64,33 @@ function ColorInput(props) {
65
64
  return (_jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("input", { type: "color", className: "qe-hover", value: rgbToHex(value.x, value.y, value.z), onChange: (e) => {
66
65
  const rgb = hexToRgb(e.target.value);
67
66
  onChange(new Vector4(rgb.r, rgb.g, rgb.b, value.w));
68
- }, style: { width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer' } }), _jsx(NumberField, { value: value.w, min: 0, step: 0.05, onChange: (a) => onChange(new Vector4(value.x, value.y, value.z, Math.min(1, a))) })] }));
67
+ }, style: {
68
+ width: 42,
69
+ height: 24,
70
+ border: 'none',
71
+ background: 'transparent',
72
+ padding: 0,
73
+ cursor: 'pointer',
74
+ } }), _jsx(NumberField, { value: value.w, min: 0, step: 0.05, onChange: (a) => onChange(new Vector4(value.x, value.y, value.z, Math.min(1, a))) })] }));
69
75
  }
70
76
  export function MainModule({ binding }) {
71
77
  const system = binding.system;
72
78
  const startColor = system.startColor;
73
79
  const colorMode = startColor instanceof RandomColor ? 'random' : 'constant';
74
- const colorA = startColor instanceof RandomColor ? startColor.a
75
- : startColor instanceof ConstantColor ? startColor.color
80
+ const colorA = startColor instanceof RandomColor
81
+ ? startColor.a
82
+ : startColor instanceof ConstantColor
83
+ ? startColor.color
76
84
  : new Vector4(1, 1, 1, 1);
77
85
  const colorB = startColor instanceof RandomColor ? startColor.b : new Vector4(1, 0.5, 0.2, 1);
78
86
  const rotation = system.startRotation;
79
87
  const isEuler = rotation instanceof EulerGenerator;
80
- const rotationMode = rotation instanceof RandomQuatGenerator ? '3d'
81
- : isEuler ? 'euler'
82
- : readScalar(rotation).mode === 'random' ? 'random'
88
+ const rotationMode = rotation instanceof RandomQuatGenerator
89
+ ? '3d'
90
+ : isEuler
91
+ ? 'euler'
92
+ : readScalar(rotation).mode === 'random'
93
+ ? 'random'
83
94
  : 'angle';
84
95
  const rotationState = readScalar(rotation instanceof RandomQuatGenerator || isEuler ? undefined : rotation);
85
96
  const eulerAngles = {
@@ -93,8 +104,10 @@ export function MainModule({ binding }) {
93
104
  });
94
105
  const startSize = system.startSize;
95
106
  const size3D = startSize.type === 'vec3function';
96
- const colorGenMode = startColor instanceof RandomColorBetweenGradient ? 'gradient2'
97
- : startColor instanceof Gradient ? 'gradient'
107
+ const colorGenMode = startColor instanceof RandomColorBetweenGradient
108
+ ? 'gradient2'
109
+ : startColor instanceof Gradient
110
+ ? 'gradient'
98
111
  : colorMode;
99
112
  return (_jsxs(ModuleSection, { title: "Main", children: [_jsx(Row, { label: "Duration", children: _jsx(NumberField, { value: system.duration, min: 0.05, onChange: (v) => binding.apply((s) => (s.duration = v)) }) }), _jsx(Row, { label: "Looping", children: _jsx(CheckboxField, { value: system.looping, onChange: (v) => binding.apply((s) => (s.looping = v)) }) }), _jsx(Row, { label: "World space", children: _jsx(CheckboxField, { value: system.worldSpace, onChange: (v) => binding.apply((s) => (s.worldSpace = v)) }) }), _jsx(Row, { label: "Prewarm", children: _jsx(CheckboxField, { value: system.prewarm, onChange: (v) => binding.apply((s) => (s.prewarm = v)) }) }), _jsx(ValueField, { label: "Start delay", generator: system.startDelay, min: 0, curveMax: 5, onChange: (g) => binding.apply((s) => {
100
113
  s.startDelay = g;
@@ -134,7 +147,8 @@ export function MainModule({ binding }) {
134
147
  s.startColor = new ConstantColor(colorA);
135
148
  }
136
149
  }) }) }), (colorGenMode === 'constant' || colorGenMode === 'random') && (_jsx(Row, { label: "", children: _jsx(ColorInput, { value: colorA, onChange: (next) => binding.apply((s) => {
137
- s.startColor = colorGenMode === 'random' ? new RandomColor(next, colorB) : new ConstantColor(next);
150
+ s.startColor =
151
+ colorGenMode === 'random' ? new RandomColor(next, colorB) : new ConstantColor(next);
138
152
  }) }) })), colorGenMode === 'random' && (_jsx(Row, { label: "", children: _jsx(ColorInput, { value: colorB, onChange: (next) => binding.apply((s) => (s.startColor = new RandomColor(colorA, next))) }) })), colorGenMode === 'gradient' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(GradientEditor, { stops: readGradientStops(startColor), onChange: (stops) => binding.apply((s) => (s.startColor = buildGradient(stops))) }) })), colorGenMode === 'gradient2' && (_jsxs("div", { style: { marginTop: 6, display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx(GradientEditor, { stops: readGradientStops(startColor.gradient1), onChange: (stops) => binding.apply((s) => {
139
153
  const g = s.startColor;
140
154
  s.startColor = new RandomColorBetweenGradient(buildGradient(stops), g.gradient2);
@@ -160,7 +174,14 @@ export function MainModule({ binding }) {
160
174
  export function EmissionModule({ binding }) {
161
175
  const system = binding.system;
162
176
  const bursts = system.emissionBursts ?? [];
163
- return (_jsxs(ModuleSection, { title: "Emission", children: [_jsx(ValueField, { label: "Rate over time", generator: system.emissionOverTime, min: 0, curveMax: 500, onChange: (g) => binding.apply((s) => (s.emissionOverTime = g)) }), _jsx(ValueField, { label: "Rate over distance", generator: system.emissionOverDistance, min: 0, curveMax: 50, onChange: (g) => binding.apply((s) => (s.emissionOverDistance = g)) }), bursts.map((burst, i) => (_jsxs("div", { style: { marginTop: 8, padding: '6px 8px', border: '1px solid #22305c', borderRadius: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("span", { style: { fontSize: 11.5, color: '#9eb9ff' }, children: ["Burst ", i + 1] }), _jsx("button", { className: "qe-hover", style: { background: 'none', border: 'none', color: '#e08c8c', cursor: 'pointer', padding: 0, display: 'inline-flex' }, title: "Remove burst", onClick: () => binding.apply((s) => s.emissionBursts.splice(i, 1)), children: _jsx(XMarkIcon, { style: iconStyle(14) }) })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2, marginTop: 4 }, children: [_jsx(Row, { label: "Time", children: _jsx(NumberField, { value: burst.time, min: 0, onChange: (time) => binding.apply(() => (burst.time = time)) }) }), _jsx(Row, { label: "Count", children: _jsx(NumberField, { value: readScalar(burst.count).value, min: 0, step: 1, onChange: (count) => binding.apply(() => (burst.count = new ConstantValue(Math.round(count)))) }) }), _jsx(Row, { label: "Cycles", children: _jsx(NumberField, { value: burst.cycle, min: 1, step: 1, onChange: (v) => binding.apply(() => (burst.cycle = Math.round(v))) }) }), _jsx(Row, { label: "Interval", children: _jsx(NumberField, { value: burst.interval, min: 0.01, onChange: (v) => binding.apply(() => (burst.interval = v)) }) }), _jsx(Row, { label: "Probability", children: _jsx(NumberField, { value: burst.probability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (burst.probability = Math.min(1, v))) }) })] })] }, i))), _jsxs("button", { className: "qe-hover", style: {
177
+ return (_jsxs(ModuleSection, { title: "Emission", children: [_jsx(ValueField, { label: "Rate over time", generator: system.emissionOverTime, min: 0, curveMax: 500, onChange: (g) => binding.apply((s) => (s.emissionOverTime = g)) }), _jsx(ValueField, { label: "Rate over distance", generator: system.emissionOverDistance, min: 0, curveMax: 50, onChange: (g) => binding.apply((s) => (s.emissionOverDistance = g)) }), bursts.map((burst, i) => (_jsxs("div", { style: { marginTop: 8, padding: '6px 8px', border: '1px solid #22305c', borderRadius: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("span", { style: { fontSize: 11.5, color: '#9eb9ff' }, children: ["Burst ", i + 1] }), _jsx("button", { className: "qe-hover", style: {
178
+ background: 'none',
179
+ border: 'none',
180
+ color: '#e08c8c',
181
+ cursor: 'pointer',
182
+ padding: 0,
183
+ display: 'inline-flex',
184
+ }, title: "Remove burst", onClick: () => binding.apply((s) => s.emissionBursts.splice(i, 1)), children: _jsx(XMarkIcon, { style: iconStyle(14) }) })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2, marginTop: 4 }, children: [_jsx(Row, { label: "Time", children: _jsx(NumberField, { value: burst.time, min: 0, onChange: (time) => binding.apply(() => (burst.time = time)) }) }), _jsx(Row, { label: "Count", children: _jsx(NumberField, { value: readScalar(burst.count).value, min: 0, step: 1, onChange: (count) => binding.apply(() => (burst.count = new ConstantValue(Math.round(count)))) }) }), _jsx(Row, { label: "Cycles", children: _jsx(NumberField, { value: burst.cycle, min: 1, step: 1, onChange: (v) => binding.apply(() => (burst.cycle = Math.round(v))) }) }), _jsx(Row, { label: "Interval", children: _jsx(NumberField, { value: burst.interval, min: 0.01, onChange: (v) => binding.apply(() => (burst.interval = v)) }) }), _jsx(Row, { label: "Probability", children: _jsx(NumberField, { value: burst.probability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (burst.probability = Math.min(1, v))) }) })] })] }, i))), _jsxs("button", { className: "qe-hover", style: {
164
185
  marginTop: 8,
165
186
  background: 'none',
166
187
  border: '1px dashed #34477f',
@@ -172,7 +193,13 @@ export function EmissionModule({ binding }) {
172
193
  display: 'inline-flex',
173
194
  alignItems: 'center',
174
195
  gap: 6,
175
- }, onClick: () => binding.apply((s) => s.emissionBursts.push({ time: 0, count: new ConstantValue(20), cycle: 1, interval: 0.01, probability: 1 })), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add burst"] })] }));
196
+ }, onClick: () => binding.apply((s) => s.emissionBursts.push({
197
+ time: 0,
198
+ count: new ConstantValue(20),
199
+ cycle: 1,
200
+ interval: 0.01,
201
+ probability: 1,
202
+ })), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add burst"] })] }));
176
203
  }
177
204
  // Shapes whose constructor doesn't accept `mode`/`spread` at all — showing those controls
178
205
  // for them would edit a value the shape silently ignores, just like the old "default to
@@ -202,7 +229,9 @@ export function ShapeModule({ binding }) {
202
229
  export function SizeOverLifeModule({ binding }) {
203
230
  const system = binding.system;
204
231
  const behavior = findBehavior(system.behaviors, 'SizeOverLife');
205
- const curve = behavior ? readScalar(behavior.size).curve : [0.2, 1, 1, 0.4];
232
+ const curve = behavior
233
+ ? readScalar(behavior.size).curve
234
+ : [0.2, 1, 1, 0.4];
206
235
  return (_jsx(ModuleSection, { title: "Size over Lifetime", enabled: !!behavior, onToggle: (enabled) => binding.apply((s) => {
207
236
  removeBehavior(s, 'SizeOverLife');
208
237
  if (enabled) {
@@ -233,7 +262,7 @@ export function ColorOverLifeModule({ binding }) {
233
262
  * isometric rotation, scales them to fit, and strokes the triangle edges on a 2D canvas — enough
234
263
  * for the user to recognise which mesh they picked without spinning up a second Babylon scene.
235
264
  */
236
- function MeshPreview({ positions, indices, size = 96 }) {
265
+ function MeshPreview({ positions, indices, size = 96, }) {
237
266
  const canvasRef = React.useRef(null);
238
267
  React.useEffect(() => {
239
268
  const canvas = canvasRef.current;
@@ -318,7 +347,8 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
318
347
  { value: RenderMode.Trail, label: 'Trail' },
319
348
  { value: RenderMode.Mesh, label: 'Mesh' },
320
349
  ], onChange: (mode) => binding.apply((s) => (s.renderMode = mode)) }) }), system.renderMode === RenderMode.Trail && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Trail length", children: _jsx(NumberField, { value: readScalar(system.rendererEmitterSettings.startLength).value, min: 1, step: 1, onChange: (v) => binding.apply((s) => {
321
- s.rendererEmitterSettings.startLength = new ConstantValue(Math.round(v));
350
+ s.rendererEmitterSettings.startLength =
351
+ new ConstantValue(Math.round(v));
322
352
  }) }) }), _jsx(Row, { label: "Follow origin", children: _jsx(CheckboxField, { value: !!system.rendererEmitterSettings.followLocalOrigin, onChange: (v) => binding.apply((s) => {
323
353
  s.rendererEmitterSettings.followLocalOrigin = v;
324
354
  }) }) })] })), system.renderMode === RenderMode.StretchedBillBoard && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Speed factor", children: _jsx(NumberField, { value: system.rendererEmitterSettings.speedFactor ?? 0, step: 0.1, onChange: (v) => binding.apply((s) => (s.rendererEmitterSettings.speedFactor = v)) }) }), _jsx(Row, { label: "Length factor", children: _jsx(NumberField, { value: system.rendererEmitterSettings.lengthFactor ?? 2, step: 0.1, onChange: (v) => binding.apply((s) => (s.rendererEmitterSettings.lengthFactor = v)) }) })] })), _jsxs("div", { style: {
package/package.json CHANGED
@@ -1,72 +1,72 @@
1
1
  {
2
- "name": "babylon.quarks-editor",
3
- "version": "0.18.1",
4
- "description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
2
+ "name": "babylon.quarks-editor",
3
+ "version": "0.20.0",
4
+ "description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./react": {
14
+ "import": "./dist/react/index.js",
15
+ "types": "./dist/react/index.d.ts"
16
+ }
12
17
  },
13
- "./react": {
14
- "import": "./dist/react/index.js",
15
- "types": "./dist/react/index.d.ts"
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "LICENSE",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "scripts": {
26
+ "clean": "rimraf dist",
27
+ "build": "npm run clean && tsc -p tsconfig.json"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
32
+ },
33
+ "keywords": [
34
+ "babylonjs",
35
+ "babylon.js",
36
+ "particles",
37
+ "particle-system",
38
+ "particle-editor",
39
+ "vfx",
40
+ "vfx-editor",
41
+ "shuriken",
42
+ "editor",
43
+ "react",
44
+ "quarks"
45
+ ],
46
+ "author": "soullnik <soullnik96@gmail.com>",
47
+ "license": "MIT",
48
+ "bugs": {
49
+ "url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
50
+ },
51
+ "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
52
+ "peerDependencies": {
53
+ "@babylonjs/core": ">=9.0.0",
54
+ "@babylonjs/materials": ">=9.0.0",
55
+ "@heroicons/react": ">=2.0.0",
56
+ "@tanstack/react-virtual": ">=3.0.0",
57
+ "babylon.quarks": ">=0.17.0",
58
+ "react": ">=18",
59
+ "react-dom": ">=18"
60
+ },
61
+ "devDependencies": {
62
+ "@babylonjs/materials": "9.6.2",
63
+ "@heroicons/react": "^2.2.0",
64
+ "@tanstack/react-virtual": "^3.14.6",
65
+ "@types/react": "^19.0.0",
66
+ "@types/react-dom": "^19.0.0",
67
+ "react": "^19.0.0",
68
+ "react-dom": "^19.0.0",
69
+ "rimraf": "^6.0.0",
70
+ "typescript": "^5.9.3"
16
71
  }
17
- },
18
- "files": [
19
- "dist",
20
- "src",
21
- "LICENSE",
22
- "README.md",
23
- "CHANGELOG.md"
24
- ],
25
- "scripts": {
26
- "clean": "rimraf dist",
27
- "build": "npm run clean && tsc -p tsconfig.json"
28
- },
29
- "repository": {
30
- "type": "git",
31
- "url": "git+https://github.com/Soullnik/babylon.quarks-standalone.git"
32
- },
33
- "keywords": [
34
- "babylonjs",
35
- "babylon.js",
36
- "particles",
37
- "particle-system",
38
- "particle-editor",
39
- "vfx",
40
- "vfx-editor",
41
- "shuriken",
42
- "editor",
43
- "react",
44
- "quarks"
45
- ],
46
- "author": "soullnik <soullnik96@gmail.com>",
47
- "license": "MIT",
48
- "bugs": {
49
- "url": "https://github.com/Soullnik/babylon.quarks-standalone/issues"
50
- },
51
- "homepage": "https://github.com/Soullnik/babylon.quarks-standalone#readme",
52
- "peerDependencies": {
53
- "@babylonjs/core": ">=9.0.0",
54
- "@babylonjs/materials": ">=9.0.0",
55
- "@heroicons/react": ">=2.0.0",
56
- "@tanstack/react-virtual": ">=3.0.0",
57
- "babylon.quarks": ">=0.17.0",
58
- "react": ">=18",
59
- "react-dom": ">=18"
60
- },
61
- "devDependencies": {
62
- "@babylonjs/materials": "9.6.2",
63
- "@heroicons/react": "^2.2.0",
64
- "@tanstack/react-virtual": "^3.14.6",
65
- "@types/react": "^19.0.0",
66
- "@types/react-dom": "^19.0.0",
67
- "react": "^19.0.0",
68
- "react-dom": "^19.0.0",
69
- "rimraf": "^6.0.0",
70
- "typescript": "^5.9.3"
71
- }
72
72
  }
@@ -0,0 +1,12 @@
1
+ import type {AssetDragPayload} from './assetTree';
2
+
3
+ let activeDrag: AssetDragPayload | null = null;
4
+
5
+ /** Stores the current asset drag payload for viewport preview (getData is drop-only in most browsers). */
6
+ export function setAssetDragSession(payload: AssetDragPayload | null): void {
7
+ activeDrag = payload;
8
+ }
9
+
10
+ export function getAssetDragSession(): AssetDragPayload | null {
11
+ return activeDrag;
12
+ }
@@ -0,0 +1,148 @@
1
+ import type {EffectAsset} from './assets';
2
+
3
+ export interface AssetTreeFolder {
4
+ kind: 'folder';
5
+ name: string;
6
+ path: string;
7
+ children: AssetTreeNode[];
8
+ }
9
+
10
+ export interface AssetTreeEffect {
11
+ kind: 'asset';
12
+ name: string;
13
+ path: string;
14
+ asset: EffectAsset;
15
+ }
16
+
17
+ export type AssetTreeNode = AssetTreeFolder | AssetTreeEffect;
18
+
19
+ function compareNodes(a: AssetTreeNode, b: AssetTreeNode): number {
20
+ if (a.kind !== b.kind) {
21
+ return a.kind === 'folder' ? -1 : 1;
22
+ }
23
+ return a.name.localeCompare(b.name, undefined, {sensitivity: 'base'});
24
+ }
25
+
26
+ /** Builds a folder tree from asset relative paths (e.g. `VFX/Fire/Explosion.json`). */
27
+ export function buildAssetTree(assets: EffectAsset[]): AssetTreeNode[] {
28
+ const roots: AssetTreeNode[] = [];
29
+
30
+ for (const asset of assets) {
31
+ const segments = asset.path
32
+ .replace(/\.json$/i, '')
33
+ .split('/')
34
+ .filter(Boolean);
35
+ const fileName = segments.pop() ?? asset.name;
36
+ let level = roots;
37
+ let pathSoFar = '';
38
+
39
+ for (const segment of segments) {
40
+ pathSoFar = pathSoFar ? `${pathSoFar}/${segment}` : segment;
41
+ let folder = level.find((node): node is AssetTreeFolder => node.kind === 'folder' && node.name === segment);
42
+ if (!folder) {
43
+ folder = {kind: 'folder', name: segment, path: pathSoFar, children: []};
44
+ level.push(folder);
45
+ }
46
+ level = folder.children;
47
+ }
48
+
49
+ level.push({kind: 'asset', name: fileName, path: asset.path, asset});
50
+ }
51
+
52
+ const sortTree = (nodes: AssetTreeNode[]): void => {
53
+ nodes.sort(compareNodes);
54
+ for (const node of nodes) {
55
+ if (node.kind === 'folder') {
56
+ sortTree(node.children);
57
+ }
58
+ }
59
+ };
60
+
61
+ sortTree(roots);
62
+ return roots;
63
+ }
64
+
65
+ /** Children of the folder at `path` (`''` is the top level), or null when it no longer exists. */
66
+ export function folderChildren(tree: AssetTreeNode[], path: string): AssetTreeNode[] | null {
67
+ let level = tree;
68
+ for (const segment of path.split('/').filter(Boolean)) {
69
+ const folder = level.find((node): node is AssetTreeFolder => node.kind === 'folder' && node.name === segment);
70
+ if (!folder) {
71
+ return null;
72
+ }
73
+ level = folder.children;
74
+ }
75
+ return level;
76
+ }
77
+
78
+ /** Assets whose name or path contains `query`, across every folder, sorted by name. */
79
+ export function searchAssets(assets: EffectAsset[], query: string): EffectAsset[] {
80
+ const q = query.trim().toLowerCase();
81
+ return assets
82
+ .filter((asset) => `${asset.path} ${asset.name}`.toLowerCase().includes(q))
83
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}));
84
+ }
85
+
86
+ export const ASSET_DRAG_MIME = 'application/x-quarks-asset-drop';
87
+
88
+ export type AssetDragPayload = {kind: 'asset'; id: number} | {kind: 'folder'; path: string};
89
+
90
+ export function writeAssetDrag(dataTransfer: DataTransfer, payload: AssetDragPayload): void {
91
+ dataTransfer.setData(ASSET_DRAG_MIME, JSON.stringify(payload));
92
+ dataTransfer.effectAllowed = 'copy';
93
+ }
94
+
95
+ export function readAssetDrag(dataTransfer: DataTransfer): AssetDragPayload | null {
96
+ const raw = dataTransfer.getData(ASSET_DRAG_MIME);
97
+ if (!raw) {
98
+ return null;
99
+ }
100
+ try {
101
+ const parsed = JSON.parse(raw) as AssetDragPayload;
102
+ if (parsed?.kind === 'asset' && typeof parsed.id === 'number') {
103
+ return parsed;
104
+ }
105
+ if (parsed?.kind === 'folder' && typeof parsed.path === 'string') {
106
+ return parsed;
107
+ }
108
+ } catch {
109
+ return null;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /** Returns every asset whose JSON path sits inside `folderPath` (recursive). */
115
+ export function collectAssetsInFolder(assets: EffectAsset[], folderPath: string): EffectAsset[] {
116
+ const prefix = `${folderPath}/`;
117
+ return assets.filter((asset) => asset.path.startsWith(prefix));
118
+ }
119
+
120
+ /** Collects all assets under a folder tree node. */
121
+ export function collectFolderAssets(node: AssetTreeFolder): EffectAsset[] {
122
+ const collected: EffectAsset[] = [];
123
+ const walk = (nodes: AssetTreeNode[]): void => {
124
+ for (const child of nodes) {
125
+ if (child.kind === 'asset') {
126
+ collected.push(child.asset);
127
+ } else {
128
+ walk(child.children);
129
+ }
130
+ }
131
+ };
132
+ walk(node.children);
133
+ return collected;
134
+ }
135
+
136
+ /** Resolves a drag payload to the assets that would be instantiated on drop. */
137
+ export function describeAssetDrag(
138
+ assets: EffectAsset[],
139
+ payload: AssetDragPayload
140
+ ): {assets: EffectAsset[]; label: string} {
141
+ if (payload.kind === 'asset') {
142
+ const asset = assets.find((item) => item.id === payload.id);
143
+ return {assets: asset ? [asset] : [], label: asset?.name ?? 'Effect'};
144
+ }
145
+
146
+ const label = payload.path.split('/').filter(Boolean).pop() ?? payload.path;
147
+ return {assets: collectAssetsInFolder(assets, payload.path), label};
148
+ }