@nice2dev/ui-3d 1.3.14 → 1.3.16

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 (108) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,606 +1,1221 @@
1
- 'use strict';
2
-
3
- var jsxRuntime = require('react/jsx-runtime');
4
- var ui = require('@nice2dev/ui');
5
- var React = require('react');
6
- var i18n = require('../core/i18n.js');
7
- var ParticleEditor_module = require('./ParticleEditor.module.css.js');
8
- var particleEditorTypes = require('./particleEditorTypes.js');
9
- var particleEditorUtils = require('./particleEditorUtils.js');
10
-
11
- /* ═══════════════════════════════════════════════════════════════════════════
12
- State Reducer
13
- ═══════════════════════════════════════════════════════════════════════════ */
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const ui = require("@nice2dev/ui");
5
+ const React = require("react");
6
+ const i18n = require("../core/i18n.js");
7
+ const ParticleEditor_module = require("./ParticleEditor.module.css.js");
8
+ const particleEditorTypes = require("./particleEditorTypes.js");
9
+ const particleEditorUtils = require("./particleEditorUtils.js");
14
10
  function createInitialState(systems) {
15
- var _a;
16
- return {
17
- systems: systems || [particleEditorUtils.createDefaultSystem()],
18
- selectedSystemId: ((_a = systems === null || systems === void 0 ? void 0 : systems[0]) === null || _a === void 0 ? void 0 : _a.id) || null,
19
- selectedModule: 'main',
20
- expandedModules: ['main', 'emission', 'shape', 'colorOverLifetime'],
21
- tool: 'select',
22
- viewMode: '3d',
23
- showGrid: true,
24
- showBounds: false,
25
- showVelocities: false,
26
- showEmitterShape: true,
27
- showForces: false,
28
- isPlaying: true,
29
- isPaused: false,
30
- playbackSpeed: 1,
31
- currentTime: 0,
32
- previewMode: 'looping',
33
- undoStack: [],
34
- redoStack: [],
35
- clipboard: null,
36
- };
11
+ return {
12
+ systems: systems || [particleEditorUtils.createDefaultSystem()],
13
+ selectedSystemId: systems?.[0]?.id || null,
14
+ selectedModule: "main",
15
+ expandedModules: ["main", "emission", "shape", "colorOverLifetime"],
16
+ tool: "select",
17
+ viewMode: "3d",
18
+ showGrid: true,
19
+ showBounds: false,
20
+ showVelocities: false,
21
+ showEmitterShape: true,
22
+ showForces: false,
23
+ isPlaying: true,
24
+ isPaused: false,
25
+ playbackSpeed: 1,
26
+ currentTime: 0,
27
+ previewMode: "looping",
28
+ undoStack: [],
29
+ redoStack: [],
30
+ clipboard: null
31
+ };
37
32
  }
38
33
  function pushToUndoStack(state) {
39
- return {
40
- ...state,
41
- undoStack: [...state.undoStack.slice(-49), state.systems],
42
- redoStack: [],
43
- };
34
+ return {
35
+ ...state,
36
+ undoStack: [...state.undoStack.slice(-49), state.systems],
37
+ redoStack: []
38
+ };
44
39
  }
