babylon.quarks-editor 0.17.8 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/core/binding.js +2 -5
  4. package/dist/core/defaultEffect.d.ts +12 -0
  5. package/dist/core/defaultEffect.js +43 -0
  6. package/dist/core/editorScene.d.ts +7 -0
  7. package/dist/core/editorScene.js +11 -0
  8. package/dist/core/effectTree.js +2 -1
  9. package/dist/core/emitterShapeWireframe.d.ts +20 -0
  10. package/dist/core/emitterShapeWireframe.js +329 -0
  11. package/dist/core/galleryDragSession.d.ts +4 -0
  12. package/dist/core/galleryDragSession.js +8 -0
  13. package/dist/core/galleryDropMarker.d.ts +13 -0
  14. package/dist/core/galleryDropMarker.js +42 -0
  15. package/dist/core/galleryLayout.d.ts +20 -0
  16. package/dist/core/galleryLayout.js +68 -0
  17. package/dist/core/galleryPlayback.d.ts +14 -0
  18. package/dist/core/galleryPlayback.js +72 -0
  19. package/dist/core/galleryScene.d.ts +20 -0
  20. package/dist/core/galleryScene.js +65 -0
  21. package/dist/core/gallerySelectionMarker.d.ts +25 -0
  22. package/dist/core/gallerySelectionMarker.js +69 -0
  23. package/dist/core/galleryTree.d.ts +49 -0
  24. package/dist/core/galleryTree.js +138 -0
  25. package/dist/core/loadEffect.d.ts +13 -0
  26. package/dist/core/loadEffect.js +31 -17
  27. package/dist/core/loadEffectGallery.d.ts +36 -0
  28. package/dist/core/loadEffectGallery.js +97 -0
  29. package/dist/core/material.d.ts +13 -0
  30. package/dist/core/material.js +68 -0
  31. package/dist/core/pickGalleryFiles.d.ts +5 -0
  32. package/dist/core/pickGalleryFiles.js +42 -0
  33. package/dist/core/playback.d.ts +23 -0
  34. package/dist/core/playback.js +32 -0
  35. package/dist/core/renderDebug.d.ts +8 -0
  36. package/dist/core/renderDebug.js +42 -0
  37. package/dist/core/scrub.d.ts +14 -0
  38. package/dist/core/scrub.js +70 -0
  39. package/dist/core/timeline.d.ts +4 -1
  40. package/dist/core/timeline.js +30 -3
  41. package/dist/core/transformStore.d.ts +3 -0
  42. package/dist/core/transformStore.js +16 -0
  43. package/dist/core/viewportPicker.d.ts +25 -0
  44. package/dist/core/viewportPicker.js +86 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.js +1 -0
  47. package/dist/react/EffectEditor.js +1 -4
  48. package/dist/react/EffectEditorHost.d.ts +7 -0
  49. package/dist/react/EffectEditorHost.js +745 -115
  50. package/dist/react/GalleryPanel.d.ts +20 -0
  51. package/dist/react/GalleryPanel.js +246 -0
  52. package/dist/react/InspectorInfoIcon.d.ts +7 -0
  53. package/dist/react/InspectorInfoIcon.js +22 -0
  54. package/dist/react/InspectorLabel.d.ts +7 -0
  55. package/dist/react/InspectorLabel.js +12 -0
  56. package/dist/react/MessageDialog.d.ts +9 -0
  57. package/dist/react/MessageDialog.js +24 -0
  58. package/dist/react/ModuleSection.d.ts +2 -0
  59. package/dist/react/ModuleSection.js +8 -1
  60. package/dist/react/ObjectModule.js +14 -2
  61. package/dist/react/TimelinePanel.d.ts +8 -11
  62. package/dist/react/TimelinePanel.js +129 -98
  63. package/dist/react/TimelineTrackRow.js +29 -15
  64. package/dist/react/Tooltip.d.ts +6 -0
  65. package/dist/react/Tooltip.js +65 -0
  66. package/dist/react/ValueField.d.ts +2 -0
  67. package/dist/react/ValueField.js +1 -1
  68. package/dist/react/behaviorModules.js +8 -8
  69. package/dist/react/fields.d.ts +2 -0
  70. package/dist/react/fields.js +14 -5
  71. package/dist/react/icons.d.ts +7 -0
  72. package/dist/react/icons.js +10 -0
  73. package/dist/react/index.d.ts +4 -0
  74. package/dist/react/index.js +2 -0
  75. package/dist/react/inspectorHints.d.ts +2 -0
  76. package/dist/react/inspectorHints.js +138 -0
  77. package/dist/react/modules.js +109 -81
  78. package/dist/react/theme.d.ts +2 -0
  79. package/dist/react/theme.js +53 -1
  80. package/package.json +8 -4
  81. package/src/core/binding.ts +2 -5
  82. package/src/core/defaultEffect.ts +74 -0
  83. package/src/core/editorScene.ts +14 -0
  84. package/src/core/effectTree.ts +2 -1
  85. package/src/core/emitterShapeWireframe.ts +373 -0
  86. package/src/core/galleryDragSession.ts +12 -0
  87. package/src/core/galleryDropMarker.ts +57 -0
  88. package/src/core/galleryLayout.ts +90 -0
  89. package/src/core/galleryPlayback.ts +90 -0
  90. package/src/core/galleryScene.ts +92 -0
  91. package/src/core/gallerySelectionMarker.ts +84 -0
  92. package/src/core/galleryTree.ts +187 -0
  93. package/src/core/loadEffect.ts +55 -18
  94. package/src/core/loadEffectGallery.ts +146 -0
  95. package/src/core/material.ts +84 -0
  96. package/src/core/pickGalleryFiles.ts +58 -0
  97. package/src/core/playback.ts +49 -0
  98. package/src/core/renderDebug.ts +46 -0
  99. package/src/core/scrub.ts +76 -1
  100. package/src/core/timeline.ts +34 -3
  101. package/src/core/transformStore.ts +19 -0
  102. package/src/core/viewportPicker.ts +114 -0
  103. package/src/index.ts +2 -0
  104. package/src/react/EffectEditor.tsx +5 -7
  105. package/src/react/EffectEditorHost.tsx +936 -142
  106. package/src/react/GalleryPanel.tsx +517 -0
  107. package/src/react/InspectorInfoIcon.tsx +36 -0
  108. package/src/react/InspectorLabel.tsx +18 -0
  109. package/src/react/MessageDialog.tsx +57 -0
  110. package/src/react/ModuleSection.tsx +17 -2
  111. package/src/react/ObjectModule.tsx +17 -6
  112. package/src/react/TimelinePanel.tsx +227 -174
  113. package/src/react/TimelineTrackRow.tsx +58 -23
  114. package/src/react/Tooltip.tsx +93 -0
  115. package/src/react/ValueField.tsx +3 -1
  116. package/src/react/behaviorModules.tsx +13 -8
  117. package/src/react/fields.tsx +15 -6
  118. package/src/react/icons.ts +19 -0
  119. package/src/react/index.ts +4 -0
  120. package/src/react/inspectorHints.ts +150 -0
  121. package/src/react/modules.tsx +198 -183
  122. package/src/react/theme.ts +54 -1
