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