45
40
  function particleEditorReducer(state, action) {
46
- var _a;
47
- switch (action.type) {
48
- case 'ADD_SYSTEM': {
49
- const newState = pushToUndoStack(state);
50
- return {
51
- ...newState,
52
- systems: [...newState.systems, action.system],
53
- selectedSystemId: action.system.id,
54
- };
55
- }
56
- case 'REMOVE_SYSTEM': {
57
- if (state.systems.length <= 1) {
58
- return state;
59
- }
60
- const newState = pushToUndoStack(state);
61
- const newSystems = newState.systems.filter((s) => s.id !== action.systemId);
62
- return {
63
- ...newState,
64
- systems: newSystems,
65
- selectedSystemId: newState.selectedSystemId === action.systemId
66
- ? ((_a = newSystems[0]) === null || _a === void 0 ? void 0 : _a.id) || null
67
- : newState.selectedSystemId,
68
- };
69
- }
70
- case 'UPDATE_SYSTEM': {
71
- const newState = pushToUndoStack(state);
72
- return {
73
- ...newState,
74
- systems: newState.systems.map((s) => s.id === action.systemId ? { ...s, ...action.updates } : s),
75
- };
76
- }
77
- case 'DUPLICATE_SYSTEM': {
78
- const system = state.systems.find((s) => s.id === action.systemId);
79
- if (!system) {
80
- return state;
41
+ switch (action.type) {
42
+ case "ADD_SYSTEM": {
43
+ const newState = pushToUndoStack(state);
44
+ return {
45
+ ...newState,
46
+ systems: [...newState.systems, action.system],
47
+ selectedSystemId: action.system.id
48
+ };
49
+ }
50
+ case "REMOVE_SYSTEM": {
51
+ if (state.systems.length <= 1) {
52
+ return state;
53
+ }
54
+ const newState = pushToUndoStack(state);
55
+ const newSystems = newState.systems.filter((s) => s.id !== action.systemId);
56
+ return {
57
+ ...newState,
58
+ systems: newSystems,
59
+ selectedSystemId: newState.selectedSystemId === action.systemId ? newSystems[0]?.id || null : newState.selectedSystemId
60
+ };
61
+ }
62
+ case "UPDATE_SYSTEM": {
63
+ const newState = pushToUndoStack(state);
64
+ return {
65
+ ...newState,
66
+ systems: newState.systems.map(
67
+ (s) => s.id === action.systemId ? { ...s, ...action.updates } : s
68
+ )
69
+ };
70
+ }
71
+ case "DUPLICATE_SYSTEM": {
72
+ const system = state.systems.find((s) => s.id === action.systemId);
73
+ if (!system) {
74
+ return state;
75
+ }
76
+ const cloned = particleEditorUtils.cloneSystem(system);
77
+ const newState = pushToUndoStack(state);
78
+ return {
79
+ ...newState,
80
+ systems: [...newState.systems, cloned],
81
+ selectedSystemId: cloned.id
82
+ };
83
+ }
84
+ case "SELECT_SYSTEM":
85
+ return { ...state, selectedSystemId: action.systemId };
86
+ case "SELECT_MODULE":
87
+ return { ...state, selectedModule: action.module };
88
+ case "TOGGLE_MODULE":
89
+ return {
90
+ ...state,
91
+ expandedModules: state.expandedModules.includes(action.module) ? state.expandedModules.filter((m) => m !== action.module) : [...state.expandedModules, action.module]
92
+ };
93
+ case "SET_TOOL":
94
+ return { ...state, tool: action.tool };
95
+ case "SET_VIEW_MODE":
96
+ return { ...state, viewMode: action.mode };
97
+ case "TOGGLE_GRID":
98
+ return { ...state, showGrid: !state.showGrid };
99
+ case "TOGGLE_BOUNDS":
100
+ return { ...state, showBounds: !state.showBounds };
101
+ case "TOGGLE_VELOCITIES":
102
+ return { ...state, showVelocities: !state.showVelocities };
103
+ case "TOGGLE_EMITTER_SHAPE":
104
+ return { ...state, showEmitterShape: !state.showEmitterShape };
105
+ case "TOGGLE_FORCES":
106
+ return { ...state, showForces: !state.showForces };
107
+ case "PLAY":
108
+ return { ...state, isPlaying: true, isPaused: false };
109
+ case "PAUSE":
110
+ return { ...state, isPaused: !state.isPaused };
111
+ case "STOP":
112
+ return { ...state, isPlaying: false, isPaused: false, currentTime: 0 };
113
+ case "RESTART":
114
+ return { ...state, currentTime: 0, isPlaying: true, isPaused: false };
115
+ case "SET_PLAYBACK_SPEED":
116
+ return { ...state, playbackSpeed: action.speed };
117
+ case "SET_CURRENT_TIME":
118
+ return { ...state, currentTime: action.time };
119
+ case "SET_PREVIEW_MODE":
120
+ return { ...state, previewMode: action.mode };
121
+ case "UNDO": {
122
+ if (state.undoStack.length === 0) {
123
+ return state;
124
+ }
125
+ const previous = state.undoStack[state.undoStack.length - 1];
126
+ return {
127
+ ...state,
128
+ systems: previous,
129
+ undoStack: state.undoStack.slice(0, -1),
130
+ redoStack: [...state.redoStack, state.systems]
131
+ };
132
+ }
133
+ case "REDO": {
134
+ if (state.redoStack.length === 0) {
135
+ return state;
136
+ }
137
+ const next = state.redoStack[state.redoStack.length - 1];
138
+ return {
139
+ ...state,
140
+ systems: next,
141
+ undoStack: [...state.undoStack, state.systems],
142
+ redoStack: state.redoStack.slice(0, -1)
143
+ };
144
+ }
145
+ case "COPY_SYSTEM": {
146
+ const system = state.systems.find((s) => s.id === action.systemId);
147
+ if (!system) {
148
+ return state;
149
+ }
150
+ return { ...state, clipboard: { ...system } };
151
+ }
152
+ case "COPY_MODULE": {
153
+ const system = state.systems.find((s) => s.id === action.systemId);
154
+ if (!system) {
155
+ return state;
156
+ }
157
+ const moduleData = system[action.moduleKey];
158
+ if (!moduleData) {
159
+ return state;
160
+ }
161
+ return { ...state, clipboard: { [action.moduleKey]: moduleData } };
162
+ }
163
+ case "PASTE_SYSTEM": {
164
+ if (!state.clipboard || !state.clipboard.id) {
165
+ return state;
166
+ }
167
+ const pasted = particleEditorUtils.cloneSystem(state.clipboard);
168
+ const newState = pushToUndoStack(state);
169
+ return {
170
+ ...newState,
171
+ systems: [...newState.systems, pasted],
172
+ selectedSystemId: pasted.id
173
+ };
174
+ }
175
+ case "PASTE_MODULE": {
176
+ if (!state.clipboard || state.clipboard.id) {
177
+ return state;
178
+ }
179
+ const newState = pushToUndoStack(state);
180
+ return {
181
+ ...newState,
182
+ systems: newState.systems.map(
183
+ (s) => s.id === action.systemId ? { ...s, ...state.clipboard } : s
184
+ )
185
+ };
186
+ }
187
+ case "ADD_FORCE": {
188
+ const newState = pushToUndoStack(state);
189
+ return {
190
+ ...newState,
191
+ systems: newState.systems.map(
192
+ (s) => s.id === action.systemId ? { ...s, externalForces: [...s.externalForces, action.force] } : s
193
+ )
194
+ };
195
+ }
196
+ case "REMOVE_FORCE": {
197
+ const newState = pushToUndoStack(state);
198
+ return {
199
+ ...newState,
200
+ systems: newState.systems.map(
201
+ (s) => s.id === action.systemId ? { ...s, externalForces: s.externalForces.filter((f) => f.id !== action.forceId) } : s
202
+ )
203
+ };
204
+ }
205
+ case "UPDATE_FORCE": {
206
+ const newState = pushToUndoStack(state);
207
+ return {
208
+ ...newState,
209
+ systems: newState.systems.map(
210
+ (s) => s.id === action.systemId ? {
211
+ ...s,
212
+ externalForces: s.externalForces.map(
213
+ (f) => f.id === action.forceId ? { ...f, ...action.updates } : f
214
+ )
215
+ } : s
216
+ )
217
+ };
218
+ }
219
+ case "ADD_SUB_EMITTER": {
220
+ const newState = pushToUndoStack(state);
221
+ return {
222
+ ...newState,
223
+ systems: newState.systems.map(
224
+ (s) => s.id === action.systemId ? { ...s, subEmitters: [...s.subEmitters, action.subEmitter] } : s
225
+ )
226
+ };
227
+ }
228
+ case "REMOVE_SUB_EMITTER": {
229
+ const newState = pushToUndoStack(state);
230
+ return {
231
+ ...newState,
232
+ systems: newState.systems.map(
233
+ (s) => s.id === action.systemId ? { ...s, subEmitters: s.subEmitters.filter((se) => se.id !== action.subEmitterId) } : s
234
+ )
235
+ };
236
+ }
237
+ case "ADD_BURST": {
238
+ const newState = pushToUndoStack(state);
239
+ return {
240
+ ...newState,
241
+ systems: newState.systems.map(
242
+ (s) => s.id === action.systemId ? { ...s, emission: { ...s.emission, bursts: [...s.emission.bursts, action.burst] } } : s
243
+ )
244
+ };
245
+ }
246
+ case "REMOVE_BURST": {
247
+ const newState = pushToUndoStack(state);
248
+ return {
249
+ ...newState,
250
+ systems: newState.systems.map(
251
+ (s) => s.id === action.systemId ? {
252
+ ...s,
253
+ emission: {
254
+ ...s.emission,
255
+ bursts: s.emission.bursts.filter((_, i) => i !== action.burstIndex)
81
256
  }
82
- const cloned = particleEditorUtils.cloneSystem(system);
83
- const newState = pushToUndoStack(state);
84
- return {
85
- ...newState,
86
- systems: [...newState.systems, cloned],
87
- selectedSystemId: cloned.id,
88
- };
257
+ } : s
258
+ )
259
+ };
260
+ }
261
+ case "LOAD_SYSTEMS": {
262
+ return {
263
+ ...createInitialState(action.systems),
264
+ undoStack: state.undoStack,
265
+ redoStack: state.redoStack
266
+ };
267
+ }
268
+ case "NEW_PROJECT":
269
+ return createInitialState();
270
+ default:
271
+ return state;
272
+ }
273
+ }
274
+ function useParticleEditor(initialSystems) {
275
+ const [state, dispatch] = React.useReducer(particleEditorReducer, initialSystems, createInitialState);
276
+ return { state, dispatch };
277
+ }
278
+ function SystemListPanel({
279
+ systems,
280
+ selectedId,
281
+ onSelect,
282
+ onAdd,
283
+ onRemove,
284
+ onDuplicate,
285
+ onRename,
286
+ onAddPreset
287
+ }) {
288
+ const { t } = i18n.useNiceTranslation();
289
+ const [editingId, setEditingId] = React.useState(null);
290
+ const [editName, setEditName] = React.useState("");
291
+ const [showPresets, setShowPresets] = React.useState(false);
292
+ const handleStartRename = (system) => {
293
+ setEditingId(system.id);
294
+ setEditName(system.name);
295
+ };
296
+ const handleFinishRename = () => {
297
+ if (editingId && editName.trim()) {
298
+ onRename(editingId, editName.trim());
299
+ }
300
+ setEditingId(null);
301
+ };
302
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.systemList, children: [
303
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.systemListHeader, children: [
304
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("particleEditor.systems", "Systems ({systems})").replace(
305
+ "{systems}",
306
+ String(systems.length)
307
+ ) }),
308
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.systemListActions, children: [
309
+ /* @__PURE__ */ jsxRuntime.jsx(
310
+ "button",
311
+ {
312
+ onClick: onAdd,
313
+ title: t("particleEditor.newSystem", "New System"),
314
+ "aria-label": t("particleEditor.newSystem", "New System"),
315
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "plus", size: 14 })
316
+ }
317
+ ),
318
+ /* @__PURE__ */ jsxRuntime.jsx(
319
+ "button",
320
+ {
321
+ onClick: () => setShowPresets(!showPresets),
322
+ title: t("particleEditor.addPreset", "Add Preset"),
323
+ "aria-label": t("particleEditor.addPreset", "Add Preset"),
324
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 14 })
325
+ }
326
+ )
327
+ ] })
328
+ ] }),
329
+ showPresets && /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.presetMenu, children: Object.keys(particleEditorUtils.PARTICLE_PRESETS).map((preset) => /* @__PURE__ */ jsxRuntime.jsx(
330
+ "button",
331
+ {
332
+ className: ParticleEditor_module.default.presetItem,
333
+ onClick: () => {
334
+ onAddPreset(preset);
335
+ setShowPresets(false);
336
+ },
337
+ children: preset.charAt(0).toUpperCase() + preset.slice(1)
338
+ },
339
+ preset
340
+ )) }),
341
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.systemItems, children: systems.map((system) => /* @__PURE__ */ jsxRuntime.jsx(
342
+ "div",
343
+ {
344
+ role: "button",
345
+ tabIndex: 0,
346
+ onKeyDown: ui.clickOnActivationKey,
347
+ className: `${ParticleEditor_module.default.systemItem} ${selectedId === system.id ? ParticleEditor_module.default.selected : ""}`,
348
+ onClick: () => onSelect(system.id),
349
+ children: editingId === system.id ? /* @__PURE__ */ jsxRuntime.jsx(
350
+ "input",
351
+ {
352
+ type: "text",
353
+ value: editName,
354
+ onChange: (e) => setEditName(e.target.value),
355
+ onBlur: handleFinishRename,
356
+ onKeyDown: (e) => e.key === "Enter" && handleFinishRename(),
357
+ autoFocus: true,
358
+ className: ParticleEditor_module.default.renameInput
359
+ }
360
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
361
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: ParticleEditor_module.default.systemName, onDoubleClick: () => handleStartRename(system), children: system.name }),
362
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.systemItemActions, children: [
363
+ /* @__PURE__ */ jsxRuntime.jsx(
364
+ "button",
365
+ {
366
+ onClick: (e) => {
367
+ e.stopPropagation();
368
+ onDuplicate(system.id);
369
+ },
370
+ title: t("particleEditor.duplicate", "Duplicate"),
371
+ "aria-label": t("particleEditor.duplicate", "Duplicate"),
372
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 14 })
373
+ }
374
+ ),
375
+ /* @__PURE__ */ jsxRuntime.jsx(
376
+ "button",
377
+ {
378
+ onClick: (e) => {
379
+ e.stopPropagation();
380
+ onRemove(system.id);
381
+ },
382
+ title: t("particleEditor.remove", "Remove"),
383
+ "aria-label": t("particleEditor.remove", "Remove"),
384
+ disabled: systems.length <= 1,
385
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "trash", size: 14 })
386
+ }
387
+ )
388
+ ] })
389
+ ] })
390
+ },
391
+ system.id
392
+ )) })
393
+ ] });
394
+ }
395
+ function InspectorPanel({
396
+ system,
397
+ expandedModules,
398
+ selectedModule,
399
+ onToggleModule,
400
+ onSelectModule: _onSelectModule,
401
+ onUpdateSystem
402
+ }) {
403
+ const { t } = i18n.useNiceTranslation();
404
+ if (!system) {
405
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.inspector, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.emptyInspector, children: t("particleEditor.noSystemSelected", "No system selected") }) });
406
+ }
407
+ const modules = [
408
+ { key: "main", hasEnabled: false },
409
+ { key: "emission", hasEnabled: true, enabled: system.emission.enabled },
410
+ { key: "shape", hasEnabled: false },
411
+ { key: "velocityOverLifetime", hasEnabled: true, enabled: system.velocityOverLifetime.enabled },
412
+ {
413
+ key: "limitVelocityOverLifetime",
414
+ hasEnabled: true,
415
+ enabled: system.limitVelocityOverLifetime.enabled
416
+ },
417
+ { key: "inheritVelocity", hasEnabled: true, enabled: system.inheritVelocity.enabled },
418
+ { key: "forceOverLifetime", hasEnabled: true, enabled: system.forceOverLifetime.enabled },
419
+ { key: "colorOverLifetime", hasEnabled: true, enabled: system.colorOverLifetime.enabled },
420
+ { key: "colorBySpeed", hasEnabled: true, enabled: system.colorBySpeed.enabled },
421
+ { key: "sizeOverLifetime", hasEnabled: true, enabled: system.sizeOverLifetime.enabled },
422
+ { key: "sizeBySpeed", hasEnabled: true, enabled: system.sizeBySpeed.enabled },
423
+ { key: "rotationOverLifetime", hasEnabled: true, enabled: system.rotationOverLifetime.enabled },
424
+ { key: "rotationBySpeed", hasEnabled: true, enabled: system.rotationBySpeed.enabled },
425
+ { key: "externalForces", hasEnabled: false },
426
+ { key: "noise", hasEnabled: true, enabled: system.noise.enabled },
427
+ { key: "collision", hasEnabled: true, enabled: system.collision.enabled },
428
+ { key: "triggers", hasEnabled: true, enabled: system.triggers.enabled },
429
+ { key: "subEmitters", hasEnabled: false },
430
+ {
431
+ key: "textureSheetAnimation",
432
+ hasEnabled: true,
433
+ enabled: system.textureSheetAnimation.enabled
434
+ },
435
+ { key: "lights", hasEnabled: true, enabled: system.lights.enabled },
436
+ { key: "trails", hasEnabled: false },
437
+ { key: "renderer", hasEnabled: false },
438
+ { key: "material", hasEnabled: false }
439
+ ];
440
+ const handleModuleToggle = (key, enabled) => {
441
+ const current = system[key];
442
+ onUpdateSystem({ [key]: { ...current, enabled } });
443
+ };
444
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.inspector, children: [
445
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.inspectorHeader, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: system.name }) }),
446
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.inspectorModules, children: modules.map((mod) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.moduleSection, children: [
447
+ /* @__PURE__ */ jsxRuntime.jsxs(
448
+ "div",
449
+ {
450
+ role: "button",
451
+ tabIndex: 0,
452
+ "aria-expanded": expandedModules.includes(mod.key),
453
+ onKeyDown: ui.clickOnActivationKey,
454
+ className: `${ParticleEditor_module.default.moduleHeader} ${selectedModule === mod.key ? ParticleEditor_module.default.selected : ""}`,
455
+ onClick: () => onToggleModule(mod.key),
456
+ children: [
457
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: ParticleEditor_module.default.moduleIcon, children: /* @__PURE__ */ jsxRuntime.jsx(
458
+ ui.NiceIcon,
459
+ {
460
+ name: expandedModules.includes(mod.key) ? "chevron-down" : "chevron-right",
461
+ size: 14
462
+ }
463
+ ) }),
464
+ mod.hasEnabled && /* @__PURE__ */ jsxRuntime.jsx(
465
+ "input",
466
+ {
467
+ type: "checkbox",
468
+ checked: mod.enabled,
469
+ onChange: (e) => {
470
+ e.stopPropagation();
471
+ handleModuleToggle(mod.key, e.target.checked);
472
+ },
473
+ className: ParticleEditor_module.default.moduleCheckbox
474
+ }
475
+ ),
476
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: ParticleEditor_module.default.moduleName, children: particleEditorTypes.MODULE_NAMES[mod.key] || mod.key })
477
+ ]
89
478
  }