@@ -1,16 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
3
- import { inputStyle, labelStyle, rowStyle } from './theme';
2
+ import React, { useEffect, useState } from 'react';
3
+ import { InspectorLabel } from './InspectorLabel';
4
+ import { inputStyle, rowStyle } from './theme';
4
5
  export function Row(props) {
5
- return (_jsxs("div", { style: rowStyle, children: [_jsx("span", { style: labelStyle, title: props.label, children: props.label }), props.children] }));
6
+ return (_jsxs("div", { style: rowStyle, children: [_jsx(InspectorLabel, { label: props.label, hint: props.hint, hintKey: props.hintKey }), props.children] }));
6
7
  }
7
8
  /** Number input that keeps focus-friendly local text state and commits parsed values. */
8
9
  export function NumberField(props) {
9
10
  const [text, setText] = useState(String(round(props.value)));
11
+ const focusedRef = React.useRef(false);
10
12
  useEffect(() => {
11
- setText(String(round(props.value)));
13
+ if (!focusedRef.current) {
14
+ setText(String(round(props.value)));
15
+ }
12
16
  }, [props.value]);
13
- return (_jsx("input", { type: "number", className: "qe-hover", style: inputStyle, value: text, step: props.step ?? 0.1, min: props.min, onChange: (e) => {
17
+ return (_jsx("input", { type: "number", className: "qe-hover", style: inputStyle, value: text, step: props.step ?? 0.1, min: props.min, onFocus: () => {
18
+ focusedRef.current = true;
19
+ }, onBlur: () => {
20
+ focusedRef.current = false;
21
+ setText(String(round(props.value)));
22
+ }, onChange: (e) => {
14
23
  setText(e.target.value);
15
24
  const parsed = parseFloat(e.target.value);
16
25
  if (!Number.isNaN(parsed)) {
@@ -0,0 +1,7 @@
1
+ import type { ComponentType, CSSProperties, SVGProps } from 'react';
2
+ export type HeroIcon = ComponentType<SVGProps<SVGSVGElement> & {
3
+ title?: string;
4
+ titleId?: string;
5
+ }>;
6
+ /** Shared size/style for toolbar and inline icon buttons. */
7
+ export declare function iconStyle(size?: number, extra?: CSSProperties): CSSProperties;
@@ -0,0 +1,10 @@
1
+ /** Shared size/style for toolbar and inline icon buttons. */
2
+ export function iconStyle(size = 14, extra) {
3
+ return {
4
+ width: size,
5
+ height: size,
6
+ flexShrink: 0,
7
+ display: 'block',
8
+ ...extra,
9
+ };
10
+ }
@@ -16,5 +16,9 @@ export { ValueField } from './ValueField';
16
16
  export { Row, NumberField, CheckboxField, SelectField } from './fields';
17
17
  export { MainModule, EmissionModule, ShapeModule, SizeOverLifeModule, ColorOverLifeModule, RendererModule, } from './modules';
18
18
  export { theme, inputStyle, rowStyle, labelStyle, buttonStyle } from './theme';
19
+ export { MessageDialog } from './MessageDialog';
20
+ export type { MessageDialogProps } from './MessageDialog';
21
+ export { GalleryPanel } from './GalleryPanel';
22
+ export type { GalleryPanelProps } from './GalleryPanel';
19
23
  export { EffectEditorHost, QuarksEffectEditor } from './EffectEditorHost';
20
24
  export type { EffectEditorHostProps, EffectEditorHostHandle } from './EffectEditorHost';
@@ -11,4 +11,6 @@ export { ValueField } from './ValueField';
11
11
  export { Row, NumberField, CheckboxField, SelectField } from './fields';
12
12
  export { MainModule, EmissionModule, ShapeModule, SizeOverLifeModule, ColorOverLifeModule, RendererModule, } from './modules';
13
13
  export { theme, inputStyle, rowStyle, labelStyle, buttonStyle } from './theme';
14
+ export { MessageDialog } from './MessageDialog';
15
+ export { GalleryPanel } from './GalleryPanel';
14
16
  export { EffectEditorHost, QuarksEffectEditor } from './EffectEditorHost';
@@ -0,0 +1,2 @@
1
+ /** Returns the English tooltip for an inspector label or hint key. */
2
+ export declare function getInspectorHint(key: string): string | undefined;
@@ -0,0 +1,138 @@
1
+ /** English tooltips for inspector labels and module titles. */
2
+ const INSPECTOR_HINTS = {
3
+ // Module titles
4
+ Main: 'Core timing and initial particle properties when each particle is born.',
5
+ Emission: 'How many particles spawn continuously, along movement, or in timed bursts.',
6
+ Shape: 'The volume or surface particles spawn from (sphere, box, cone, mesh, etc.).',
7
+ 'Size over Lifetime': 'Multiplies each particle’s size over its normalized lifetime (0 = birth, 1 = death).',
8
+ 'Color over Lifetime': 'Tints color and alpha over each particle’s normalized lifetime.',
9
+ Renderer: 'How particles are rendered: billboard mode, material, texture, depth, and sort order.',
10
+ 'Speed over Lifetime': 'Multiplies particle speed over normalized lifetime.',
11
+ 'Limit Speed over Lifetime': 'Clamps particle speed and optionally dampens velocity toward the limit.',
12
+ 'Force over Lifetime': 'Adds constant world-space acceleration (force per mass) over lifetime.',
13
+ 'Velocity over Lifetime': 'Adds linear and orbital velocity each frame in local or world space.',
14
+ 'Inherit Velocity': 'Copies the emitter’s movement into new particle velocity.',
15
+ Gravity: 'Applies a constant downward force (Earth-like gravity by default).',
16
+ 'Rotation over Lifetime': 'Spins particles by setting angular velocity over lifetime.',
17
+ Noise: 'Procedural noise that jitters particle position and rotation.',
18
+ Turbulence: '3D turbulent velocity field that swirls particles through space.',
19
+ 'Randomize Direction': 'Randomizes emission direction within a cone angle around the shape normal.',
20
+ Collision: 'Bounces particles off a ground plane in the editor preview.',
21
+ 'Color by Speed': 'Maps particle color from current speed using a gradient.',
22
+ 'Size by Speed': 'Maps particle size from current speed using a curve.',
23
+ 'Rotation by Speed': 'Maps angular velocity from current speed.',
24
+ 'Orbit over Lifetime': 'Orbits particles around a local axis over their lifetime.',
25
+ 'Width over Trail': 'Controls trail ribbon width along the particle path (trail render mode).',
26
+ 'Texture Sheet Animation': 'Animates UV tiles on the texture like a flipbook sprite sheet.',
27
+ 'Sub Emitters': 'Spawns a linked child particle system on birth, death, or every frame.',
28
+ Object: 'Transform of the selected node. Moving the stage parent is preview-only and not exported.',
29
+ // Main
30
+ Duration: 'How long the particle system runs before stopping (non-looping) or one loop cycle.',
31
+ Looping: 'When enabled, the effect restarts from the beginning after duration ends.',
32
+ 'World space': 'Simulates in world coordinates so moving the emitter does not drag existing particles.',
33
+ Prewarm: 'Simulates the effect once at start so it already looks mid-playback when play begins.',
34
+ 'Start delay': 'Wait time before emission starts after play or restart.',
35
+ 'Start lifetime': 'How long each spawned particle lives before dying.',
36
+ 'Start speed': 'Initial speed along the emission direction when a particle is born.',
37
+ 'Start size': 'Initial size of particles — uniform scalar or separate X/Y/Z for mesh billboards.',
38
+ 'Start color': 'Initial color: solid, random between two, or gradient-based.',
39
+ 'Start rotation': 'Initial orientation: fixed angle, random, per-axis Euler, or random 3D quaternion.',
40
+ ' X (rad)': 'Start rotation around the local X axis in radians.',
41
+ ' Y (rad)': 'Start rotation around the local Y axis in radians.',
42
+ ' Z (rad)': 'Start rotation around the local Z axis in radians.',
43
+ // Emission
44
+ 'Rate over time': 'Particles spawned per second while the system is playing.',
45
+ 'Rate over distance': 'Particles spawned per unit of emitter movement.',
46
+ Time: 'Seconds from effect start when this burst fires.',
47
+ Count: 'Number of particles emitted in each burst cycle.',
48
+ Cycles: 'How many times the burst repeats.',
49
+ Interval: 'Seconds between repeated cycles of the same burst.',
50
+ Probability: 'Chance (0–1) that the burst or sub-emitter spawn actually happens.',
51
+ // Shape
52
+ 'shape.type': 'Emitter volume type that defines where new particles appear.',
53
+ Radius: 'Outer radius of spherical or circular emitter shapes.',
54
+ Arc: 'Angular span (radians) of partial circle or sphere emission.',
55
+ Thickness: 'Shell thickness for hollow shapes (0 = surface, 1 = filled volume).',
56
+ Angle: 'Cone opening angle for cone-shaped emitters.',
57
+ 'Donut radius': 'Inner tube radius of a torus/donut emitter.',
58
+ Width: 'Emitter extent along the local X axis (box/grid shapes).',
59
+ Height: 'Emitter extent along the local Y axis (box/grid shapes).',
60
+ Columns: 'Number of grid columns for grid-shaped emission.',
61
+ Rows: 'Number of grid rows for grid-shaped emission.',
62
+ 'Source mesh': 'Scene mesh whose surface particles spawn from (mesh surface shape).',
63
+ 'Emit mode': 'How particles are distributed across the shape: random, loop, ping-pong, or burst spread.',
64
+ Spread: 'Randomizes spawn position within the shape (0 = exact shape, higher = more scatter).',
65
+ // Renderer
66
+ 'Render mode': 'Orientation and geometry used to draw each particle.',
67
+ 'Trail length': 'Maximum number of trail segments per particle (trail mode).',
68
+ 'Follow origin': 'Keeps the trail anchored to the emitter origin instead of the particle head.',
69
+ 'Speed factor': 'How much particle speed stretches the billboard (stretched billboard mode).',
70
+ 'Length factor': 'Base stretch multiplier for stretched billboards.',
71
+ Source: 'Material preset loaded from the effect JSON (read-only reference).',
72
+ 'Blend mode': 'How particle color is composited over the background.',
73
+ Transparent: 'Enables alpha blending for soft edges and fade-outs.',
74
+ Texture: 'Sprite or flipbook image applied to each particle.',
75
+ 'Alpha test': 'Pixels below this alpha are discarded (cutout/hard edges).',
76
+ 'Depth write': 'Writes to the depth buffer — usually off for additive particles.',
77
+ 'Depth test': 'Whether particles are occluded by scene geometry.',
78
+ Geometry: 'Mesh vertices used when render mode is Mesh.',
79
+ 'Render order': 'Sort priority within the batch — higher draws on top.',
80
+ 'Soft particles': 'Fades particles near intersecting geometry to reduce hard cutouts.',
81
+ 'Fade near/far': 'Distance range (near, far) for soft-particle depth fading.',
82
+ // Texture sheet
83
+ 'Tiles U': 'Number of horizontal cells in the texture atlas.',
84
+ 'Tiles V': 'Number of vertical cells in the texture atlas.',
85
+ 'Blend tiles': 'Smoothly blends between adjacent atlas tiles during animation.',
86
+ 'Start tile': 'Atlas cell index where playback begins (0 = top-left).',
87
+ 'Animate frames': 'Enables frame-over-lifetime curve to cycle through atlas tiles.',
88
+ // Sub emitters
89
+ Trigger: 'When the child system is spawned: on particle birth, death, or every frame.',
90
+ 'Use velocity': 'Orients the sub-emitter spawn using the parent particle’s velocity.',
91
+ // Object transform
92
+ Position: 'Local X/Y/Z position of the selected emitter or group node.',
93
+ Rotation: 'Local rotation in degrees (Euler) of the selected node.',
94
+ Scale: 'Local X/Y/Z scale of the selected node.',
95
+ // Gradient editor
96
+ 'Stop color': 'RGB color at the selected gradient key.',
97
+ 'Stop alpha': 'Opacity at the selected gradient key (0 = transparent, 1 = opaque).',
98
+ 'Stop position': 'Normalized position of this key along the gradient (0 = start, 1 = end).',
99
+ // Shared / behavior fields
100
+ Dampen: 'How quickly velocity is reduced when speed exceeds the limit (0 = hard clamp, 1 = smooth).',
101
+ Space: 'Whether velocity is applied in local emitter space or world space.',
102
+ 'Linear X': 'Added velocity along local/world X over lifetime.',
103
+ 'Linear Y': 'Added velocity along local/world Y over lifetime.',
104
+ 'Linear Z': 'Added velocity along local/world Z over lifetime.',
105
+ 'Orbital X': 'Orbital rotation speed around the X axis (radians per second).',
106
+ 'Orbital Y': 'Orbital rotation speed around the Y axis (radians per second).',
107
+ 'Orbital Z': 'Orbital rotation speed around the Z axis (radians per second).',
108
+ Multiplier: 'Scales how much emitter velocity is copied to new particles.',
109
+ 'inheritVelocity.mode': 'Initial copies spawn-time velocity; Current uses live emitter velocity.',
110
+ 'gravity.strength': 'Magnitude of the downward force applied to every particle.',
111
+ 'noise.frequency': 'How quickly the noise pattern changes over time and space.',
112
+ 'noise.strength': 'Overall intensity of the noise displacement.',
113
+ 'Position amount': 'How strongly noise affects particle position.',
114
+ 'Rotation amount': 'How strongly noise affects particle rotation.',
115
+ 'turbulence.scale': 'Size of turbulence cells — larger values make smoother, broader swirls.',
116
+ Octaves: 'Number of noise layers combined for finer turbulent detail.',
117
+ 'turbulence.strength': 'Per-axis multiplier for turbulent velocity.',
118
+ 'Time scale': 'How fast the turbulence field evolves over time (per axis).',
119
+ 'Angle (rad)': 'Maximum random deflection from the base emit direction, in radians.',
120
+ Bounce: 'Energy retained after hitting the ground (0 = no bounce, 1 = full bounce).',
121
+ 'Floor Y': 'World-space Y height of the preview ground plane used for collision.',
122
+ 'Speed range': 'Particle speed range (min, max) that maps to the color/size/rotation curve.',
123
+ 'Orbit speed': 'Angular speed of orbit around the configured axis over lifetime.',
124
+ 'rotationOverLife.velocity': 'Angular velocity applied over normalized lifetime (rad/s).',
125
+ 'rotationBySpeed.velocity': 'Angular velocity mapped from particle speed (rad/s).',
126
+ // Size main axes
127
+ X: 'Start size along the local X axis.',
128
+ Y: 'Start size along the local Y axis.',
129
+ Z: 'Start size along the local Z axis.',
130
+ // Force over life axes
131
+ 'force.x': 'Acceleration along world X applied over lifetime.',
132
+ 'force.y': 'Acceleration along world Y applied over lifetime.',
133
+ 'force.z': 'Acceleration along world Z applied over lifetime.',
134
+ };
135
+ /** Returns the English tooltip for an inspector label or hint key. */
136
+ export function getInspectorHint(key) {
137
+ return INSPECTOR_HINTS[key];
138
+ }
@@ -1,16 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { useState } from 'react';
2
+ import React, { useMemo, useState } from 'react';
3
3
  import { Mesh } from '@babylonjs/core/Meshes/mesh';
4
+ import { Texture } from '@babylonjs/core/Materials/Textures/texture';
4
5
  import { ColorOverLife, ConstantColor, ConstantValue, EulerGenerator, Gradient, IntervalValue, RandomColor, RandomColorBetweenGradient, RandomQuatGenerator, RenderMode, SizeOverLife, Vector3Function, Vector4, } from 'babylon.quarks';
5
6
  import { DEFAULT_GRADIENT_STOPS, buildGradient, findBehavior, hexToRgb, readGradientStops, rgbToHex, } from '../core/colors';
6
7
  import { DEFAULT_SHAPE_PARAMS, SHAPE_PARAM_KEYS, SHAPE_TYPES, createShape, getShapeType, readShapeParams } from '../core/shapes';
7
8
  import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values';
9
+ import { applyRendererMaterial, getMaterialLabel } from '../core/material';
8
10
  import { CurveEditor } from './CurveEditor';
9
11
  import { GradientEditor } from './GradientEditor';
10
12
  import { ModuleSection } from './ModuleSection';
11
13
  import { PromptDialog } from './PromptDialog';
12
14
  import { CheckboxField, NumberField, Row, SelectField } from './fields';
13
15
  import { ValueField } from './ValueField';
16
+ import { iconStyle } from './icons';
17
+ import { theme } from './theme';
18
+ import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
19
+ import { InspectorInfoIcon } from './InspectorInfoIcon';
14
20
  const MESH_QUAD_POSITIONS = new Float32Array([-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0]);
15
21
  const MESH_QUAD_INDICES = new Uint32Array([0, 1, 2, 0, 2, 3]);
16
22
  const MESH_QUAD_UVS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
@@ -154,7 +160,19 @@ export function MainModule({ binding }) {
154
160
  export function EmissionModule({ binding }) {
155
161
  const system = binding.system;
156
162
  const bursts = system.emissionBursts ?? [];
157
- return (_jsxs(ModuleSection, { title: "Emission", children: [_jsx(ValueField, { label: "Rate over time", generator: system.emissionOverTime, min: 0, curveMax: 500, onChange: (g) => binding.apply((s) => (s.emissionOverTime = g)) }), _jsx(ValueField, { label: "Rate over distance", generator: system.emissionOverDistance, min: 0, curveMax: 50, onChange: (g) => binding.apply((s) => (s.emissionOverDistance = g)) }), bursts.map((burst, i) => (_jsxs("div", { style: { marginTop: 8, padding: '6px 8px', border: '1px solid #22305c', borderRadius: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("span", { style: { fontSize: 11.5, color: '#9eb9ff' }, children: ["Burst ", i + 1] }), _jsx("button", { className: "qe-hover", style: { background: 'none', border: 'none', color: '#e08c8c', cursor: 'pointer', fontSize: 13 }, title: "Remove burst", onClick: () => binding.apply((s) => s.emissionBursts.splice(i, 1)), children: "\u2715" })] }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginTop: 4 }, children: [_jsxs("label", { style: { fontSize: 10.5, color: '#b7c6ea' }, children: ["Time", _jsx(NumberField, { value: burst.time, min: 0, onChange: (time) => binding.apply(() => (burst.time = time)) })] }), _jsxs("label", { style: { fontSize: 10.5, color: '#b7c6ea' }, children: ["Count", _jsx(NumberField, { value: readScalar(burst.count).value, min: 0, step: 1, onChange: (count) => binding.apply(() => (burst.count = new ConstantValue(Math.round(count)))) })] }), _jsxs("label", { style: { fontSize: 10.5, color: '#b7c6ea' }, children: ["Cycles", _jsx(NumberField, { value: burst.cycle, min: 1, step: 1, onChange: (v) => binding.apply(() => (burst.cycle = Math.round(v))) })] }), _jsxs("label", { style: { fontSize: 10.5, color: '#b7c6ea' }, children: ["Interval", _jsx(NumberField, { value: burst.interval, min: 0.01, onChange: (v) => binding.apply(() => (burst.interval = v)) })] }), _jsxs("label", { style: { fontSize: 10.5, color: '#b7c6ea' }, children: ["Probability", _jsx(NumberField, { value: burst.probability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (burst.probability = Math.min(1, v))) })] })] })] }, i))), _jsx("button", { className: "qe-hover", style: { marginTop: 8, background: 'none', border: '1px dashed #34477f', color: '#9eb9ff', borderRadius: 8, padding: '5px 10px', cursor: 'pointer', fontSize: 12.5 }, onClick: () => binding.apply((s) => s.emissionBursts.push({ time: 0, count: new ConstantValue(20), cycle: 1, interval: 0.01, probability: 1 })), children: "+ Add burst" })] }));
163
+ return (_jsxs(ModuleSection, { title: "Emission", children: [_jsx(ValueField, { label: "Rate over time", generator: system.emissionOverTime, min: 0, curveMax: 500, onChange: (g) => binding.apply((s) => (s.emissionOverTime = g)) }), _jsx(ValueField, { label: "Rate over distance", generator: system.emissionOverDistance, min: 0, curveMax: 50, onChange: (g) => binding.apply((s) => (s.emissionOverDistance = g)) }), bursts.map((burst, i) => (_jsxs("div", { style: { marginTop: 8, padding: '6px 8px', border: '1px solid #22305c', borderRadius: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs("span", { style: { fontSize: 11.5, color: '#9eb9ff' }, children: ["Burst ", i + 1] }), _jsx("button", { className: "qe-hover", style: { background: 'none', border: 'none', color: '#e08c8c', cursor: 'pointer', padding: 0, display: 'inline-flex' }, title: "Remove burst", onClick: () => binding.apply((s) => s.emissionBursts.splice(i, 1)), children: _jsx(XMarkIcon, { style: iconStyle(14) }) })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 2, marginTop: 4 }, children: [_jsx(Row, { label: "Time", children: _jsx(NumberField, { value: burst.time, min: 0, onChange: (time) => binding.apply(() => (burst.time = time)) }) }), _jsx(Row, { label: "Count", children: _jsx(NumberField, { value: readScalar(burst.count).value, min: 0, step: 1, onChange: (count) => binding.apply(() => (burst.count = new ConstantValue(Math.round(count)))) }) }), _jsx(Row, { label: "Cycles", children: _jsx(NumberField, { value: burst.cycle, min: 1, step: 1, onChange: (v) => binding.apply(() => (burst.cycle = Math.round(v))) }) }), _jsx(Row, { label: "Interval", children: _jsx(NumberField, { value: burst.interval, min: 0.01, onChange: (v) => binding.apply(() => (burst.interval = v)) }) }), _jsx(Row, { label: "Probability", children: _jsx(NumberField, { value: burst.probability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (burst.probability = Math.min(1, v))) }) })] })] }, i))), _jsxs("button", { className: "qe-hover", style: {
164
+ marginTop: 8,
165
+ background: 'none',
166
+ border: '1px dashed #34477f',
167
+ color: '#9eb9ff',
168
+ borderRadius: 8,
169
+ padding: '5px 10px',
170
+ cursor: 'pointer',
171
+ fontSize: 12.5,
172
+ display: 'inline-flex',
173
+ alignItems: 'center',
174
+ gap: 6,
175
+ }, onClick: () => binding.apply((s) => s.emissionBursts.push({ time: 0, count: new ConstantValue(20), cycle: 1, interval: 0.01, probability: 1 })), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add burst"] })] }));
158
176
  }
159
177
  // Shapes whose constructor doesn't accept `mode`/`spread` at all — showing those controls
160
178
  // for them would edit a value the shape silently ignores, just like the old "default to
@@ -168,7 +186,7 @@ export function ShapeModule({ binding }) {
168
186
  const scene = system.emitter.getScene();
169
187
  // 'vfxBatch' is the renderer's own internal instancing mesh — never a valid emission source.
170
188
  const meshOptions = scene.meshes.filter((m) => m instanceof Mesh && m.name !== 'vfxBatch' && m.getTotalVertices() > 0);
171
- return (_jsxs(ModuleSection, { title: "Shape", children: [_jsx(Row, { label: "Shape", children: _jsx(SelectField, { value: type, options: SHAPE_TYPES, onChange: (next) => binding.apply((s) => (s.emitterShape = createShape(next, params))) }) }), keys.map((key) => (_jsx(Row, { label: PARAM_LABELS[key], children: _jsx(NumberField, { value: params[key], min: 0, onChange: (v) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, [key]: v }))) }) }, key))), type === 'mesh_surface' && (_jsx(Row, { label: "Source mesh", children: _jsx(SelectField, { value: params.mesh?.uniqueId != null ? String(params.mesh.uniqueId) : '__none', options: [
189
+ return (_jsxs(ModuleSection, { title: "Shape", children: [_jsx(Row, { label: "Shape", hintKey: "shape.type", children: _jsx(SelectField, { value: type, options: SHAPE_TYPES, onChange: (next) => binding.apply((s) => (s.emitterShape = createShape(next, params))) }) }), keys.map((key) => (_jsx(Row, { label: PARAM_LABELS[key], children: _jsx(NumberField, { value: params[key], min: 0, onChange: (v) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, [key]: v }))) }) }, key))), type === 'mesh_surface' && (_jsx(Row, { label: "Source mesh", children: _jsx(SelectField, { value: params.mesh?.uniqueId != null ? String(params.mesh.uniqueId) : '__none', options: [
172
190
  { value: '__none', label: '(none)' },
173
191
  ...meshOptions.map((m) => ({ value: String(m.uniqueId), label: m.name })),
174
192
  ], onChange: (id) => {
@@ -279,6 +297,19 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
279
297
  const system = binding.system;
280
298
  const currentTextureUrl = system.texture?.url ?? '';
281
299
  const [promptingTextureUrl, setPromptingTextureUrl] = useState(false);
300
+ const materialLabel = getMaterialLabel(system.material);
301
+ const loadTexture = useMemo(() => {
302
+ return (url) => {
303
+ if (resolveTexture) {
304
+ return resolveTexture(url);
305
+ }
306
+ const scene = system.emitter.getScene();
307
+ return scene ? new Texture(url, scene) : null;
308
+ };
309
+ }, [resolveTexture, system]);
310
+ const patchMaterial = (patch) => {
311
+ binding.apply((s) => applyRendererMaterial(s, patch));
312
+ };
282
313
  return (_jsxs(ModuleSection, { title: "Renderer", children: [_jsx(Row, { label: "Render mode", children: _jsx(SelectField, { value: system.renderMode, options: [
283
314
  { value: RenderMode.BillBoard, label: 'Billboard' },
284
315
  { value: RenderMode.StretchedBillBoard, label: 'Stretched billboard' },
@@ -290,62 +321,7 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
290
321
  s.rendererEmitterSettings.startLength = new ConstantValue(Math.round(v));
291
322
  }) }) }), _jsx(Row, { label: "Follow origin", children: _jsx(CheckboxField, { value: !!system.rendererEmitterSettings.followLocalOrigin, onChange: (v) => binding.apply((s) => {
292
323
  s.rendererEmitterSettings.followLocalOrigin = v;
293
- }) }) })] })), system.renderMode === RenderMode.StretchedBillBoard && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Speed factor", children: _jsx(NumberField, { value: system.rendererEmitterSettings.speedFactor ?? 0, step: 0.1, onChange: (v) => binding.apply((s) => (s.rendererEmitterSettings.speedFactor = v)) }) }), _jsx(Row, { label: "Length factor", children: _jsx(NumberField, { value: system.rendererEmitterSettings.lengthFactor ?? 2, step: 0.1, onChange: (v) => binding.apply((s) => (s.rendererEmitterSettings.lengthFactor = v)) }) })] })), system.renderMode === RenderMode.Mesh &&
294
- (() => {
295
- const preset = detectGeometryPreset(system.instancingGeometry);
296
- const vertexCount = system.instancingGeometry.length / 3;
297
- const triangleCount = (system.getRendererSettings().instancingIndices?.length ?? 0) / 3;
298
- const hostGeometry = geometryOptions ?? [];
299
- const applyGeometry = (g) => binding.apply((s) => {
300
- const settings = s.getRendererSettings();
301
- settings.instancingIndices = (g.indices ? new Uint32Array(g.indices) : undefined);
302
- settings.instancingUVs = (g.uvs ? new Float32Array(g.uvs) : undefined);
303
- settings.instancingNormals = (g.normals ? new Float32Array(g.normals) : undefined);
304
- s.instancingGeometry = new Float32Array(g.positions);
305
- });
306
- return (_jsx(Row, { label: "Geometry", children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx(SelectField, { value: preset, options: [
307
- ...(preset === 'custom' ? [{ value: 'custom', label: 'Custom (imported)' }] : []),
308
- { value: 'quad', label: 'Quad' },
309
- { value: 'cube', label: 'Cube' },
310
- ...hostGeometry.map((g, i) => ({ value: `host:${i}`, label: g.label })),
311
- ...(resolveGeometry ? [{ value: '__file', label: 'Load from file…' }] : []),
312
- ], onChange: (next) => {
313
- if (next === 'custom') {
314
- return;
315
- }
316
- if (next === '__file') {
317
- const input = document.createElement('input');
318
- input.type = 'file';
319
- input.accept = '.glb,.gltf,model/gltf-binary,model/gltf+json';
320
- input.onchange = () => {
321
- const file = input.files?.[0];
322
- if (file) {
323
- resolveGeometry(file).then(applyGeometry);
324
- }
325
- };
326
- input.click();
327
- return;
328
- }
329
- if (next.startsWith('host:')) {
330
- applyGeometry(hostGeometry[Number(next.slice(5))]);
331
- return;
332
- }
333
- binding.apply((s) => {
334
- const settings = s.getRendererSettings();
335
- if (next === 'cube') {
336
- settings.instancingIndices = MESH_CUBE_INDICES;
337
- settings.instancingUVs = undefined;
338
- settings.instancingNormals = undefined;
339
- s.instancingGeometry = MESH_CUBE_POSITIONS;
340
- }
341
- else {
342
- settings.instancingIndices = MESH_QUAD_INDICES;
343
- settings.instancingUVs = MESH_QUAD_UVS;
344
- s.instancingGeometry = MESH_QUAD_POSITIONS;
345
- }
346
- });
347
- } }), _jsx(MeshPreview, { positions: system.instancingGeometry, indices: system.getRendererSettings().instancingIndices }), preset === 'custom' && (_jsxs("span", { style: { fontSize: 10.5, color: '#9eb9ff' }, children: [vertexCount, " verts / ", triangleCount, " tris \u2014 picking a preset replaces this mesh"] }))] }) }));
348
- })(), _jsx("div", { style: {
324
+ }) }) })] })), system.renderMode === RenderMode.StretchedBillBoard && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Speed factor", children: _jsx(NumberField, { value: system.rendererEmitterSettings.speedFactor ?? 0, step: 0.1, onChange: (v) => binding.apply((s) => (s.rendererEmitterSettings.speedFactor = v)) }) }), _jsx(Row, { label: "Length factor", children: _jsx(NumberField, { value: system.rendererEmitterSettings.lengthFactor ?? 2, step: 0.1, onChange: (v) => binding.apply((s) => (s.rendererEmitterSettings.lengthFactor = v)) }) })] })), _jsxs("div", { style: {
349
325
  marginTop: 10,
350
326
  marginBottom: 2,
351
327
  fontSize: 11,
@@ -353,15 +329,15 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
353
329
  color: '#7c8db5',
354
330
  textTransform: 'uppercase',
355
331
  letterSpacing: 0.4,
356
- }, children: "Material" }), system.material != null && (_jsxs("div", { style: { fontSize: 10.5, color: '#9eb9ff', marginBottom: 2 }, children: ["Imported material: ", system.material.name ?? 'unnamed', " \u2014 settings below were derived from it"] })), _jsx(Row, { label: "Blend mode", children: _jsx(SelectField, { value: system.blending, options: [
332
+ display: 'flex',
333
+ alignItems: 'center',
334
+ gap: 5,
335
+ }, children: ["Material", _jsx(InspectorInfoIcon, { content: "Shader and blending settings applied to the particle sprite or mesh.", size: 12 })] }), materialLabel ? (_jsx(Row, { label: "Source", children: _jsx("div", { style: { fontSize: 12, color: theme.textDim, lineHeight: 1.45 }, children: materialLabel }) })) : null, _jsx(Row, { label: "Blend mode", children: _jsx(SelectField, { value: system.blending, options: [
357
336
  { value: 1, label: 'Additive' },
358
337
  { value: 2, label: 'Alpha blend' },
359
338
  { value: 3, label: 'Subtract' },
360
339
  { value: 4, label: 'Multiply' },
361
- ], onChange: (blend) => binding.apply((s) => (s.blending = blend)) }) }), _jsx(Row, { label: "Transparent", children: _jsx(CheckboxField, { value: !!system.getRendererSettings().materialTransparent, onChange: (v) => binding.apply((s) => {
362
- s.getRendererSettings().materialTransparent = v;
363
- s.neededToUpdateRender = true;
364
- }) }) }), resolveTexture && (_jsxs(Row, { label: "Texture", children: [_jsx(SelectField, { value: !currentTextureUrl
340
+ ], onChange: (blend) => patchMaterial({ blendMode: blend }) }) }), _jsx(Row, { label: "Transparent", children: _jsx(CheckboxField, { value: !!system.getRendererSettings().materialTransparent, onChange: (v) => patchMaterial({ transparent: v }) }) }), _jsxs(Row, { label: "Texture", children: [_jsx(SelectField, { value: !currentTextureUrl
365
341
  ? '__none'
366
342
  : (textureOptions ?? []).some((o) => o.url === currentTextureUrl)
367
343
  ? currentTextureUrl
@@ -378,7 +354,7 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
378
354
  return;
379
355
  }
380
356
  if (url === '__none') {
381
- binding.apply((s) => (s.texture = null));
357
+ patchMaterial({ texture: null });
382
358
  return;
383
359
  }
384
360
  if (url === '__url') {
@@ -394,20 +370,27 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
394
370
  if (!file) {
395
371
  return;
396
372
  }
397
- // Read as a data URI so the texture survives Export/Save/reimport;
398
- // an object URL (blob:) would be dead after a reload.
399
373
  const reader = new FileReader();
400
374
  reader.onload = () => {
401
- binding.apply((s) => (s.texture = resolveTexture(reader.result)));
375
+ const texture = loadTexture(reader.result);
376
+ if (texture) {
377
+ patchMaterial({ texture });
378
+ }
402
379
  };
403
380
  reader.readAsDataURL(file);
404
381
  };
405
382
  input.click();
406
383
  return;
407
384
  }
408
- binding.apply((s) => (s.texture = resolveTexture(url)));
385
+ const texture = loadTexture(url);
386
+ if (texture) {
387
+ patchMaterial({ texture });
388
+ }
409
389
  } }), promptingTextureUrl && (_jsx(PromptDialog, { title: "Texture URL", defaultValue: currentTextureUrl, placeholder: "https://\u2026", onSubmit: (url) => {
410
- binding.apply((s) => (s.texture = resolveTexture(url)));
390
+ const texture = loadTexture(url);
391
+ if (texture) {
392
+ patchMaterial({ texture });
393
+ }
411
394
  setPromptingTextureUrl(false);
412
395
  }, onCancel: () => setPromptingTextureUrl(false) })), currentTextureUrl && (_jsx("img", { src: currentTextureUrl, alt: "texture preview", style: {
413
396
  width: 64,
@@ -416,18 +399,63 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
416
399
  objectFit: 'contain',
417
400
  borderRadius: 6,
418
401
  border: '1px solid #2b3761',
419
- // Checkerboard so texture transparency is visible.
420
402
  background: 'repeating-conic-gradient(#2a3252 0% 25%, #171d33 0% 50%) 0 0 / 12px 12px',
421
- } }))] })), _jsx(Row, { label: "Render order", children: _jsx(NumberField, { value: system.renderOrder, step: 1, onChange: (v) => binding.apply((s) => (s.renderOrder = Math.round(v))) }) }), _jsx(Row, { label: "Soft particles", children: _jsx(CheckboxField, { value: system.softParticles, onChange: (v) => binding.apply((s) => (s.softParticles = v)) }) }), system.softParticles && (_jsx(Row, { label: "Fade near/far", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: system.softNearFade, min: 0, onChange: (v) => binding.apply((s) => (s.softNearFade = v)) }), _jsx(NumberField, { value: system.softFarFade, min: 0, onChange: (v) => binding.apply((s) => (s.softFarFade = v)) })] }) })), _jsx(Row, { label: "Alpha test", children: _jsx(NumberField, { value: system.getRendererSettings().materialAlphaTest ?? 0, min: 0, step: 0.05, onChange: (v) => binding.apply((s) => {
422
- s.getRendererSettings().materialAlphaTest = Math.min(1, v);
423
- s.neededToUpdateRender = true;
424
- }) }) }), _jsx(Row, { label: "Depth write", children: _jsx(CheckboxField, { value: system.getRendererSettings().materialDepthWrite, onChange: (v) => binding.apply((s) => {
425
- s.getRendererSettings().materialDepthWrite = v;
426
- s.neededToUpdateRender = true;
427
- }) }) }), _jsx(Row, { label: "Depth test", children: _jsx(CheckboxField, { value: system.getRendererSettings().materialDepthTest, onChange: (v) => binding.apply((s) => {
428
- s.getRendererSettings().materialDepthTest = v;
429
- s.neededToUpdateRender = true;
430
- }) }) })] }));
403
+ } }))] }), _jsx(Row, { label: "Alpha test", children: _jsx(NumberField, { value: system.getRendererSettings().materialAlphaTest ?? 0, min: 0, step: 0.05, onChange: (v) => patchMaterial({ alphaTest: Math.min(1, v) }) }) }), _jsx(Row, { label: "Depth write", children: _jsx(CheckboxField, { value: system.getRendererSettings().materialDepthWrite, onChange: (v) => patchMaterial({ depthWrite: v }) }) }), _jsx(Row, { label: "Depth test", children: _jsx(CheckboxField, { value: system.getRendererSettings().materialDepthTest, onChange: (v) => patchMaterial({ depthTest: v }) }) }), system.renderMode === RenderMode.Mesh &&
404
+ (() => {
405
+ const preset = detectGeometryPreset(system.instancingGeometry);
406
+ const vertexCount = system.instancingGeometry.length / 3;
407
+ const triangleCount = (system.getRendererSettings().instancingIndices?.length ?? 0) / 3;
408
+ const hostGeometry = geometryOptions ?? [];
409
+ const applyGeometry = (g) => binding.apply((s) => {
410
+ const settings = s.getRendererSettings();
411
+ settings.instancingIndices = (g.indices ? new Uint32Array(g.indices) : undefined);
412
+ settings.instancingUVs = (g.uvs ? new Float32Array(g.uvs) : undefined);
413
+ settings.instancingNormals = (g.normals ? new Float32Array(g.normals) : undefined);
414
+ s.instancingGeometry = new Float32Array(g.positions);
415
+ });
416
+ return (_jsx(Row, { label: "Geometry", children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: [_jsx(SelectField, { value: preset, options: [
417
+ ...(preset === 'custom' ? [{ value: 'custom', label: 'Custom (imported)' }] : []),
418
+ { value: 'quad', label: 'Quad' },
419
+ { value: 'cube', label: 'Cube' },
420
+ ...hostGeometry.map((g, i) => ({ value: `host:${i}`, label: g.label })),
421
+ ...(resolveGeometry ? [{ value: '__file', label: 'Load from file…' }] : []),
422
+ ], onChange: (next) => {
423
+ if (next === 'custom') {
424
+ return;
425
+ }
426
+ if (next === '__file') {
427
+ const input = document.createElement('input');
428
+ input.type = 'file';
429
+ input.accept = '.glb,.gltf,model/gltf-binary,model/gltf+json';
430
+ input.onchange = () => {
431
+ const file = input.files?.[0];
432
+ if (file) {
433
+ resolveGeometry(file).then(applyGeometry);
434
+ }
435
+ };
436
+ input.click();
437
+ return;
438
+ }
439
+ if (next.startsWith('host:')) {
440
+ applyGeometry(hostGeometry[Number(next.slice(5))]);
441
+ return;
442
+ }
443
+ binding.apply((s) => {
444
+ const settings = s.getRendererSettings();
445
+ if (next === 'cube') {
446
+ settings.instancingIndices = MESH_CUBE_INDICES;
447
+ settings.instancingUVs = undefined;
448
+ settings.instancingNormals = undefined;
449
+ s.instancingGeometry = MESH_CUBE_POSITIONS;
450
+ }
451
+ else {
452
+ settings.instancingIndices = MESH_QUAD_INDICES;
453
+ settings.instancingUVs = MESH_QUAD_UVS;
454
+ s.instancingGeometry = MESH_QUAD_POSITIONS;
455
+ }
456
+ });
457
+ } }), _jsx(MeshPreview, { positions: system.instancingGeometry, indices: system.getRendererSettings().instancingIndices }), preset === 'custom' && (_jsxs("span", { style: { fontSize: 10.5, color: '#9eb9ff' }, children: [vertexCount, " verts / ", triangleCount, " tris \u2014 picking a preset replaces this mesh"] }))] }) }));
458
+ })(), _jsx(Row, { label: "Render order", children: _jsx(NumberField, { value: system.renderOrder, step: 1, onChange: (v) => binding.apply((s) => (s.renderOrder = Math.round(v))) }) }), _jsx(Row, { label: "Soft particles", children: _jsx(CheckboxField, { value: system.softParticles, onChange: (v) => binding.apply((s) => (s.softParticles = v)) }) }), system.softParticles && (_jsx(Row, { label: "Fade near/far", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: system.softNearFade, min: 0, onChange: (v) => binding.apply((s) => (s.softNearFade = v)) }), _jsx(NumberField, { value: system.softFarFade, min: 0, onChange: (v) => binding.apply((s) => (s.softFarFade = v)) })] }) }))] }));
431
459
  }
