vizcore 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +66 -648
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/index.html +2 -1
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/frontend/index.html +26 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/main.js +268 -0
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/renderer/engine.js +10 -1
- data/frontend/src/renderer/layer-manager.js +18 -4
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/lib/vizcore/cli/dsl_reference.rb +1 -1
- data/lib/vizcore/cli/scene_validator.rb +92 -0
- data/lib/vizcore/dsl/layer_builder.rb +795 -7
- data/lib/vizcore/dsl/mapping_resolver.rb +158 -4
- data/lib/vizcore/layer_catalog.rb +4 -2
- data/lib/vizcore/renderer/scene_frame_source.rb +14 -1
- data/lib/vizcore/renderer/snapshot_renderer.rb +507 -15
- data/lib/vizcore/server/frame_broadcaster.rb +53 -4
- data/lib/vizcore/server/runner.rb +21 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +1 -0
- data/sig/vizcore.rbs +100 -1
- metadata +12 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="Vizcore Ruby wasm playground for editing Ruby scene DSL and previewing audio-reactive visuals in the browser." />
|
|
7
|
+
<meta name="theme-color" content="#08110f" />
|
|
8
|
+
<link rel="canonical" href="https://ydah.github.io/vizcore/playground.html" />
|
|
9
|
+
<meta property="og:type" content="website" />
|
|
10
|
+
<meta property="og:title" content="Vizcore Playground" />
|
|
11
|
+
<meta property="og:description" content="Edit Vizcore Ruby DSL and preview audio-reactive scenes in the browser." />
|
|
12
|
+
<meta property="og:url" content="https://ydah.github.io/vizcore/playground.html" />
|
|
13
|
+
<meta property="og:image" content="https://ydah.github.io/vizcore/assets/vizcore-poster.png" />
|
|
14
|
+
<title>Vizcore Playground</title>
|
|
15
|
+
<link rel="stylesheet" href="assets/playground.css" />
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<a class="skip-link" href="#editor">Skip to editor</a>
|
|
19
|
+
|
|
20
|
+
<header class="topbar" aria-label="Playground header">
|
|
21
|
+
<a class="brand" href="index.html">Vizcore</a>
|
|
22
|
+
<nav class="topbar-nav" aria-label="Playground navigation">
|
|
23
|
+
<a href="index.html">Home</a>
|
|
24
|
+
<a href="https://github.com/ydah/vizcore">GitHub</a>
|
|
25
|
+
</nav>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<main class="playground-shell">
|
|
29
|
+
<section class="editor-panel" aria-labelledby="editor-title">
|
|
30
|
+
<div class="panel-header">
|
|
31
|
+
<div>
|
|
32
|
+
<p class="eyebrow">Ruby wasm</p>
|
|
33
|
+
<h1 id="editor-title">Playground</h1>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="toolbar">
|
|
36
|
+
<label class="preset-label" for="preset-select">Preset</label>
|
|
37
|
+
<select id="preset-select" class="preset-select"></select>
|
|
38
|
+
<button id="run-button" class="button primary" type="button">Run</button>
|
|
39
|
+
<button id="reset-button" class="button secondary" type="button">Reset</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<textarea id="editor" class="code-editor" spellcheck="false" autocomplete="off" autocapitalize="off"></textarea>
|
|
44
|
+
|
|
45
|
+
<div class="output-row">
|
|
46
|
+
<div class="status-line" aria-live="polite">
|
|
47
|
+
<span id="ruby-status">Ruby wasm idle</span>
|
|
48
|
+
<span id="compile-status">Waiting</span>
|
|
49
|
+
</div>
|
|
50
|
+
<details class="json-output">
|
|
51
|
+
<summary>Scene JSON</summary>
|
|
52
|
+
<pre id="json-output">{}</pre>
|
|
53
|
+
</details>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<section class="preview-panel" aria-labelledby="preview-title">
|
|
58
|
+
<div class="preview-header">
|
|
59
|
+
<div>
|
|
60
|
+
<p class="eyebrow">Canvas preview</p>
|
|
61
|
+
<h2 id="preview-title">Output</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<div id="scene-tabs" class="scene-tabs" aria-label="Scenes"></div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="canvas-wrap">
|
|
67
|
+
<canvas id="preview-canvas" width="1280" height="720" aria-label="Vizcore visual preview"></canvas>
|
|
68
|
+
<div class="preview-meter" aria-live="polite">
|
|
69
|
+
<span id="scene-stat">Scene: --</span>
|
|
70
|
+
<span id="audio-stat">Amplitude: --</span>
|
|
71
|
+
<span id="beat-stat">Beat: --</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div id="error-output" class="error-output" role="alert" hidden></div>
|
|
76
|
+
</section>
|
|
77
|
+
</main>
|
|
78
|
+
|
|
79
|
+
<script type="module" src="assets/playground.js"></script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
data/docs/shape_dsl.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Vizcore Shape DSL
|
|
2
|
+
|
|
3
|
+
Vizcore shape layers can declare 2D vector primitives directly in Ruby. Calling a
|
|
4
|
+
shape primitive inside a layer automatically marks that layer as `type: :shape`.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
Vizcore.define do
|
|
8
|
+
scene :logo do
|
|
9
|
+
layer :badge do
|
|
10
|
+
rect :panel, width: 360, height: 160, radius: 24 do
|
|
11
|
+
fill "#111827"
|
|
12
|
+
stroke width: 2, color: "#38bdf8"
|
|
13
|
+
opacity 0.8
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
star :spark, points: 5, radius: 72, inner_radius: 28 do
|
|
17
|
+
translate x: 180, y: 0
|
|
18
|
+
map beat_pulse, to: :scale, range: 0.8..1.4
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Primitives
|
|
26
|
+
|
|
27
|
+
Supported primitives:
|
|
28
|
+
|
|
29
|
+
- `circle id = nil, x: 0, y: 0, radius: 100, count: 1, segments: 96`
|
|
30
|
+
- `line id = nil, x1: -100, y1: 0, x2: 100, y2: 0`
|
|
31
|
+
- `rect id = nil, x: 0, y: 0, width:, height:, radius: 0`
|
|
32
|
+
- `polygon id = nil, points: [[x, y], ...], closed: true`
|
|
33
|
+
- `polyline id = nil, points: [[x, y], ...]`
|
|
34
|
+
- `path id = nil, detail: 32, tolerance: nil, max_segments: 4096 do ... end`
|
|
35
|
+
- `bezier id = nil, from:, control:, to:` for quadratic curves
|
|
36
|
+
- `bezier id = nil, from:, c1:, c2:, to:` for cubic curves
|
|
37
|
+
- `star id = nil, points: 5, radius: 100, inner_radius: 50`
|
|
38
|
+
- `custom_shape name_or_class, **params`
|
|
39
|
+
|
|
40
|
+
`draw do ... end` may be used to group declarations for readability.
|
|
41
|
+
`group do ... end` applies shared style and transform to child primitives.
|
|
42
|
+
|
|
43
|
+
## Path Commands
|
|
44
|
+
|
|
45
|
+
Path blocks support an SVG-like command subset:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
path :blob, detail: 48 do
|
|
49
|
+
move_to 0, 120
|
|
50
|
+
line_to 120, 0
|
|
51
|
+
quad_to 80, -100, 0, -120
|
|
52
|
+
cubic_to -80, -100, -120, 80, 0, 120
|
|
53
|
+
close
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Commands are serialized as `M`, `L`, `Q`, `C`, `H`, `V`, `A`, and `Z`. The
|
|
58
|
+
Canvas2D renderer draws `arc_to` as an SVG-style elliptical arc, and the line
|
|
59
|
+
fallback/snapshot renderer flattens curves and arcs to line segments.
|
|
60
|
+
`detail` controls the default curve/arc subdivision count. When `tolerance` is
|
|
61
|
+
provided, quadratic and cubic curves use adaptive flattening instead. `max_segments`
|
|
62
|
+
caps the number of flattened line segments per path; the DSL raises when the
|
|
63
|
+
estimated flattened path would exceed that budget.
|
|
64
|
+
|
|
65
|
+
## Style And Transform
|
|
66
|
+
|
|
67
|
+
Inside a shape block, these methods target the shape rather than the layer:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
fill "#f472b6"
|
|
71
|
+
stroke 2
|
|
72
|
+
stroke width: 3, color: "#ffffff"
|
|
73
|
+
blend :add
|
|
74
|
+
opacity 0.75
|
|
75
|
+
|
|
76
|
+
translate x: 100, y: 40
|
|
77
|
+
translate 100, 40
|
|
78
|
+
rotate 30
|
|
79
|
+
scale 1.2
|
|
80
|
+
scale x: 1.4, y: 0.8
|
|
81
|
+
origin x: 0, y: 0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The transform order is origin adjustment, scale, rotate, then translate.
|
|
85
|
+
|
|
86
|
+
## Groups
|
|
87
|
+
|
|
88
|
+
Groups are flattened into child primitives during DSL evaluation. Child shapes
|
|
89
|
+
inherit style values they do not set themselves, and group opacity is multiplied
|
|
90
|
+
with child opacity.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
group :spinner do
|
|
94
|
+
translate x: 0, y: 80
|
|
95
|
+
rotate 15
|
|
96
|
+
scale 1.2
|
|
97
|
+
opacity 0.75
|
|
98
|
+
stroke width: 2, color: "#38bdf8"
|
|
99
|
+
|
|
100
|
+
12.times do |index|
|
|
101
|
+
rect width: 12, height: 80 do
|
|
102
|
+
translate x: 0, y: 160
|
|
103
|
+
rotate index * 30
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Mapping
|
|
110
|
+
|
|
111
|
+
Mappings declared inside a shape block are scoped to that shape:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
circle :ring, radius: 120 do
|
|
115
|
+
map bass, to: :radius, range: 80..240
|
|
116
|
+
map beat_pulse, to: :scale, range: 1.0..1.4
|
|
117
|
+
map high, to: :opacity, range: 0.2..1.0
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Transform aliases:
|
|
122
|
+
|
|
123
|
+
- `:translate_x` -> `transform.translate.x`
|
|
124
|
+
- `:translate_y` -> `transform.translate.y`
|
|
125
|
+
- `:rotate` and `:rotation` -> `transform.rotate`
|
|
126
|
+
- `:scale` -> `transform.scale`
|
|
127
|
+
- `:scale_x` -> `transform.scale.x`
|
|
128
|
+
- `:scale_y` -> `transform.scale.y`
|
|
129
|
+
- `:origin_x` -> `transform.origin.x`
|
|
130
|
+
- `:origin_y` -> `transform.origin.y`
|
|
131
|
+
|
|
132
|
+
You can also map to a named shape from the layer scope:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
rect :panel, width: 360, height: 160
|
|
136
|
+
map bass, to: shape(:panel).rotate, range: -12..12
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Mappings inside a static `custom_shape` block are applied to each primitive
|
|
140
|
+
generated by that custom shape:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
custom_shape :badge, radius: 120 do
|
|
144
|
+
fill "#22d3ee"
|
|
145
|
+
map beat_pulse, to: :scale, range: 0.8..1.2
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Custom Shapes
|
|
150
|
+
|
|
151
|
+
Custom shapes are registered in Ruby and expanded into regular shape
|
|
152
|
+
primitives before the browser receives the scene.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class DiamondShape
|
|
156
|
+
include Vizcore::Shape
|
|
157
|
+
|
|
158
|
+
param :radius, default: 100, min: 10, max: 400
|
|
159
|
+
|
|
160
|
+
def draw(ctx)
|
|
161
|
+
radius = ctx.param(:radius)
|
|
162
|
+
|
|
163
|
+
ctx.polygon points: [
|
|
164
|
+
[0, radius],
|
|
165
|
+
[radius, 0],
|
|
166
|
+
[0, -radius],
|
|
167
|
+
[-radius, 0]
|
|
168
|
+
]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
Vizcore.register_shape :diamond, DiamondShape
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Use the registered shape from a layer:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
layer :generated_logo do
|
|
179
|
+
custom_shape :diamond, radius: 140 do
|
|
180
|
+
rotate 45
|
|
181
|
+
stroke width: 2, color: "#ffffff"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Block registration is supported for small generators:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
Vizcore.register_shape :spark do |ctx|
|
|
190
|
+
ctx.star points: 5, radius: ctx.param(:radius, 80), inner_radius: 32
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`draw(ctx)` may return a primitive hash, an array of primitive hashes, or build
|
|
195
|
+
primitives with `ctx.draw`, `ctx.circle`, `ctx.rect`, `ctx.path`, and the other
|
|
196
|
+
shape methods. Values declared with `param` are validated against their
|
|
197
|
+
`min`/`max` metadata before `draw(ctx)` runs, and dynamic custom shape
|
|
198
|
+
descriptors include the same schema for editor tooling.
|
|
199
|
+
|
|
200
|
+
By default, custom shapes are expanded when the DSL is evaluated. Use
|
|
201
|
+
`dynamic: true` when the generator needs mapped params, `ctx.time`, `ctx.frame`,
|
|
202
|
+
or `ctx.audio` at runtime:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
custom_shape :orbit, count: 32, radius: 260, dynamic: true do
|
|
206
|
+
fill "#22d3ee"
|
|
207
|
+
map bass, to: :radius, range: 120..280
|
|
208
|
+
map beat_pulse, to: :scale, range: 0.8..1.2
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Inside a dynamic custom shape block, mappings to custom params such as `:radius`
|
|
213
|
+
are applied before `draw(ctx)` runs. Transform aliases such as `:scale` and
|
|
214
|
+
`:rotate` are applied to the generated primitives after expansion.
|
|
215
|
+
|
|
216
|
+
Use `static: true` for generators that are independent of runtime context. Static
|
|
217
|
+
custom shapes with identical renderer, params, layer context, palette, and
|
|
218
|
+
resolution reuse a cached expansion, while each call still receives its own copy
|
|
219
|
+
of the generated primitives.
|
|
220
|
+
|
|
221
|
+
## Coordinates
|
|
222
|
+
|
|
223
|
+
New shape schema layers use center-origin logical coordinates by default:
|
|
224
|
+
|
|
225
|
+
- `x` increases to the right.
|
|
226
|
+
- `y` increases upward.
|
|
227
|
+
- `x: 0, y: 0` is the center of the canvas.
|
|
228
|
+
|
|
229
|
+
Legacy `circle` and `line` layers without `shape_schema_version: 2` keep the old
|
|
230
|
+
coordinate heuristic for compatibility with existing examples.
|
|
231
|
+
|
|
232
|
+
Set `units :ndc` on the layer to use normalized device coordinates directly.
|
|
233
|
+
|
|
234
|
+
## Validation
|
|
235
|
+
|
|
236
|
+
The DSL raises early for malformed primitives:
|
|
237
|
+
|
|
238
|
+
- duplicate shape IDs within a layer
|
|
239
|
+
- negative `radius`, `inner_radius`, `width`, `height`, or `stroke_width`
|
|
240
|
+
- `polygon` with fewer than 3 points
|
|
241
|
+
- `polyline` with fewer than 2 points
|
|
242
|
+
- `path` without commands
|
|
243
|
+
- `path` whose estimated flattened segment count exceeds `max_segments`
|
|
244
|
+
- unknown `custom_shape`
|
|
245
|
+
- custom shapes that return unsupported primitive kinds
|
|
246
|
+
|
|
247
|
+
`vizcore validate` also emits warnings for shape payloads that are renderable but
|
|
248
|
+
likely surprising: unsupported primitive kinds in raw `params[:shapes]`, fills
|
|
249
|
+
that line fallback will ignore, opacity values outside `0..1`, and zero or very
|
|
250
|
+
large scale values that will collapse or clamp.
|
|
251
|
+
|
|
252
|
+
## Renderer Notes
|
|
253
|
+
|
|
254
|
+
The browser renderer uses a Canvas2D shape backend composited through the
|
|
255
|
+
existing WebGL layer pipeline. It supports fill, stroke color/width, opacity,
|
|
256
|
+
dash, line caps/joins, transforms, and logical/ndc coordinates for the shape
|
|
257
|
+
schema.
|
|
258
|
+
|
|
259
|
+
If Canvas2D is unavailable, the browser falls back to the existing line renderer.
|
|
260
|
+
That fallback ignores fill and only approximates stroke geometry. The software
|
|
261
|
+
snapshot renderer also uses line flattening.
|
|
262
|
+
|
|
263
|
+
The browser HUD includes local shape editor controls for resolved shape layers:
|
|
264
|
+
primitive kind, translate, rotate, scale, opacity, fill, stroke color, and stroke
|
|
265
|
+
width can be adjusted in the running view without mutating the source DSL file.
|
|
266
|
+
Dynamic custom shape params with `param` metadata are also exposed as HUD
|
|
267
|
+
controls; changing one sends a runtime override to the server so the Ruby custom
|
|
268
|
+
shape is re-expanded on the next frame. The HUD also lists valid mapping target
|
|
269
|
+
paths for layer params, primitive params/transforms, and custom shape params.
|
data/frontend/index.html
CHANGED
|
@@ -409,6 +409,29 @@
|
|
|
409
409
|
text-align: right;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
.shader-param-controls details {
|
|
413
|
+
margin-top: 0.35rem;
|
|
414
|
+
padding-top: 0.3rem;
|
|
415
|
+
border-top: 1px solid rgba(153, 195, 255, 0.12);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.shader-param-controls summary {
|
|
419
|
+
cursor: pointer;
|
|
420
|
+
color: #d9e8ff;
|
|
421
|
+
font-size: 0.78rem;
|
|
422
|
+
font-weight: 700;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.shader-param-controls select,
|
|
426
|
+
.shader-param-controls input[type="color"] {
|
|
427
|
+
width: 100%;
|
|
428
|
+
min-height: 1.65rem;
|
|
429
|
+
border: 1px solid rgba(153, 195, 255, 0.28);
|
|
430
|
+
border-radius: 0.35rem;
|
|
431
|
+
background: rgba(2, 6, 14, 0.72);
|
|
432
|
+
color: var(--fg);
|
|
433
|
+
}
|
|
434
|
+
|
|
412
435
|
@media (max-width: 520px) {
|
|
413
436
|
.hud {
|
|
414
437
|
right: 1rem;
|
|
@@ -507,6 +530,9 @@
|
|
|
507
530
|
</div>
|
|
508
531
|
</div>
|
|
509
532
|
<div id="shader-param-controls" class="shader-param-controls" hidden aria-label="Shader parameter controls"></div>
|
|
533
|
+
<div id="shape-editor-controls" class="shader-param-controls" hidden aria-label="Shape editor controls"></div>
|
|
534
|
+
<div id="custom-shape-param-controls" class="shader-param-controls" hidden aria-label="Custom shape parameter controls"></div>
|
|
535
|
+
<div id="mapping-target-selector" class="shader-param-controls" hidden aria-label="Mapping target selector"></div>
|
|
510
536
|
<div id="scene-switcher" class="scene-switcher" hidden aria-label="Scene switcher"></div>
|
|
511
537
|
<button id="audio-toggle" type="button" hidden>Play Audio</button>
|
|
512
538
|
</section>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const customShapeParamControlEntries = (layers, overrides = {}) => {
|
|
2
|
+
const entries = [];
|
|
3
|
+
const layerList = Array.isArray(layers) ? layers : [];
|
|
4
|
+
|
|
5
|
+
layerList.forEach((layer, layerIndex) => {
|
|
6
|
+
const controls = Array.isArray(layer?.params?.custom_shape_controls) ? layer.params.custom_shape_controls : [];
|
|
7
|
+
if (!controls.length) return;
|
|
8
|
+
|
|
9
|
+
const layerKey = layerControlKey(layer, layerIndex);
|
|
10
|
+
const layerName = String(layer?.name || `layer_${layerIndex + 1}`);
|
|
11
|
+
controls.forEach((control, controlIndex) => {
|
|
12
|
+
const customShapeIndex = finiteIndex(control?.index, controlIndex);
|
|
13
|
+
const customShapeName = String(control?.name || `custom_shape_${customShapeIndex + 1}`);
|
|
14
|
+
const params = control?.params && typeof control.params === "object" ? control.params : {};
|
|
15
|
+
const schemaByName = paramSchemaByName(control?.param_schema);
|
|
16
|
+
const paramNames = [...new Set([...Object.keys(schemaByName), ...Object.keys(params)])].sort();
|
|
17
|
+
|
|
18
|
+
paramNames.forEach((paramName) => {
|
|
19
|
+
const schema = schemaByName[paramName] || {};
|
|
20
|
+
const baseValue = finiteNumber(params[paramName], finiteNumber(schema.default, 0));
|
|
21
|
+
const min = finiteNumber(schema.min, Math.min(0, baseValue));
|
|
22
|
+
const fallbackMax = Math.max(min + 1, Math.abs(baseValue) * 2, 1);
|
|
23
|
+
const max = Math.max(min, finiteNumber(schema.max, fallbackMax));
|
|
24
|
+
const step = positiveNumber(schema.step, Math.max((max - min) / 100, 0.01));
|
|
25
|
+
const value = finiteNumber(overrides?.[layerKey]?.[customShapeIndex]?.[paramName], baseValue);
|
|
26
|
+
|
|
27
|
+
entries.push({
|
|
28
|
+
key: `${layerKey}:custom_shapes.${customShapeIndex}.params.${paramName}`,
|
|
29
|
+
layerKey,
|
|
30
|
+
layerName,
|
|
31
|
+
customShapeIndex,
|
|
32
|
+
customShapeName,
|
|
33
|
+
paramName,
|
|
34
|
+
label: `${layerName}.${customShapeName}.${paramName}`,
|
|
35
|
+
target: `custom_shapes.${customShapeIndex}.params.${paramName}`,
|
|
36
|
+
min,
|
|
37
|
+
max,
|
|
38
|
+
step,
|
|
39
|
+
value: clamp(value, min, max),
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return entries;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const pruneCustomShapeParamOverrides = (overrides = {}, entries = []) => {
|
|
49
|
+
const validKeys = new Set(entries.map((entry) => `${entry.layerKey}:${entry.customShapeIndex}:${entry.paramName}`));
|
|
50
|
+
const next = {};
|
|
51
|
+
|
|
52
|
+
Object.entries(overrides || {}).forEach(([layerKey, controls]) => {
|
|
53
|
+
if (!controls || typeof controls !== "object") return;
|
|
54
|
+
|
|
55
|
+
Object.entries(controls).forEach(([customShapeIndex, params]) => {
|
|
56
|
+
if (!params || typeof params !== "object") return;
|
|
57
|
+
|
|
58
|
+
Object.entries(params).forEach(([paramName, value]) => {
|
|
59
|
+
if (!validKeys.has(`${layerKey}:${customShapeIndex}:${paramName}`) || !Number.isFinite(Number(value))) return;
|
|
60
|
+
|
|
61
|
+
next[layerKey] ||= {};
|
|
62
|
+
next[layerKey][customShapeIndex] ||= {};
|
|
63
|
+
next[layerKey][customShapeIndex][paramName] = Number(value);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return next;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const customShapeParamMessage = (entry, value) => ({
|
|
72
|
+
layer: entry.layerName,
|
|
73
|
+
custom_shape_index: entry.customShapeIndex,
|
|
74
|
+
param: entry.paramName,
|
|
75
|
+
value: clamp(finiteNumber(value, entry.value), entry.min, entry.max),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const layerControlKey = (layer, index) => `${index}:${String(layer?.name || "layer")}`;
|
|
79
|
+
|
|
80
|
+
const paramSchemaByName = (schema) => {
|
|
81
|
+
const output = {};
|
|
82
|
+
(Array.isArray(schema) ? schema : []).forEach((entry) => {
|
|
83
|
+
const name = String(entry?.name || "").trim();
|
|
84
|
+
if (!name) return;
|
|
85
|
+
|
|
86
|
+
output[name] = entry;
|
|
87
|
+
});
|
|
88
|
+
return output;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const finiteIndex = (value, fallback) => {
|
|
92
|
+
const numeric = Number(value);
|
|
93
|
+
return Number.isInteger(numeric) && numeric >= 0 ? numeric : fallback;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const finiteNumber = (value, fallback) => {
|
|
97
|
+
const numeric = Number(value);
|
|
98
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const positiveNumber = (value, fallback) => {
|
|
102
|
+
const numeric = finiteNumber(value, fallback);
|
|
103
|
+
return numeric > 0 ? numeric : fallback;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|