90
- case 'SELECT_SYSTEM':
91
- return { ...state, selectedSystemId: action.systemId };
92
- case 'SELECT_MODULE':
93
- return { ...state, selectedModule: action.module };
94
- case 'TOGGLE_MODULE':
95
- return {
96
- ...state,
97
- expandedModules: state.expandedModules.includes(action.module)
98
- ? state.expandedModules.filter((m) => m !== action.module)
99
- : [...state.expandedModules, action.module],
100
- };
101
- case 'SET_TOOL':
102
- return { ...state, tool: action.tool };
103
- case 'SET_VIEW_MODE':
104
- return { ...state, viewMode: action.mode };
105
- case 'TOGGLE_GRID':
106
- return { ...state, showGrid: !state.showGrid };
107
- case 'TOGGLE_BOUNDS':
108
- return { ...state, showBounds: !state.showBounds };
109
- case 'TOGGLE_VELOCITIES':
110
- return { ...state, showVelocities: !state.showVelocities };
111
- case 'TOGGLE_EMITTER_SHAPE':
112
- return { ...state, showEmitterShape: !state.showEmitterShape };
113
- case 'TOGGLE_FORCES':
114
- return { ...state, showForces: !state.showForces };
115
- case 'PLAY':
116
- return { ...state, isPlaying: true, isPaused: false };
117
- case 'PAUSE':
118
- return { ...state, isPaused: !state.isPaused };
119
- case 'STOP':
120
- return { ...state, isPlaying: false, isPaused: false, currentTime: 0 };
121
- case 'RESTART':
122
- return { ...state, currentTime: 0, isPlaying: true, isPaused: false };
123
- case 'SET_PLAYBACK_SPEED':
124
- return { ...state, playbackSpeed: action.speed };
125
- case 'SET_CURRENT_TIME':
126
- return { ...state, currentTime: action.time };
127
- case 'SET_PREVIEW_MODE':
128
- return { ...state, previewMode: action.mode };
129
- case 'UNDO': {
130
- if (state.undoStack.length === 0) {
131
- return state;
479
+ ),
480
+ expandedModules.includes(mod.key) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.moduleContent, children: /* @__PURE__ */ jsxRuntime.jsx(ModuleEditor, { system, moduleKey: mod.key, onUpdate: onUpdateSystem }) })
481
+ ] }, mod.key)) })
482
+ ] });
483
+ }
484
+ function ModuleEditor({ system, moduleKey, onUpdate }) {
485
+ const { t } = i18n.useNiceTranslation();
486
+ const renderDistributionInput = (label, value, onChange) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.propertyRow, children: [
487
+ /* @__PURE__ */ jsxRuntime.jsx("label", { children: label }),
488
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.distributionInput, children: [
489
+ /* @__PURE__ */ jsxRuntime.jsxs(
490
+ "select",
491
+ {
492
+ value: value.type,
493
+ onChange: (e) => {
494
+ const newType = e.target.value;
495
+ if (newType === "constant") {
496
+ onChange({ type: "constant", value: value.type === "constant" ? value.value : 1 });
497
+ } else if (newType === "random") {
498
+ onChange({ type: "random", min: 0, max: 1 });
499
+ } else if (newType === "curve") {
500
+ onChange({
501
+ type: "curve",
502
+ curve: [
503
+ { time: 0, value: 1 },
504
+ { time: 1, value: 1 }
505
+ ]
506
+ });
132
507
  }
133
- const previous = state.undoStack[state.undoStack.length - 1];
134
- return {
135
- ...state,
136
- systems: previous,
137
- undoStack: state.undoStack.slice(0, -1),
138
- redoStack: [...state.redoStack, state.systems],
139
- };
508
+ },
509
+ className: ParticleEditor_module.default.distributionType,
510
+ children: [
511
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "constant", children: t("particleEditor.constant", "Constant") }),
512
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "random", children: t("particleEditor.random", "Random") }),
513
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "curve", children: t("particleEditor.curve", "Curve") })
514
+ ]
140
515
  }
141
- case 'REDO': {
142
- if (state.redoStack.length === 0) {
143
- return state;
144
- }
145
- const next = state.redoStack[state.redoStack.length - 1];
146
- return {
147
- ...state,
148
- systems: next,
149
- undoStack: [...state.undoStack, state.systems],
150
- redoStack: state.redoStack.slice(0, -1),
151
- };
516
+ ),
517
+ value.type === "constant" && /* @__PURE__ */ jsxRuntime.jsx(
518
+ "input",
519
+ {
520
+ type: "number",
521
+ value: value.value,
522
+ onChange: (e) => onChange({ ...value, value: parseFloat(e.target.value) || 0 }),
523
+ className: ParticleEditor_module.default.numberInput
152
524
  }
153
- case 'COPY_SYSTEM': {
154
- const system = state.systems.find((s) => s.id === action.systemId);
155
- if (!system) {
156
- return state;
525
+ ),
526
+ value.type === "random" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
527
+ /* @__PURE__ */ jsxRuntime.jsx(
528
+ "input",
529
+ {
530
+ type: "number",
531
+ value: value.min,
532
+ onChange: (e) => onChange({ ...value, min: parseFloat(e.target.value) || 0 }),
533
+ className: ParticleEditor_module.default.numberInput,
534
+ placeholder: t("particleEditor.min", "Min")
535
+ }
536
+ ),
537
+ /* @__PURE__ */ jsxRuntime.jsx(
538
+ "input",
539
+ {
540
+ type: "number",
541
+ value: value.max,
542
+ onChange: (e) => onChange({ ...value, max: parseFloat(e.target.value) || 0 }),
543
+ className: ParticleEditor_module.default.numberInput,
544
+ placeholder: t("particleEditor.max", "Max")
545
+ }
546
+ )
547
+ ] })
548
+ ] })
549
+ ] });
550
+ const renderNumberInput = (label, value, onChange, props) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.propertyRow, children: [
551
+ /* @__PURE__ */ jsxRuntime.jsx("label", { children: label }),
552
+ /* @__PURE__ */ jsxRuntime.jsx(
553
+ "input",
554
+ {
555
+ type: "number",
556
+ value,
557
+ onChange: (e) => onChange(parseFloat(e.target.value) || 0),
558
+ className: ParticleEditor_module.default.numberInput,
559
+ ...props
560
+ }
561
+ )
562
+ ] });
563
+ const renderToggle = (label, value, onChange) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.propertyRow, children: [
564
+ /* @__PURE__ */ jsxRuntime.jsx("label", { children: label }),
565
+ /* @__PURE__ */ jsxRuntime.jsx(
566
+ "input",
567
+ {
568
+ type: "checkbox",
569
+ checked: value,
570
+ onChange: (e) => onChange(e.target.checked),
571
+ className: ParticleEditor_module.default.toggleInput
572
+ }
573
+ )
574
+ ] });
575
+ const renderSelect = (label, value, options, onChange) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.propertyRow, children: [
576
+ /* @__PURE__ */ jsxRuntime.jsx("label", { children: label }),
577
+ /* @__PURE__ */ jsxRuntime.jsx(
578
+ "select",
579
+ {
580
+ value,
581
+ onChange: (e) => onChange(e.target.value),
582
+ className: ParticleEditor_module.default.selectInput,
583
+ children: Object.entries(options).map(([key, name]) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: key, children: name }, key))
584
+ }
585
+ )
586
+ ] });
587
+ switch (moduleKey) {
588
+ case "main":
589
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
590
+ renderNumberInput("Duration", system.duration, (v) => onUpdate({ duration: v }), {
591
+ min: 0,
592
+ step: 0.1
593
+ }),
594
+ renderToggle("Looping", system.looping, (v) => onUpdate({ looping: v })),
595
+ renderToggle("Prewarm", system.prewarm, (v) => onUpdate({ prewarm: v })),
596
+ renderDistributionInput(
597
+ "Start Delay",
598
+ system.startDelay,
599
+ (v) => onUpdate({ startDelay: v })
600
+ ),
601
+ renderDistributionInput(
602
+ "Start Lifetime",
603
+ system.startLifetime,
604
+ (v) => onUpdate({ startLifetime: v })
605
+ ),
606
+ renderDistributionInput(
607
+ "Start Speed",
608
+ system.startSpeed,
609
+ (v) => onUpdate({ startSpeed: v })
610
+ ),
611
+ renderDistributionInput(
612
+ "Start Size",
613
+ system.startSize,
614
+ (v) => onUpdate({ startSize: v })
615
+ ),
616
+ renderDistributionInput(
617
+ "Start Rotation",
618
+ system.startRotation,
619
+ (v) => onUpdate({ startRotation: v })
620
+ ),
621
+ renderDistributionInput(
622
+ "Gravity Modifier",
623
+ system.gravityModifier,
624
+ (v) => onUpdate({ gravityModifier: v })
625
+ ),
626
+ renderSelect(
627
+ "Simulation Space",
628
+ system.simulationSpace,
629
+ { local: "Local", world: "World", custom: "Custom" },
630
+ (v) => onUpdate({ simulationSpace: v })
631
+ ),
632
+ renderNumberInput(
633
+ "Simulation Speed",
634
+ system.simulationSpeed,
635
+ (v) => onUpdate({ simulationSpeed: v }),
636
+ { min: 0, step: 0.1 }
637
+ ),
638
+ renderNumberInput(
639
+ "Max Particles",
640
+ system.maxParticles,
641
+ (v) => onUpdate({ maxParticles: v }),
642
+ { min: 1, step: 1 }
643
+ )
644
+ ] });
645
+ case "emission":
646
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
647
+ renderDistributionInput(
648
+ "Rate over Time",
649
+ system.emission.rateOverTime,
650
+ (v) => onUpdate({ emission: { ...system.emission, rateOverTime: v } })
651
+ ),
652
+ renderDistributionInput(
653
+ "Rate over Distance",
654
+ system.emission.rateOverDistance,
655
+ (v) => onUpdate({ emission: { ...system.emission, rateOverDistance: v } })
656
+ ),
657
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.propertyRow, children: [
658
+ /* @__PURE__ */ jsxRuntime.jsx("label", { children: t("particleEditor.bursts", "Bursts ({bursts})").replace(
659
+ "{bursts}",
660
+ String(system.emission.bursts.length)
661
+ ) }),
662
+ /* @__PURE__ */ jsxRuntime.jsx(
663
+ "button",
664
+ {
665
+ className: ParticleEditor_module.default.addButton,
666
+ onClick: () => onUpdate({
667
+ emission: {
668
+ ...system.emission,
669
+ bursts: [
670
+ ...system.emission.bursts,
671
+ {
672
+ time: 0,
673
+ count: { type: "constant", value: 30 },
674
+ cycles: 1,
675
+ interval: 0.01,
676
+ probability: 1
677
+ }
678
+ ]
679
+ }
680
+ }),
681
+ children: t("particleEditor.addBurst", "Add Burst")
157
682
  }
158
- return { ...state, clipboard: { ...system } };
683
+ )
684
+ ] })
685
+ ] });
686
+ case "shape":
687
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
688
+ renderSelect(
689
+ "Shape",
690
+ system.shape.type,
691
+ particleEditorTypes.EMITTER_SHAPE_NAMES,
692
+ (v) => (
693
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
694
+ onUpdate({ shape: { ...system.shape, type: v } })
695
+ )
696
+ ),
697
+ renderToggle(
698
+ "Emit from Shell",
699
+ system.shape.emitFromShell,
700
+ (v) => onUpdate({ shape: { ...system.shape, emitFromShell: v } })
701
+ ),
702
+ renderToggle(
703
+ "Random Direction",
704
+ system.shape.randomDirection,
705
+ (v) => onUpdate({ shape: { ...system.shape, randomDirection: v } })
706
+ ),
707
+ (system.shape.type === "sphere" || system.shape.type === "hemisphere" || system.shape.type === "cone" || system.shape.type === "circle") && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderDistributionInput(
708
+ "Radius",
709
+ system.shape.radius,
710
+ (v) => (
711
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
712
+ onUpdate({ shape: { ...system.shape, radius: v } })
713
+ )
714
+ ) }),
715
+ system.shape.type === "cone" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
716
+ renderNumberInput(
717
+ "Angle",
718
+ system.shape.angle,
719
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
720
+ (v) => onUpdate({ shape: { ...system.shape, angle: v } }),
721
+ { min: 0, max: 90 }
722
+ ),
723
+ renderNumberInput(
724
+ "Length",
725
+ system.shape.length,
726
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
727
+ (v) => onUpdate({ shape: { ...system.shape, length: v } }),
728
+ { min: 0 }
729
+ )
730
+ ] })
731
+ ] });
732
+ case "renderer":
733
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
734
+ renderSelect(
735
+ "Render Mode",
736
+ system.renderer.type,
737
+ particleEditorTypes.RENDER_MODE_NAMES,
738
+ (v) => (
739
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
740
+ onUpdate({ renderer: { ...system.renderer, type: v } })
741
+ )
742
+ ),
743
+ renderSelect(
744
+ "Sort Mode",
745
+ system.renderer.sortMode,
746
+ {
747
+ none: "None",
748
+ by_distance: "By Distance",
749
+ oldest_in_front: "Oldest in Front",
750
+ youngest_in_front: "Youngest in Front"
751
+ },
752
+ (v) => onUpdate({ renderer: { ...system.renderer, sortMode: v } })
753
+ )
754
+ ] });
755
+ case "material":
756
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
757
+ renderSelect(
758
+ "Shading",
759
+ system.material.shading,
760
+ { unlit: "Unlit", lit: "Lit" },
761
+ (v) => onUpdate({ material: { ...system.material, shading: v } })
762
+ ),
763
+ renderSelect(
764
+ "Blend Mode",
765
+ system.material.blendMode,
766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- BLEND_MODE_NAMES is keyed by ParticleBlendMode but renderSelect is generic
767
+ particleEditorTypes.BLEND_MODE_NAMES,
768
+ (v) => onUpdate({ material: { ...system.material, blendMode: v } })
769
+ )
770
+ ] });
771
+ default:
772
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.moduleNotImplemented, children: t("particleEditor.moduleEditorNotYetImplemented", "Module editor not yet implemented") });
773
+ }
774
+ }
775
+ function Toolbar({
776
+ isPlaying,
777
+ isPaused,
778
+ playbackSpeed,
779
+ currentTime,
780
+ canUndo,
781
+ canRedo,
782
+ showGrid,
783
+ showBounds,
784
+ showEmitterShape,
785
+ viewMode,
786
+ onPlay,
787
+ onPause,
788
+ onStop,
789
+ onRestart,
790
+ onSpeedChange,
791
+ onUndo,
792
+ onRedo,
793
+ onToggleGrid,
794
+ onToggleBounds,
795
+ onToggleEmitterShape,
796
+ onViewModeChange,
797
+ onNew,
798
+ onExport,
799
+ onImport
800
+ }) {
801
+ const { t } = i18n.useNiceTranslation();
802
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.toolbar, children: [
803
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.toolbarGroup, children: [
804
+ /* @__PURE__ */ jsxRuntime.jsx(
805
+ "button",
806
+ {
807
+ onClick: onNew,
808
+ title: t("particleEditor.newProject", "New Project"),
809
+ "aria-label": t("particleEditor.newProject", "New Project"),
810
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "file", size: 16 })
159
811
  }
160
- case 'COPY_MODULE': {
161
- const system = state.systems.find((s) => s.id === action.systemId);
162
- if (!system) {
163
- return state;
164
- }
165
- const moduleData = system[action.moduleKey];
166
- if (!moduleData) {
167
- return state;
168
- }
169
- return { ...state, clipboard: { [action.moduleKey]: moduleData } };
812
+ ),
813
+ /* @__PURE__ */ jsxRuntime.jsx(
814
+ "button",
815
+ {
816
+ onClick: onImport,
817
+ title: t("particleEditor.import", "Import"),
818
+ "aria-label": t("particleEditor.import", "Import"),
819
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "folder", size: 16 })
170
820
  }
171
- case 'PASTE_SYSTEM': {
172
- if (!state.clipboard || !state.clipboard.id) {
173
- return state;
174
- }
175
- const pasted = particleEditorUtils.cloneSystem(state.clipboard);
176
- const newState = pushToUndoStack(state);
177
- return {
178
- ...newState,
179
- systems: [...newState.systems, pasted],
180
- selectedSystemId: pasted.id,
181
- };
821
+ ),
822
+ /* @__PURE__ */ jsxRuntime.jsx(
823
+ "button",
824
+ {
825
+ onClick: onExport,
826
+ title: t("particleEditor.export", "Export"),
827
+ "aria-label": t("particleEditor.export", "Export"),
828
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "save", size: 16 })
182
829
  }