432
460
  function removeBehavior(system, type) {
433
461
  const behaviors = system.behaviors;
@@ -15,6 +15,8 @@ export declare const inputStyle: CSSProperties;
15
15
  export declare const rowStyle: CSSProperties;
16
16
  export declare const labelStyle: CSSProperties;
17
17
  export declare const buttonStyle: CSSProperties;
18
+ /** Visually hides a file input while keeping it activatable via an associated <label>. */
19
+ export declare const hiddenFileInputStyle: CSSProperties;
18
20
  /**
19
21
  * Two hover conventions, applied via className (inline `style` can't express `:hover`):
20
22
  * - `qe-hover` — brightens elements that already have a solid inline background/border
@@ -42,10 +42,22 @@ export const buttonStyle = {
42
42
  color: '#d8e3ff',
43
43
  padding: '6px 10px',
44
44
  borderRadius: 8,
45
- fontSize: 12.5,
46
45
  cursor: 'pointer',
46
+ fontSize: 12.5,
47
47
  fontFamily: theme.font,
48
48
  };
49
+ /** Visually hides a file input while keeping it activatable via an associated <label>. */
50
+ export const hiddenFileInputStyle = {
51
+ position: 'absolute',
52
+ width: 1,
53
+ height: 1,
54
+ padding: 0,
55
+ margin: -1,
56
+ overflow: 'hidden',
57
+ clip: 'rect(0, 0, 0, 0)',
58
+ whiteSpace: 'nowrap',
59
+ border: 0,
60
+ };
49
61
  /**
50
62
  * Two hover conventions, applied via className (inline `style` can't express `:hover`):
51
63
  * - `qe-hover` — brightens elements that already have a solid inline background/border
@@ -98,4 +110,44 @@ select.qe-hover:hover, input.qe-hover:hover, select.qe-hover:focus, input.qe-hov
98
110
  .qe-root ::-webkit-scrollbar-corner {
99
111
  background: transparent;
100
112
  }
113
+ .qe-tooltip {
114
+ z-index: 100000;
115
+ padding: 8px 10px;
116
+ font-size: 11.5px;
117
+ line-height: 1.45;
118
+ color: ${theme.text};
119
+ background: linear-gradient(180deg, rgba(18, 26, 52, 0.98), rgba(8, 12, 26, 0.98));
120
+ border: 1px solid ${theme.borderActive};
121
+ border-radius: 8px;
122
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
123
+ pointer-events: none;
124
+ font-family: ${theme.font};
125
+ animation: qe-tooltip-in 0.12s ease;
126
+ }
127
+ .qe-tooltip--right::before,
128
+ .qe-tooltip--left::before {
129
+ content: '';
130
+ position: absolute;
131
+ top: 50%;
132
+ width: 0;
133
+ height: 0;
134
+ border: 5px solid transparent;
135
+ transform: translateY(-50%);
136
+ }
137
+ .qe-tooltip--right::before {
138
+ left: -10px;
139
+ border-right-color: ${theme.borderActive};
140
+ }
141
+ .qe-tooltip--left::before {
142
+ right: -10px;
143
+ border-left-color: ${theme.borderActive};
144
+ }
145
+ @keyframes qe-tooltip-in {
146
+ from {
147
+ opacity: 0;
148
+ }
149
+ to {
150
+ opacity: 1;
151
+ }
152
+ }
101
153
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babylon.quarks-editor",
3
- "version": "0.17.8",
3
+ "version": "0.18.0",
4
4
  "description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,17 +52,21 @@
52
52
  "peerDependencies": {
53
53
  "@babylonjs/core": ">=9.0.0",
54
54
  "@babylonjs/materials": ">=9.0.0",
55
+ "@heroicons/react": ">=2.0.0",
56
+ "@tanstack/react-virtual": ">=3.0.0",
55
57
  "babylon.quarks": ">=0.17.0",
56
58
  "react": ">=18",
57
59
  "react-dom": ">=18"
58
60
  },
59
61
  "devDependencies": {
60
62
  "@babylonjs/materials": "9.6.2",
63
+ "@heroicons/react": "^2.2.0",
64
+ "@tanstack/react-virtual": "^3.14.6",
61
65
  "@types/react": "^19.0.0",
66
+ "@types/react-dom": "^19.0.0",
62
67
  "react": "^19.0.0",
63
- "rimraf": "^6.0.0",
64
- "typescript": "^5.9.3",
65
68
  "react-dom": "^19.0.0",
66
- "@types/react-dom": "^19.0.0"
69
+ "rimraf": "^6.0.0",
70
+ "typescript": "^5.9.3"
67
71
  }
68
72
  }