183
- case 'PASTE_MODULE': {
184
- if (!state.clipboard || state.clipboard.id) {
185
- return state;
186
- }
187
- const newState = pushToUndoStack(state);
188
- return {
189
- ...newState,
190
- systems: newState.systems.map((s) => s.id === action.systemId ? { ...s, ...state.clipboard } : s),
191
- };
830
+ )
831
+ ] }),
832
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.toolbarSeparator }),
833
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.toolbarGroup, children: [
834
+ /* @__PURE__ */ jsxRuntime.jsx(
835
+ "button",
836
+ {
837
+ onClick: onUndo,
838
+ disabled: !canUndo,
839
+ title: t("particleEditor.undo", "Undo"),
840
+ "aria-label": t("particleEditor.undo", "Undo"),
841
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 })
192
842
  }
193
- case 'ADD_FORCE': {
194
- const newState = pushToUndoStack(state);
195
- return {
196
- ...newState,
197
- systems: newState.systems.map((s) => s.id === action.systemId
198
- ? { ...s, externalForces: [...s.externalForces, action.force] }
199
- : s),
200
- };
843
+ ),
844
+ /* @__PURE__ */ jsxRuntime.jsx(
845
+ "button",
846
+ {
847
+ onClick: onRedo,
848
+ disabled: !canRedo,
849
+ title: t("particleEditor.redo", "Redo"),
850
+ "aria-label": t("particleEditor.redo", "Redo"),
851
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 })
201
852
  }
202
- case 'REMOVE_FORCE': {
203
- const newState = pushToUndoStack(state);
204
- return {
205
- ...newState,
206
- systems: newState.systems.map((s) => s.id === action.systemId
207
- ? { ...s, externalForces: s.externalForces.filter((f) => f.id !== action.forceId) }
208
- : s),
209
- };
853
+ )
854
+ ] }),
855
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.toolbarSeparator }),
856
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.toolbarGroup, children: [
857
+ isPlaying && !isPaused ? /* @__PURE__ */ jsxRuntime.jsx(
858
+ "button",
859
+ {
860
+ onClick: onPause,
861
+ title: t("particleEditor.pause", "Pause"),
862
+ "aria-label": t("particleEditor.pause", "Pause"),
863
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "pause", size: 16 })
210
864
  }
211
- case 'UPDATE_FORCE': {
212
- const newState = pushToUndoStack(state);
213
- return {
214
- ...newState,
215
- systems: newState.systems.map((s) => s.id === action.systemId
216
- ? {
217
- ...s,
218
- externalForces: s.externalForces.map((f) => f.id === action.forceId ? { ...f, ...action.updates } : f),
219
- }
220
- : s),
221
- };
865
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
866
+ "button",
867
+ {
868
+ onClick: onPlay,
869
+ title: t("particleEditor.play", "Play"),
870
+ "aria-label": t("particleEditor.play", "Play"),
871
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "play", size: 16 })
222
872
  }
223
- case 'ADD_SUB_EMITTER': {
224
- const newState = pushToUndoStack(state);
225
- return {
226
- ...newState,
227
- systems: newState.systems.map((s) => s.id === action.systemId
228
- ? { ...s, subEmitters: [...s.subEmitters, action.subEmitter] }
229
- : s),
230
- };
873
+ ),
874
+ /* @__PURE__ */ jsxRuntime.jsx(
875
+ "button",
876
+ {
877
+ onClick: onStop,
878
+ title: t("particleEditor.stop", "Stop"),
879
+ "aria-label": t("particleEditor.stop", "Stop"),
880
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "stop", size: 16 })
231
881
  }
232
- case 'REMOVE_SUB_EMITTER': {
233
- const newState = pushToUndoStack(state);
234
- return {
235
- ...newState,
236
- systems: newState.systems.map((s) => s.id === action.systemId
237
- ? { ...s, subEmitters: s.subEmitters.filter((se) => se.id !== action.subEmitterId) }
238
- : s),
239
- };
882
+ ),
883
+ /* @__PURE__ */ jsxRuntime.jsx(
884
+ "button",
885
+ {
886
+ onClick: onRestart,
887
+ title: t("particleEditor.restart", "Restart"),
888
+ "aria-label": t("particleEditor.restart", "Restart"),
889
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "refresh", size: 16 })
240
890
  }
241
- case 'ADD_BURST': {
242
- const newState = pushToUndoStack(state);
243
- return {
244
- ...newState,
245
- systems: newState.systems.map((s) => s.id === action.systemId
246
- ? { ...s, emission: { ...s.emission, bursts: [...s.emission.bursts, action.burst] } }
247
- : s),
248
- };
891
+ ),
892
+ /* @__PURE__ */ jsxRuntime.jsxs(
893
+ "select",
894
+ {
895
+ value: playbackSpeed,
896
+ onChange: (e) => onSpeedChange(parseFloat(e.target.value)),
897
+ className: ParticleEditor_module.default.speedSelect,
898
+ title: t("particleEditor.playbackSpeed", "Playback Speed"),
899
+ children: [
900
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 0.25, children: "0.25x" }),
901
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 0.5, children: "0.5x" }),
902
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 1, children: "1x" }),
903
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 2, children: "2x" }),
904
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: 4, children: "4x" })
905
+ ]
249
906
  }
250
- case 'REMOVE_BURST': {
251
- const newState = pushToUndoStack(state);
252
- return {
253
- ...newState,
254
- systems: newState.systems.map((s) => s.id === action.systemId
255
- ? {
256
- ...s,
257
- emission: {
258
- ...s.emission,
259
- bursts: s.emission.bursts.filter((_, i) => i !== action.burstIndex),
260
- },
261
- }
262
- : s),
263
- };
907
+ ),
908
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: ParticleEditor_module.default.timeDisplay, children: [
909
+ currentTime.toFixed(2),
910
+ "s"
911
+ ] })
912
+ ] }),
913
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.toolbarSeparator }),
914
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ParticleEditor_module.default.toolbarGroup, children: [
915
+ /* @__PURE__ */ jsxRuntime.jsx(
916
+ "button",
917
+ {
918
+ onClick: onToggleGrid,
919
+ className: showGrid ? ParticleEditor_module.default.active : "",
920
+ title: t("particleEditor.toggleGrid", "Toggle Grid"),
921
+ "aria-label": t("particleEditor.toggleGrid", "Toggle Grid"),
922
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "hash", size: 16 })
264
923
  }
265
- case 'LOAD_SYSTEMS': {
266
- return {
267
- ...createInitialState(action.systems),
268
- undoStack: state.undoStack,
269
- redoStack: state.redoStack,
270
- };
924
+ ),
925
+ /* @__PURE__ */ jsxRuntime.jsx(
926
+ "button",
927
+ {
928
+ onClick: onToggleBounds,
929
+ className: showBounds ? ParticleEditor_module.default.active : "",
930
+ title: t("particleEditor.toggleBounds", "Toggle Bounds"),
931
+ "aria-label": t("particleEditor.toggleBounds", "Toggle Bounds"),
932
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "square", size: 16 })
271
933
  }
272
- case 'NEW_PROJECT':
273
- return createInitialState();
274
- default:
275
- return state;
276
- }
277
- }
278
- /* ═══════════════════════════════════════════════════════════════════════════
279
- Hook: useParticleEditor
280
- ═══════════════════════════════════════════════════════════════════════════ */
281
- function useParticleEditor(initialSystems) {
282
- const [state, dispatch] = React.useReducer(particleEditorReducer, initialSystems, createInitialState);
283
- return { state, dispatch };
284
- }
285
- function SystemListPanel({ systems, selectedId, onSelect, onAdd, onRemove, onDuplicate, onRename, onAddPreset, }) {
286
- const { t } = i18n.useNiceTranslation();
287
- const [editingId, setEditingId] = React.useState(null);
288
- const [editName, setEditName] = React.useState('');
289
- const [showPresets, setShowPresets] = React.useState(false);
290
- const handleStartRename = (system) => {
291
- setEditingId(system.id);
292
- setEditName(system.name);
293
- };
294
- const handleFinishRename = () => {
295
- if (editingId && editName.trim()) {
296
- onRename(editingId, editName.trim());
934
+ ),
935
+ /* @__PURE__ */ jsxRuntime.jsx(
936
+ "button",
937
+ {
938
+ onClick: onToggleEmitterShape,
939
+ className: showEmitterShape ? ParticleEditor_module.default.active : "",
940
+ title: t("particleEditor.toggleEmitterShape", "Toggle Emitter Shape"),
941
+ "aria-label": t("particleEditor.toggleEmitterShape", "Toggle Emitter Shape"),
942
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "target", size: 16 })
297
943
  }
298
- setEditingId(null);
299
- };
300
- return (jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemList, children: [jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemListHeader, children: [jsxRuntime.jsx("span", { children: t('particleEditor.systems', 'Systems ({systems})').replace('{systems}', String(systems.length)) }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemListActions, children: [jsxRuntime.jsx("button", { onClick: onAdd, title: t('particleEditor.newSystem', 'New System'), "aria-label": t('particleEditor.newSystem', 'New System'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "plus", size: 14 }) }), jsxRuntime.jsx("button", { onClick: () => setShowPresets(!showPresets), title: t('particleEditor.addPreset', 'Add Preset'), "aria-label": t('particleEditor.addPreset', 'Add Preset'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 14 }) })] })] }), showPresets && (jsxRuntime.jsx("div", { className: ParticleEditor_module.presetMenu, children: Object.keys(particleEditorUtils.PARTICLE_PRESETS).map((preset) => (jsxRuntime.jsx("button", { className: ParticleEditor_module.presetItem, onClick: () => {
301
- onAddPreset(preset);
302
- setShowPresets(false);
303
- }, children: preset.charAt(0).toUpperCase() + preset.slice(1) }, preset))) })), jsxRuntime.jsx("div", { className: ParticleEditor_module.systemItems, children: systems.map((system) => (jsxRuntime.jsx("div", { role: "button", tabIndex: 0, onKeyDown: ui.clickOnActivationKey, className: `${ParticleEditor_module.systemItem} ${selectedId === system.id ? ParticleEditor_module.selected : ''}`, onClick: () => onSelect(system.id), children: editingId === system.id ? (jsxRuntime.jsx("input", { type: "text", value: editName, onChange: (e) => setEditName(e.target.value), onBlur: handleFinishRename, onKeyDown: (e) => e.key === 'Enter' && handleFinishRename(), autoFocus: true, className: ParticleEditor_module.renameInput })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: ParticleEditor_module.systemName, onDoubleClick: () => handleStartRename(system), children: system.name }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.systemItemActions, children: [jsxRuntime.jsx("button", { onClick: (e) => {
304
- e.stopPropagation();
305
- onDuplicate(system.id);
306
- }, title: t('particleEditor.duplicate', 'Duplicate'), "aria-label": t('particleEditor.duplicate', 'Duplicate'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "clipboard", size: 14 }) }), jsxRuntime.jsx("button", { onClick: (e) => {
307
- e.stopPropagation();
308
- onRemove(system.id);
309
- }, title: t('particleEditor.remove', 'Remove'), "aria-label": t('particleEditor.remove', 'Remove'), disabled: systems.length <= 1, children: jsxRuntime.jsx(ui.NiceIcon, { name: "trash", size: 14 }) })] })] })) }, system.id))) })] }));
944
+ )
945
+ ] }),
946
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.toolbarSeparator }),
947
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.toolbarGroup, children: ["3d", "top", "side", "front"].map((mode) => /* @__PURE__ */ jsxRuntime.jsx(
948
+ "button",
949
+ {
950
+ onClick: () => onViewModeChange(mode),
951
+ className: viewMode === mode ? ParticleEditor_module.default.active : "",
952
+ title: mode.toUpperCase(),
953
+ children: mode === "3d" ? /* @__PURE__ */ jsxRuntime.jsx(ui.NiceIcon, { name: "video", size: 16 }) : mode.charAt(0).toUpperCase()
954
+ },
955
+ mode
956
+ )) })
957
+ ] });
310
958
  }
311
- function InspectorPanel({ system, expandedModules, selectedModule, onToggleModule, onSelectModule: _onSelectModule, onUpdateSystem, }) {
312
- const { t } = i18n.useNiceTranslation();
313
- if (!system) {
314
- return (jsxRuntime.jsx("div", { className: ParticleEditor_module.inspector, children: jsxRuntime.jsx("div", { className: ParticleEditor_module.emptyInspector, children: t('particleEditor.noSystemSelected', 'No system selected') }) }));
959
+ function PreviewPanel({
960
+ systems,
961
+ isPlaying,
962
+ isPaused,
963
+ currentTime,
964
+ showGrid,
965
+ showEmitterShape
966
+ }) {
967
+ const canvasRef = React.useRef(null);
968
+ React.useEffect(() => {
969
+ const canvas = canvasRef.current;
970
+ if (!canvas) {
971
+ return;
315
972
  }
316
- const modules = [
317
- { key: 'main', hasEnabled: false },
318
- { key: 'emission', hasEnabled: true, enabled: system.emission.enabled },
319
- { key: 'shape', hasEnabled: false },
320
- { key: 'velocityOverLifetime', hasEnabled: true, enabled: system.velocityOverLifetime.enabled },
321
- {
322
- key: 'limitVelocityOverLifetime',
323
- hasEnabled: true,
324
- enabled: system.limitVelocityOverLifetime.enabled,
325
- },
326
- { key: 'inheritVelocity', hasEnabled: true, enabled: system.inheritVelocity.enabled },
327
- { key: 'forceOverLifetime', hasEnabled: true, enabled: system.forceOverLifetime.enabled },
328
- { key: 'colorOverLifetime', hasEnabled: true, enabled: system.colorOverLifetime.enabled },
329
- { key: 'colorBySpeed', hasEnabled: true, enabled: system.colorBySpeed.enabled },
330
- { key: 'sizeOverLifetime', hasEnabled: true, enabled: system.sizeOverLifetime.enabled },
331
- { key: 'sizeBySpeed', hasEnabled: true, enabled: system.sizeBySpeed.enabled },
332
- { key: 'rotationOverLifetime', hasEnabled: true, enabled: system.rotationOverLifetime.enabled },
333
- { key: 'rotationBySpeed', hasEnabled: true, enabled: system.rotationBySpeed.enabled },
334
- { key: 'externalForces', hasEnabled: false },
335
- { key: 'noise', hasEnabled: true, enabled: system.noise.enabled },
336
- { key: 'collision', hasEnabled: true, enabled: system.collision.enabled },
337
- { key: 'triggers', hasEnabled: true, enabled: system.triggers.enabled },
338
- { key: 'subEmitters', hasEnabled: false },
339
- {
340
- key: 'textureSheetAnimation',
341
- hasEnabled: true,
342
- enabled: system.textureSheetAnimation.enabled,
343
- },
344
- { key: 'lights', hasEnabled: true, enabled: system.lights.enabled },
345
- { key: 'trails', hasEnabled: false },
346
- { key: 'renderer', hasEnabled: false },
347
- { key: 'material', hasEnabled: false },
348
- ];
349
- const handleModuleToggle = (key, enabled) => {
350
- const current = system[key];
351
- onUpdateSystem({ [key]: { ...current, enabled } });
352
- };
353
- return (jsxRuntime.jsxs("div", { className: ParticleEditor_module.inspector, children: [jsxRuntime.jsx("div", { className: ParticleEditor_module.inspectorHeader, children: jsxRuntime.jsx("span", { children: system.name }) }), jsxRuntime.jsx("div", { className: ParticleEditor_module.inspectorModules, children: modules.map((mod) => (jsxRuntime.jsxs("div", { className: ParticleEditor_module.moduleSection, children: [jsxRuntime.jsxs("div", { role: "button", tabIndex: 0, "aria-expanded": expandedModules.includes(mod.key), onKeyDown: ui.clickOnActivationKey, className: `${ParticleEditor_module.moduleHeader} ${selectedModule === mod.key ? ParticleEditor_module.selected : ''}`, onClick: () => onToggleModule(mod.key), children: [jsxRuntime.jsx("span", { className: ParticleEditor_module.moduleIcon, children: jsxRuntime.jsx(ui.NiceIcon, { name: expandedModules.includes(mod.key) ? 'chevron-down' : 'chevron-right', size: 14 }) }), mod.hasEnabled && (jsxRuntime.jsx("input", { type: "checkbox", checked: mod.enabled, onChange: (e) => {
354
- e.stopPropagation();
355
- handleModuleToggle(mod.key, e.target.checked);
356
- }, className: ParticleEditor_module.moduleCheckbox })), jsxRuntime.jsx("span", { className: ParticleEditor_module.moduleName, children: particleEditorTypes.MODULE_NAMES[mod.key] || mod.key })] }), expandedModules.includes(mod.key) && (jsxRuntime.jsx("div", { className: ParticleEditor_module.moduleContent, children: jsxRuntime.jsx(ModuleEditor, { system: system, moduleKey: mod.key, onUpdate: onUpdateSystem }) }))] }, mod.key))) })] }));
357
- }
358
- function ModuleEditor({ system, moduleKey, onUpdate }) {
359
- const { t } = i18n.useNiceTranslation();
360
- const renderDistributionInput = (label, value, onChange) => (jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsx("label", { children: label }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.distributionInput, children: [jsxRuntime.jsxs("select", { value: value.type, onChange: (e) => {
361
- const newType = e.target.value;
362
- if (newType === 'constant') {
363
- onChange({ type: 'constant', value: value.type === 'constant' ? value.value : 1 });
364
- }
365
- else if (newType === 'random') {
366
- onChange({ type: 'random', min: 0, max: 1 });
367
- }
368
- else if (newType === 'curve') {
369
- onChange({
370
- type: 'curve',
371
- curve: [
372
- { time: 0, value: 1 },
373
- { time: 1, value: 1 },
374
- ],
375
- });
376
- }
377
- }, className: ParticleEditor_module.distributionType, children: [jsxRuntime.jsx("option", { value: "constant", children: t('particleEditor.constant', 'Constant') }), jsxRuntime.jsx("option", { value: "random", children: t('particleEditor.random', 'Random') }), jsxRuntime.jsx("option", { value: "curve", children: t('particleEditor.curve', 'Curve') })] }), value.type === 'constant' && (jsxRuntime.jsx("input", { type: "number", value: value.value, onChange: (e) => onChange({ ...value, value: parseFloat(e.target.value) || 0 }), className: ParticleEditor_module.numberInput })), value.type === 'random' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { type: "number", value: value.min, onChange: (e) => onChange({ ...value, min: parseFloat(e.target.value) || 0 }), className: ParticleEditor_module.numberInput, placeholder: t('particleEditor.min', 'Min') }), jsxRuntime.jsx("input", { type: "number", value: value.max, onChange: (e) => onChange({ ...value, max: parseFloat(e.target.value) || 0 }), className: ParticleEditor_module.numberInput, placeholder: t('particleEditor.max', 'Max') })] }))] })] }));
378
- const renderNumberInput = (label, value, onChange, props) => (jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsx("label", { children: label }), jsxRuntime.jsx("input", { type: "number", value: value, onChange: (e) => onChange(parseFloat(e.target.value) || 0), className: ParticleEditor_module.numberInput, ...props })] }));
379
- const renderToggle = (label, value, onChange) => (jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsx("label", { children: label }), jsxRuntime.jsx("input", { type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked), className: ParticleEditor_module.toggleInput })] }));
380
- const renderSelect = (label, value, options, onChange) => (jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsx("label", { children: label }), jsxRuntime.jsx("select", { value: value, onChange: (e) => onChange(e.target.value), className: ParticleEditor_module.selectInput, children: Object.entries(options).map(([key, name]) => (jsxRuntime.jsx("option", { value: key, children: name }, key))) })] }));
381
- switch (moduleKey) {
382
- case 'main':
383
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderNumberInput('Duration', system.duration, (v) => onUpdate({ duration: v }), {
384
- min: 0,
385
- step: 0.1,
386
- }), renderToggle('Looping', system.looping, (v) => onUpdate({ looping: v })), renderToggle('Prewarm', system.prewarm, (v) => onUpdate({ prewarm: v })), renderDistributionInput('Start Delay', system.startDelay, (v) => onUpdate({ startDelay: v })), renderDistributionInput('Start Lifetime', system.startLifetime, (v) => onUpdate({ startLifetime: v })), renderDistributionInput('Start Speed', system.startSpeed, (v) => onUpdate({ startSpeed: v })), renderDistributionInput('Start Size', system.startSize, (v) => onUpdate({ startSize: v })), renderDistributionInput('Start Rotation', system.startRotation, (v) => onUpdate({ startRotation: v })), renderDistributionInput('Gravity Modifier', system.gravityModifier, (v) => onUpdate({ gravityModifier: v })), renderSelect('Simulation Space', system.simulationSpace, { local: 'Local', world: 'World', custom: 'Custom' }, (v) => onUpdate({ simulationSpace: v })), renderNumberInput('Simulation Speed', system.simulationSpeed, (v) => onUpdate({ simulationSpeed: v }), { min: 0, step: 0.1 }), renderNumberInput('Max Particles', system.maxParticles, (v) => onUpdate({ maxParticles: v }), { min: 1, step: 1 })] }));
387
- case 'emission':
388
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderDistributionInput('Rate over Time', system.emission.rateOverTime, (v) => onUpdate({ emission: { ...system.emission, rateOverTime: v } })), renderDistributionInput('Rate over Distance', system.emission.rateOverDistance, (v) => onUpdate({ emission: { ...system.emission, rateOverDistance: v } })), jsxRuntime.jsxs("div", { className: ParticleEditor_module.propertyRow, children: [jsxRuntime.jsx("label", { children: t('particleEditor.bursts', 'Bursts ({bursts})').replace('{bursts}', String(system.emission.bursts.length)) }), jsxRuntime.jsx("button", { className: ParticleEditor_module.addButton, onClick: () => onUpdate({
389
- emission: {
390
- ...system.emission,
391
- bursts: [
392
- ...system.emission.bursts,
393
- {
394
- time: 0,
395
- count: { type: 'constant', value: 30 },
396
- cycles: 1,
397
- interval: 0.01,
398
- probability: 1,
399
- },
400
- ],
401
- },
402
- }), children: t('particleEditor.addBurst', 'Add Burst') })] })] }));
403
- case 'shape':
404
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderSelect('Shape', system.shape.type, particleEditorTypes.EMITTER_SHAPE_NAMES, (v) =>
405
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
406
- onUpdate({ shape: { ...system.shape, type: v } })), renderToggle('Emit from Shell', system.shape.emitFromShell, (v) => onUpdate({ shape: { ...system.shape, emitFromShell: v } })), renderToggle('Random Direction', system.shape.randomDirection, (v) => onUpdate({ shape: { ...system.shape, randomDirection: v } })), (system.shape.type === 'sphere' ||
407
- system.shape.type === 'hemisphere' ||
408
- system.shape.type === 'cone' ||
409
- system.shape.type === 'circle') && (jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderDistributionInput('Radius', system.shape.radius, (v) =>
410
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
411
- onUpdate({ shape: { ...system.shape, radius: v } })) })), system.shape.type === 'cone' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderNumberInput('Angle', system.shape.angle,
412
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
413
- (v) => onUpdate({ shape: { ...system.shape, angle: v } }), { min: 0, max: 90 }), renderNumberInput('Length', system.shape.length,
414
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
415
- (v) => onUpdate({ shape: { ...system.shape, length: v } }), { min: 0 })] }))] }));
416
- case 'renderer':
417
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderSelect('Render Mode', system.renderer.type, particleEditorTypes.RENDER_MODE_NAMES, (v) =>
418
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- writing into discriminated-union variant; runtime narrowed by `type`
419
- onUpdate({ renderer: { ...system.renderer, type: v } })), renderSelect('Sort Mode', system.renderer.sortMode, {
420
- none: 'None',
421
- by_distance: 'By Distance',
422
- oldest_in_front: 'Oldest in Front',
423
- youngest_in_front: 'Youngest in Front',
424
- }, (v) => onUpdate({ renderer: { ...system.renderer, sortMode: v } }))] }));
425
- case 'material':
426
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderSelect('Shading', system.material.shading, { unlit: 'Unlit', lit: 'Lit' }, (v) => onUpdate({ material: { ...system.material, shading: v } })), renderSelect('Blend Mode', system.material.blendMode,
427
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- BLEND_MODE_NAMES is keyed by ParticleBlendMode but renderSelect is generic
428
- particleEditorTypes.BLEND_MODE_NAMES, (v) => onUpdate({ material: { ...system.material, blendMode: v } }))] }));
429
- default:
430
- return (jsxRuntime.jsx("div", { className: ParticleEditor_module.moduleNotImplemented, children: t('particleEditor.moduleEditorNotYetImplemented', 'Module editor not yet implemented') }));
973
+ const ctx = canvas.getContext("2d");
974
+ if (!ctx) {
975
+ return;
431
976
  }
977
+ const width = canvas.width;
978
+ const height = canvas.height;
979
+ const centerX = width / 2;
980
+ const centerY = height / 2;
981
+ ctx.fillStyle = ui.resolveCssColor("var(--nice-bg, #1a1a1a)", ctx.canvas);
982
+ ctx.fillRect(0, 0, width, height);
983
+ if (showGrid) {
984
+ ctx.strokeStyle = ui.resolveCssColor(
985
+ "var(--nice-overlay-light-10, rgba(255, 255, 255, 0.1))",
986
+ ctx.canvas
987
+ );
988
+ ctx.lineWidth = 1;
989
+ const gridSize = 40;
990
+ for (let x = 0; x < width; x += gridSize) {
991
+ ctx.beginPath();
992
+ ctx.moveTo(x, 0);
993
+ ctx.lineTo(x, height);
994
+ ctx.stroke();
995
+ }
996
+ for (let y = 0; y < height; y += gridSize) {
997
+ ctx.beginPath();
998
+ ctx.moveTo(0, y);
999
+ ctx.lineTo(width, y);
1000
+ ctx.stroke();
1001
+ }
1002
+ }
1003
+ if (showEmitterShape && systems.length > 0) {
1004
+ ctx.strokeStyle = ui.resolveCssColor(
1005
+ "var(--nice-primary-tint-60, rgba(0,200,255,0.5))",
1006
+ ctx.canvas
1007
+ );
1008
+ ctx.lineWidth = 2;
1009
+ ctx.setLineDash([5, 5]);
1010
+ ctx.beginPath();
1011
+ ctx.arc(centerX, centerY, 30, 0, Math.PI * 2);
1012
+ ctx.stroke();
1013
+ ctx.setLineDash([]);
1014
+ }
1015
+ if (isPlaying && !isPaused) {
1016
+ const particleCount = 50;
1017
+ for (let i = 0; i < particleCount; i++) {
1018
+ const seed = i / particleCount;
1019
+ const angle = seed * Math.PI * 2 + currentTime;
1020
+ const distance = 30 + (currentTime * 50 + i * 10) % 150;
1021
+ const x = centerX + Math.cos(angle) * distance;
1022
+ const y = centerY - Math.sin(angle) * distance * 0.5 - (currentTime * 30 + i * 3) % 150;
1023
+ const alpha = 1 - distance / 180;
1024
+ const size = 3 + Math.sin(seed * 10 + currentTime) * 2;
1025
+ ctx.beginPath();
1026
+ ctx.arc(x, y, size, 0, Math.PI * 2);
1027
+ ctx.fillStyle = `rgba(255, ${150 + Math.floor(seed * 100)}, 50, ${alpha})`;
1028
+ ctx.fill();
1029
+ }
1030
+ }
1031
+ }, [systems, isPlaying, isPaused, currentTime, showGrid, showEmitterShape]);
1032
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: ParticleEditor_module.default.previewPanel, children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, width: 400, height: 400, className: ParticleEditor_module.default.previewCanvas }) });
432
1033
  }
433
- function Toolbar({ isPlaying, isPaused, playbackSpeed, currentTime, canUndo, canRedo, showGrid, showBounds, showEmitterShape, viewMode, onPlay, onPause, onStop, onRestart, onSpeedChange, onUndo, onRedo, onToggleGrid, onToggleBounds, onToggleEmitterShape, onViewModeChange, onNew, onExport, onImport, }) {
434
- const { t } = i18n.useNiceTranslation();
435
- return (jsxRuntime.jsxs("div", { className: ParticleEditor_module.toolbar, children: [jsxRuntime.jsxs("div", { className: ParticleEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onNew, title: t('particleEditor.newProject', 'New Project'), "aria-label": t('particleEditor.newProject', 'New Project'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "file", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onImport, title: t('particleEditor.import', 'Import'), "aria-label": t('particleEditor.import', 'Import'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "folder", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onExport, title: t('particleEditor.export', 'Export'), "aria-label": t('particleEditor.export', 'Export'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "save", size: 16 }) })] }), jsxRuntime.jsx("div", { className: ParticleEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onUndo, disabled: !canUndo, title: t('particleEditor.undo', 'Undo'), "aria-label": t('particleEditor.undo', 'Undo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-left", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onRedo, disabled: !canRedo, title: t('particleEditor.redo', 'Redo'), "aria-label": t('particleEditor.redo', 'Redo'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "arrow-right", size: 16 }) })] }), jsxRuntime.jsx("div", { className: ParticleEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.toolbarGroup, children: [isPlaying && !isPaused ? (jsxRuntime.jsx("button", { onClick: onPause, title: t('particleEditor.pause', 'Pause'), "aria-label": t('particleEditor.pause', 'Pause'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "pause", size: 16 }) })) : (jsxRuntime.jsx("button", { onClick: onPlay, title: t('particleEditor.play', 'Play'), "aria-label": t('particleEditor.play', 'Play'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "play", size: 16 }) })), jsxRuntime.jsx("button", { onClick: onStop, title: t('particleEditor.stop', 'Stop'), "aria-label": t('particleEditor.stop', 'Stop'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "stop", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onRestart, title: t('particleEditor.restart', 'Restart'), "aria-label": t('particleEditor.restart', 'Restart'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "refresh", size: 16 }) }), jsxRuntime.jsxs("select", { value: playbackSpeed, onChange: (e) => onSpeedChange(parseFloat(e.target.value)), className: ParticleEditor_module.speedSelect, title: t('particleEditor.playbackSpeed', 'Playback Speed'), children: [jsxRuntime.jsx("option", { value: 0.25, children: "0.25x" }), jsxRuntime.jsx("option", { value: 0.5, children: "0.5x" }), jsxRuntime.jsx("option", { value: 1, children: "1x" }), jsxRuntime.jsx("option", { value: 2, children: "2x" }), jsxRuntime.jsx("option", { value: 4, children: "4x" })] }), jsxRuntime.jsxs("span", { className: ParticleEditor_module.timeDisplay, children: [currentTime.toFixed(2), "s"] })] }), jsxRuntime.jsx("div", { className: ParticleEditor_module.toolbarSeparator }), jsxRuntime.jsxs("div", { className: ParticleEditor_module.toolbarGroup, children: [jsxRuntime.jsx("button", { onClick: onToggleGrid, className: showGrid ? ParticleEditor_module.active : '', title: t('particleEditor.toggleGrid', 'Toggle Grid'), "aria-label": t('particleEditor.toggleGrid', 'Toggle Grid'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "hash", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onToggleBounds, className: showBounds ? ParticleEditor_module.active : '', title: t('particleEditor.toggleBounds', 'Toggle Bounds'), "aria-label": t('particleEditor.toggleBounds', 'Toggle Bounds'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "square", size: 16 }) }), jsxRuntime.jsx("button", { onClick: onToggleEmitterShape, className: showEmitterShape ? ParticleEditor_module.active : '', title: t('particleEditor.toggleEmitterShape', 'Toggle Emitter Shape'), "aria-label": t('particleEditor.toggleEmitterShape', 'Toggle Emitter Shape'), children: jsxRuntime.jsx(ui.NiceIcon, { name: "target", size: 16 }) })] }), jsxRuntime.jsx("div", { className: ParticleEditor_module.toolbarSeparator }), jsxRuntime.jsx("div", { className: ParticleEditor_module.toolbarGroup, children: ['3d', 'top', 'side', 'front'].map((mode) => (jsxRuntime.jsx("button", { onClick: () => onViewModeChange(mode), className: viewMode === mode ? ParticleEditor_module.active : '', title: mode.toUpperCase(), children: mode === '3d' ? jsxRuntime.jsx(ui.NiceIcon, { name: "video", size: 16 }) : mode.charAt(0).toUpperCase() }, mode))) })] }));
436
- }
437
- function PreviewPanel({ systems, isPlaying, isPaused, currentTime, showGrid, showEmitterShape, }) {
438
- const canvasRef = React.useRef(null);
439
- // Simple preview rendering (placeholder - real implementation would use Three.js)
440
- React.useEffect(() => {
441
- const canvas = canvasRef.current;
442
- if (!canvas) {
443
- return;
444
- }
445
- const ctx = canvas.getContext('2d');
446
- if (!ctx) {
447
- return;
448
- }
449
- const width = canvas.width;
450
- const height = canvas.height;
451
- const centerX = width / 2;
452
- const centerY = height / 2;
453
- // Clear
454
- ctx.fillStyle = ui.resolveCssColor('var(--nice-bg, #1a1a1a)', ctx.canvas);
455
- ctx.fillRect(0, 0, width, height);
456
- // Draw grid
457
- if (showGrid) {
458
- ctx.strokeStyle = ui.resolveCssColor('var(--nice-overlay-light-10, rgba(255, 255, 255, 0.1))', ctx.canvas);
459
- ctx.lineWidth = 1;
460
- const gridSize = 40;
461
- for (let x = 0; x < width; x += gridSize) {
462
- ctx.beginPath();
463
- ctx.moveTo(x, 0);
464
- ctx.lineTo(x, height);
465
- ctx.stroke();
466
- }
467
- for (let y = 0; y < height; y += gridSize) {
468
- ctx.beginPath();
469
- ctx.moveTo(0, y);
470
- ctx.lineTo(width, y);
471
- ctx.stroke();
472
- }
473
- }
474
- // Draw emitter shape indicator
475
- if (showEmitterShape && systems.length > 0) {
476
- ctx.strokeStyle = ui.resolveCssColor('var(--nice-primary-tint-60, rgba(0,200,255,0.5))', ctx.canvas);
477
- ctx.lineWidth = 2;
478
- ctx.setLineDash([5, 5]);
479
- ctx.beginPath();
480
- ctx.arc(centerX, centerY, 30, 0, Math.PI * 2);
481
- ctx.stroke();
482
- ctx.setLineDash([]);
483
- }
484
- // Draw simple particle simulation
485
- if (isPlaying && !isPaused) {
486
- const particleCount = 50;
487
- for (let i = 0; i < particleCount; i++) {
488
- const seed = i / particleCount;
489
- const angle = seed * Math.PI * 2 + currentTime;
490
- const distance = 30 + ((currentTime * 50 + i * 10) % 150);
491
- const x = centerX + Math.cos(angle) * distance;
492
- const y = centerY - Math.sin(angle) * distance * 0.5 - ((currentTime * 30 + i * 3) % 150);
493
- const alpha = 1 - distance / 180;
494
- const size = 3 + Math.sin(seed * 10 + currentTime) * 2;
495
- ctx.beginPath();
496
- ctx.arc(x, y, size, 0, Math.PI * 2);
497
- ctx.fillStyle = `rgba(255, ${150 + Math.floor(seed * 100)}, 50, ${alpha})`;
498
- ctx.fill();
499
- }
500
- }
501
- }, [systems, isPlaying, isPaused, currentTime, showGrid, showEmitterShape]);
502
- return (jsxRuntime.jsx("div", { className: ParticleEditor_module.previewPanel, children: jsxRuntime.jsx("canvas", { ref: canvasRef, width: 400, height: 400, className: ParticleEditor_module.previewCanvas }) }));
503
- }
504
- /* ═══════════════════════════════════════════════════════════════════════════
505
- Main Component
506
- ═══════════════════════════════════════════════════════════════════════════ */
507
- /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
508
1034
  const PARTICLE_EDITOR_TOUR = [
509
- {
510
- target: '.nice-particle-editor',
511
- titleKey: 'tutorial.particleEditor.intro.title',
512
- title: 'Particle editor',
513
- contentKey: 'tutorial.particleEditor.intro.content',
514
- content: 'Pick a particle system, tune its emitter and modules in the inspector, and play, pause or stop the live preview.',
515
- placement: 'auto',
516
- },
1035
+ {
1036
+ target: ".nice-particle-editor",
1037
+ titleKey: "tutorial.particleEditor.intro.title",
1038
+ title: "Particle editor",
1039
+ contentKey: "tutorial.particleEditor.intro.content",
1040
+ content: "Pick a particle system, tune its emitter and modules in the inspector, and play, pause or stop the live preview.",
1041
+ placement: "auto"
1042
+ }
517
1043
  ];
518
- function NiceParticleEditor({ systems: initialSystems, onSystemsChange, onPlay, onPause, onStop, customTextures: _customTextures, customMeshes: _customMeshes, width = '100%', height = '600px', readOnly: _readOnly = false, showToolbar = true, showSystemList = true, showInspector = true, showPreview = true, showTimeline: _showTimeline = true, theme = 'dark', className, tutorial, }) {
519
- const { state, dispatch } = useParticleEditor(initialSystems);
520
- const animationFrameRef = React.useRef(undefined);
521
- const selectedSystem = React.useMemo(() => state.systems.find((s) => s.id === state.selectedSystemId) || null, [state.systems, state.selectedSystemId]);
522
- // Notify parent of changes
523
- React.useEffect(() => {
524
- onSystemsChange === null || onSystemsChange === void 0 ? void 0 : onSystemsChange(state.systems);
525
- }, [state.systems, onSystemsChange]);
526
- // Animation loop
527
- React.useEffect(() => {
528
- if (!state.isPlaying || state.isPaused) {
529
- return;
530
- }
531
- let lastTime = performance.now();
532
- const animate = (time) => {
533
- const dt = ((time - lastTime) / 1000) * state.playbackSpeed;
534
- lastTime = time;
535
- dispatch({ type: 'SET_CURRENT_TIME', time: state.currentTime + dt });
536
- // Check if we need to loop
537
- const duration = (selectedSystem === null || selectedSystem === void 0 ? void 0 : selectedSystem.duration) || 5;
538
- if (state.currentTime >= duration && (selectedSystem === null || selectedSystem === void 0 ? void 0 : selectedSystem.looping)) {
539
- dispatch({ type: 'SET_CURRENT_TIME', time: 0 });
540
- }
541
- animationFrameRef.current = requestAnimationFrame(animate);
542
- };
543
- animationFrameRef.current = requestAnimationFrame(animate);
544
- return () => {
545
- if (animationFrameRef.current) {
546
- cancelAnimationFrame(animationFrameRef.current);
547
- }
548
- };
549
- }, [
550
- state.isPlaying,
551
- state.isPaused,
552
- state.playbackSpeed,
553
- state.currentTime,
554
- selectedSystem,
555
- dispatch,
556
- ]);
557
- // Export/Import handlers
558
- const handleExport = () => {
559
- const json = particleEditorUtils.exportSystemsToJSON(state.systems);
560
- const blob = new Blob([json], { type: 'application/json' });
561
- const url = URL.createObjectURL(blob);
562
- const a = document.createElement('a');
563
- a.href = url;
564
- a.download = 'particle-systems.json';
565
- a.click();
566
- URL.revokeObjectURL(url);
1044
+ function NiceParticleEditor({
1045
+ systems: initialSystems,
1046
+ onSystemsChange,
1047
+ onPlay,
1048
+ onPause,
1049
+ onStop,
1050
+ customTextures: _customTextures,
1051
+ customMeshes: _customMeshes,
1052
+ width = "100%",
1053
+ height = "600px",
1054
+ readOnly: _readOnly = false,
1055
+ showToolbar = true,
1056
+ showSystemList = true,
1057
+ showInspector = true,
1058
+ showPreview = true,
1059
+ showTimeline: _showTimeline = true,
1060
+ theme = "dark",
1061
+ className,
1062
+ tutorial
1063
+ }) {
1064
+ const { state, dispatch } = useParticleEditor(initialSystems);
1065
+ const animationFrameRef = React.useRef(void 0);
1066
+ const selectedSystem = React.useMemo(
1067
+ () => state.systems.find((s) => s.id === state.selectedSystemId) || null,
1068
+ [state.systems, state.selectedSystemId]
1069
+ );
1070
+ React.useEffect(() => {
1071
+ onSystemsChange?.(state.systems);
1072
+ }, [state.systems, onSystemsChange]);
1073
+ React.useEffect(() => {
1074
+ if (!state.isPlaying || state.isPaused) {
1075
+ return;
1076
+ }
1077
+ let lastTime = performance.now();
1078
+ const animate = (time) => {
1079
+ const dt = (time - lastTime) / 1e3 * state.playbackSpeed;
1080
+ lastTime = time;
1081
+ dispatch({ type: "SET_CURRENT_TIME", time: state.currentTime + dt });
1082
+ const duration = selectedSystem?.duration || 5;
1083
+ if (state.currentTime >= duration && selectedSystem?.looping) {
1084
+ dispatch({ type: "SET_CURRENT_TIME", time: 0 });
1085
+ }
1086
+ animationFrameRef.current = requestAnimationFrame(animate);
567
1087
  };
568
- const handleImport = () => {
569
- const input = document.createElement('input');
570
- input.type = 'file';
571
- input.accept = '.json';
572
- input.onchange = async (e) => {
573
- var _a;
574
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
575
- if (!file) {
576
- return;
577
- }
578
- const text = await file.text();
579
- const systems = particleEditorUtils.importSystemsFromJSON(text);
580
- if (systems) {
581
- dispatch({ type: 'LOAD_SYSTEMS', systems });
582
- }
583
- };
584
- input.click();
1088
+ animationFrameRef.current = requestAnimationFrame(animate);
1089
+ return () => {
1090
+ if (animationFrameRef.current) {
1091
+ cancelAnimationFrame(animationFrameRef.current);
1092
+ }
1093
+ };
1094
+ }, [
1095
+ state.isPlaying,
1096
+ state.isPaused,
1097
+ state.playbackSpeed,
1098
+ state.currentTime,
1099
+ selectedSystem,
1100
+ dispatch
1101
+ ]);
1102
+ const handleExport = () => {
1103
+ const json = particleEditorUtils.exportSystemsToJSON(state.systems);
1104
+ const blob = new Blob([json], { type: "application/json" });
1105
+ const url = URL.createObjectURL(blob);
1106
+ const a = document.createElement("a");
1107
+ a.href = url;
1108
+ a.download = "particle-systems.json";
1109
+ a.click();
1110
+ URL.revokeObjectURL(url);
1111
+ };
1112
+ const handleImport = () => {
1113
+ const input = document.createElement("input");
1114
+ input.type = "file";
1115
+ input.accept = ".json";
1116
+ input.onchange = async (e) => {
1117
+ const file = e.target.files?.[0];
1118
+ if (!file) {
1119
+ return;
1120
+ }
1121
+ const text = await file.text();
1122
+ const systems = particleEditorUtils.importSystemsFromJSON(text);
1123
+ if (systems) {
1124
+ dispatch({ type: "LOAD_SYSTEMS", systems });
1125
+ }
585
1126
  };
586
- return (jsxRuntime.jsxs("div", { style: { position: 'relative', width, height }, children: [jsxRuntime.jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, PARTICLE_EDITOR_TOUR) }) }), jsxRuntime.jsx(ui.NiceAppShell, { className: `nice-particle-editor ${ParticleEditor_module.particleEditor} ${ParticleEditor_module[theme]} ${className || ''}`, flush: true, toolbar: showToolbar ? (jsxRuntime.jsx(Toolbar, { isPlaying: state.isPlaying, isPaused: state.isPaused, playbackSpeed: state.playbackSpeed, currentTime: state.currentTime, canUndo: state.undoStack.length > 0, canRedo: state.redoStack.length > 0, showGrid: state.showGrid, showBounds: state.showBounds, showEmitterShape: state.showEmitterShape, viewMode: state.viewMode, onPlay: () => {
587
- dispatch({ type: 'PLAY' });
588
- onPlay === null || onPlay === void 0 ? void 0 : onPlay();
589
- }, onPause: () => {
590
- dispatch({ type: 'PAUSE' });
591
- onPause === null || onPause === void 0 ? void 0 : onPause();
592
- }, onStop: () => {
593
- dispatch({ type: 'STOP' });
594
- onStop === null || onStop === void 0 ? void 0 : onStop();
595
- }, onRestart: () => dispatch({ type: 'RESTART' }), onSpeedChange: (speed) => dispatch({ type: 'SET_PLAYBACK_SPEED', speed }), onUndo: () => dispatch({ type: 'UNDO' }), onRedo: () => dispatch({ type: 'REDO' }), onToggleGrid: () => dispatch({ type: 'TOGGLE_GRID' }), onToggleBounds: () => dispatch({ type: 'TOGGLE_BOUNDS' }), onToggleEmitterShape: () => dispatch({ type: 'TOGGLE_EMITTER_SHAPE' }), onViewModeChange: (mode) => dispatch({ type: 'SET_VIEW_MODE', mode }), onNew: () => dispatch({ type: 'NEW_PROJECT' }), onExport: handleExport, onImport: handleImport })) : undefined, sidebar: showSystemList ? (jsxRuntime.jsx(SystemListPanel, { systems: state.systems, selectedId: state.selectedSystemId, onSelect: (id) => dispatch({ type: 'SELECT_SYSTEM', systemId: id }), onAdd: () => dispatch({ type: 'ADD_SYSTEM', system: particleEditorUtils.createDefaultSystem() }), onRemove: (id) => dispatch({ type: 'REMOVE_SYSTEM', systemId: id }), onDuplicate: (id) => dispatch({ type: 'DUPLICATE_SYSTEM', systemId: id }), onRename: (id, name) => dispatch({ type: 'UPDATE_SYSTEM', systemId: id, updates: { name } }), onAddPreset: (preset) => dispatch({
596
- type: 'ADD_SYSTEM',
597
- system: particleEditorUtils.applyPreset(preset),
598
- }) })) : undefined, rightSidebar: showInspector ? (jsxRuntime.jsx(InspectorPanel, { system: selectedSystem, expandedModules: state.expandedModules, selectedModule: state.selectedModule, onToggleModule: (module) => dispatch({ type: 'TOGGLE_MODULE', module }), onSelectModule: (module) => dispatch({ type: 'SELECT_MODULE', module }), onUpdateSystem: (updates) => {
599
- if (state.selectedSystemId) {
600
- dispatch({ type: 'UPDATE_SYSTEM', systemId: state.selectedSystemId, updates });
601
- }
602
- } })) : undefined, children: showPreview && (jsxRuntime.jsx(PreviewPanel, { systems: state.systems, isPlaying: state.isPlaying, isPaused: state.isPaused, currentTime: state.currentTime, showGrid: state.showGrid, showEmitterShape: state.showEmitterShape })) })] }));
1127
+ input.click();
1128
+ };
1129
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", width, height }, children: [
1130
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { position: "absolute", top: 6, right: 6, zIndex: 30 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.NiceTutorialButton, { steps: ui.resolveTutorial(tutorial, PARTICLE_EDITOR_TOUR) }) }),
1131
+ /* @__PURE__ */ jsxRuntime.jsx(
1132
+ ui.NiceAppShell,
1133
+ {
1134
+ className: `nice-particle-editor ${ParticleEditor_module.default.particleEditor} ${ParticleEditor_module.default[theme]} ${className || ""}`,
1135
+ flush: true,
1136
+ toolbar: showToolbar ? /* @__PURE__ */ jsxRuntime.jsx(
1137
+ Toolbar,
1138
+ {
1139
+ isPlaying: state.isPlaying,
1140
+ isPaused: state.isPaused,
1141
+ playbackSpeed: state.playbackSpeed,
1142
+ currentTime: state.currentTime,
1143
+ canUndo: state.undoStack.length > 0,
1144
+ canRedo: state.redoStack.length > 0,
1145
+ showGrid: state.showGrid,
1146
+ showBounds: state.showBounds,
1147
+ showEmitterShape: state.showEmitterShape,
1148
+ viewMode: state.viewMode,
1149
+ onPlay: () => {
1150
+ dispatch({ type: "PLAY" });
1151
+ onPlay?.();
1152
+ },
1153
+ onPause: () => {
1154
+ dispatch({ type: "PAUSE" });
1155
+ onPause?.();
1156
+ },
1157
+ onStop: () => {
1158
+ dispatch({ type: "STOP" });
1159
+ onStop?.();
1160
+ },
1161
+ onRestart: () => dispatch({ type: "RESTART" }),
1162
+ onSpeedChange: (speed) => dispatch({ type: "SET_PLAYBACK_SPEED", speed }),
1163
+ onUndo: () => dispatch({ type: "UNDO" }),
1164
+ onRedo: () => dispatch({ type: "REDO" }),
1165
+ onToggleGrid: () => dispatch({ type: "TOGGLE_GRID" }),
1166
+ onToggleBounds: () => dispatch({ type: "TOGGLE_BOUNDS" }),
1167
+ onToggleEmitterShape: () => dispatch({ type: "TOGGLE_EMITTER_SHAPE" }),
1168
+ onViewModeChange: (mode) => dispatch({ type: "SET_VIEW_MODE", mode }),
1169
+ onNew: () => dispatch({ type: "NEW_PROJECT" }),
1170
+ onExport: handleExport,
1171
+ onImport: handleImport
1172
+ }
1173
+ ) : void 0,
1174
+ sidebar: showSystemList ? /* @__PURE__ */ jsxRuntime.jsx(
1175
+ SystemListPanel,
1176
+ {
1177
+ systems: state.systems,
1178
+ selectedId: state.selectedSystemId,
1179
+ onSelect: (id) => dispatch({ type: "SELECT_SYSTEM", systemId: id }),
1180
+ onAdd: () => dispatch({ type: "ADD_SYSTEM", system: particleEditorUtils.createDefaultSystem() }),
1181
+ onRemove: (id) => dispatch({ type: "REMOVE_SYSTEM", systemId: id }),
1182
+ onDuplicate: (id) => dispatch({ type: "DUPLICATE_SYSTEM", systemId: id }),
1183
+ onRename: (id, name) => dispatch({ type: "UPDATE_SYSTEM", systemId: id, updates: { name } }),
1184
+ onAddPreset: (preset) => dispatch({
1185
+ type: "ADD_SYSTEM",
1186
+ system: particleEditorUtils.applyPreset(preset)
1187
+ })
1188
+ }
1189
+ ) : void 0,
1190
+ rightSidebar: showInspector ? /* @__PURE__ */ jsxRuntime.jsx(
1191
+ InspectorPanel,
1192
+ {
1193
+ system: selectedSystem,
1194
+ expandedModules: state.expandedModules,
1195
+ selectedModule: state.selectedModule,
1196
+ onToggleModule: (module2) => dispatch({ type: "TOGGLE_MODULE", module: module2 }),
1197
+ onSelectModule: (module2) => dispatch({ type: "SELECT_MODULE", module: module2 }),
1198
+ onUpdateSystem: (updates) => {
1199
+ if (state.selectedSystemId) {
1200
+ dispatch({ type: "UPDATE_SYSTEM", systemId: state.selectedSystemId, updates });
1201
+ }
1202
+ }
1203
+ }
1204
+ ) : void 0,
1205
+ children: showPreview && /* @__PURE__ */ jsxRuntime.jsx(
1206
+ PreviewPanel,
1207
+ {
1208
+ systems: state.systems,
1209
+ isPlaying: state.isPlaying,
1210
+ isPaused: state.isPaused,
1211
+ currentTime: state.currentTime,
1212
+ showGrid: state.showGrid,
1213
+ showEmitterShape: state.showEmitterShape
1214
+ }
1215
+ )
1216
+ }
1217
+ )
1218
+ ] });
603
1219
  }
604
-
605
1220
  exports.NiceParticleEditor = NiceParticleEditor;
606
1221
  exports.useParticleEditor = useParticleEditor;