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
|
@@ -2,15 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "mapping_transform_builder"
|
|
4
4
|
require_relative "reaction_builder"
|
|
5
|
+
require_relative "../shape"
|
|
5
6
|
|
|
6
7
|
module Vizcore
|
|
7
8
|
module DSL
|
|
8
9
|
# Builder for one render layer in a scene.
|
|
9
10
|
class LayerBuilder
|
|
10
11
|
NO_ARGUMENT = Object.new.freeze
|
|
12
|
+
SHAPE_SCHEMA_VERSION = 2
|
|
11
13
|
MAPPING_SOURCE_KINDS = %i[
|
|
12
14
|
amplitude frequency_band fft_spectrum onset kick snare hihat beat beat_confidence beat_pulse beat_count bpm
|
|
13
15
|
].freeze
|
|
16
|
+
PATH_DEFAULT_DETAIL = 32
|
|
17
|
+
PATH_MIN_DETAIL = 4
|
|
18
|
+
PATH_MAX_DETAIL = 128
|
|
19
|
+
PATH_DEFAULT_MAX_SEGMENTS = 4096
|
|
20
|
+
SHAPE_TARGET_ALIASES = {
|
|
21
|
+
"translate_x" => "transform.translate.x",
|
|
22
|
+
"translate_y" => "transform.translate.y",
|
|
23
|
+
"rotate" => "transform.rotate",
|
|
24
|
+
"rotation" => "transform.rotate",
|
|
25
|
+
"scale" => "transform.scale",
|
|
26
|
+
"scale_x" => "transform.scale.x",
|
|
27
|
+
"scale_y" => "transform.scale.y",
|
|
28
|
+
"origin_x" => "transform.origin.x",
|
|
29
|
+
"origin_y" => "transform.origin.y"
|
|
30
|
+
}.freeze
|
|
31
|
+
SHAPE_STYLE_KEYS = Vizcore::Shape::STYLE_KEYS
|
|
32
|
+
SHAPE_TRANSFORM_KEYS = %i[translate rotate rotation scale origin].freeze
|
|
33
|
+
|
|
34
|
+
# Reference to an already declared shape, used by `map ... to: shape(:id).radius`.
|
|
35
|
+
class ShapeReference
|
|
36
|
+
def initialize(prefix)
|
|
37
|
+
@prefix = prefix
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def method_missing(method_name, *args, &block)
|
|
41
|
+
return super unless args.empty? && block.nil?
|
|
42
|
+
|
|
43
|
+
target = SHAPE_TARGET_ALIASES.fetch(method_name.to_s, method_name.to_s)
|
|
44
|
+
:"#{@prefix}.#{target}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
14
51
|
|
|
15
52
|
# @param name [Symbol, String] layer identifier
|
|
16
53
|
# @param styles [Hash] reusable layer parameter styles
|
|
@@ -24,6 +61,8 @@ module Vizcore
|
|
|
24
61
|
@params = deep_dup(defaults)
|
|
25
62
|
@param_schema = {}
|
|
26
63
|
@mappings = []
|
|
64
|
+
@shape_index_by_id = {}
|
|
65
|
+
@shape_group_stack = [{}]
|
|
27
66
|
end
|
|
28
67
|
|
|
29
68
|
# Evaluate a layer block.
|
|
@@ -72,8 +111,8 @@ module Vizcore
|
|
|
72
111
|
# @param options [Hash] shape params such as `count`, `radius`, `x`, and `y`
|
|
73
112
|
# @yield optional block evaluated in the shape context
|
|
74
113
|
# @return [Hash]
|
|
75
|
-
def circle(**options, &block)
|
|
76
|
-
build_shape(:circle, options, &block)
|
|
114
|
+
def circle(id = nil, **options, &block)
|
|
115
|
+
build_shape(:circle, shape_options(id, options), &block)
|
|
77
116
|
end
|
|
78
117
|
|
|
79
118
|
# Declare a 2D line primitive for a shape layer.
|
|
@@ -81,8 +120,133 @@ module Vizcore
|
|
|
81
120
|
# @param options [Hash] shape params such as `x1`, `y1`, `x2`, and `y2`
|
|
82
121
|
# @yield optional block evaluated in the shape context
|
|
83
122
|
# @return [Hash]
|
|
84
|
-
def line(**options, &block)
|
|
85
|
-
build_shape(:line, options, &block)
|
|
123
|
+
def line(id = nil, **options, &block)
|
|
124
|
+
build_shape(:line, shape_options(id, options), &block)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Declare a 2D rectangle primitive for a shape layer.
|
|
128
|
+
#
|
|
129
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
130
|
+
# @param options [Hash] shape params such as `x`, `y`, `width`, `height`, and `radius`
|
|
131
|
+
# @yield optional block evaluated in the shape context
|
|
132
|
+
# @return [Hash]
|
|
133
|
+
def rect(id = nil, **options, &block)
|
|
134
|
+
build_shape(:rect, shape_options(id, options), schema_version: true, &block)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Declare a closed polygon primitive for a shape layer.
|
|
138
|
+
#
|
|
139
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
140
|
+
# @param options [Hash] shape params including `points`
|
|
141
|
+
# @yield optional block evaluated in the shape context
|
|
142
|
+
# @return [Hash]
|
|
143
|
+
def polygon(id = nil, **options, &block)
|
|
144
|
+
build_shape(:polygon, shape_options(id, options), schema_version: true, &block)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Declare an open polyline primitive for a shape layer.
|
|
148
|
+
#
|
|
149
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
150
|
+
# @param options [Hash] shape params including `points`
|
|
151
|
+
# @yield optional block evaluated in the shape context
|
|
152
|
+
# @return [Hash]
|
|
153
|
+
def polyline(id = nil, **options, &block)
|
|
154
|
+
build_shape(:polyline, shape_options(id, options).merge(closed: false), schema_version: true, &block)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Declare a path primitive using SVG-like path commands.
|
|
158
|
+
#
|
|
159
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
160
|
+
# @param options [Hash] path params such as `detail`
|
|
161
|
+
# @yield block containing path commands and shape styling
|
|
162
|
+
# @return [Hash]
|
|
163
|
+
def path(id = nil, **options, &block)
|
|
164
|
+
shape = shape_options(id, options)
|
|
165
|
+
shape[:commands] ||= []
|
|
166
|
+
build_shape(:path, shape, schema_version: true, &block)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Declare a quadratic or cubic bezier curve. The serialized primitive is a path.
|
|
170
|
+
#
|
|
171
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
172
|
+
# @param from [Array<Numeric>] start point
|
|
173
|
+
# @param to [Array<Numeric>] end point
|
|
174
|
+
# @param control [Array<Numeric>, nil] quadratic control point
|
|
175
|
+
# @param c1 [Array<Numeric>, nil] first cubic control point
|
|
176
|
+
# @param c2 [Array<Numeric>, nil] second cubic control point
|
|
177
|
+
# @param options [Hash] additional path params
|
|
178
|
+
# @yield optional block evaluated in the shape context
|
|
179
|
+
# @return [Hash]
|
|
180
|
+
def bezier(id = nil, from:, to:, control: nil, c1: nil, c2: nil, **options, &block)
|
|
181
|
+
commands = [["M", *point_values(from)]]
|
|
182
|
+
if control
|
|
183
|
+
commands << ["Q", *point_values(control), *point_values(to)]
|
|
184
|
+
elsif c1 && c2
|
|
185
|
+
commands << ["C", *point_values(c1), *point_values(c2), *point_values(to)]
|
|
186
|
+
else
|
|
187
|
+
raise ArgumentError, "bezier requires either :control or both :c1 and :c2"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
build_shape(:path, shape_options(id, options).merge(commands: commands), schema_version: true, &block)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Declare a star polygon primitive for a shape layer.
|
|
194
|
+
#
|
|
195
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
196
|
+
# @param options [Hash] shape params such as `points`, `radius`, and `inner_radius`
|
|
197
|
+
# @yield optional block evaluated in the shape context
|
|
198
|
+
# @return [Hash]
|
|
199
|
+
def star(id = nil, **options, &block)
|
|
200
|
+
build_shape(:star, shape_options(id, options), schema_version: true, &block)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Expand a registered Ruby custom shape into normal shape primitives.
|
|
204
|
+
#
|
|
205
|
+
# @param renderer [Symbol, String, Class, Module, #call] registered shape name or renderer
|
|
206
|
+
# @param options [Hash] custom shape params
|
|
207
|
+
# @yield optional block applied to each generated primitive
|
|
208
|
+
# @return [Array<Hash>]
|
|
209
|
+
def custom_shape(renderer, **options, &block)
|
|
210
|
+
mark_shape_schema_version!
|
|
211
|
+
shape_id = options.delete(:id)
|
|
212
|
+
dynamic = options.delete(:dynamic)
|
|
213
|
+
static = options.delete(:static)
|
|
214
|
+
raise ArgumentError, "custom_shape cannot be both static and dynamic" if dynamic && static
|
|
215
|
+
|
|
216
|
+
dynamic = true if static == false
|
|
217
|
+
return append_dynamic_custom_shape(renderer, options, shape_id: shape_id, &block) if dynamic
|
|
218
|
+
|
|
219
|
+
primitives = expand_custom_shape(renderer, options, shape_id: shape_id, cache: !!static)
|
|
220
|
+
raise ArgumentError, "custom_shape produced no primitives" if primitives.empty?
|
|
221
|
+
raise ArgumentError, "custom_shape id can only be assigned when one primitive is produced" if shape_id && primitives.length > 1
|
|
222
|
+
|
|
223
|
+
@type ||= :shape
|
|
224
|
+
@params[:shapes] ||= []
|
|
225
|
+
primitives.map do |primitive|
|
|
226
|
+
primitive[:id] ||= shape_id.to_sym if shape_id
|
|
227
|
+
append_expanded_shape(primitive, &block)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Apply shared style and transform to shape primitives declared in the block.
|
|
232
|
+
#
|
|
233
|
+
# Group attributes are flattened into child primitives so the frontend only
|
|
234
|
+
# needs to render regular shape primitives.
|
|
235
|
+
#
|
|
236
|
+
# @param id [Symbol, String, nil] optional group identifier, currently documentation-only
|
|
237
|
+
# @param attrs [Hash] initial group style/transform attrs
|
|
238
|
+
# @yield shape declarations
|
|
239
|
+
# @return [Array<Hash>]
|
|
240
|
+
def group(_id = nil, **attrs, &block)
|
|
241
|
+
raise ArgumentError, "group requires a block" unless block
|
|
242
|
+
|
|
243
|
+
mark_shape_schema_version!
|
|
244
|
+
@type ||= :shape
|
|
245
|
+
@shape_group_stack << merge_shape_group(current_shape_group, normalize_shape_group(attrs))
|
|
246
|
+
instance_eval(&block)
|
|
247
|
+
@params[:shapes] || []
|
|
248
|
+
ensure
|
|
249
|
+
@shape_group_stack.pop if @shape_group_stack.length > 1
|
|
86
250
|
end
|
|
87
251
|
|
|
88
252
|
# Group shape primitives in a block for readability.
|
|
@@ -146,6 +310,22 @@ module Vizcore
|
|
|
146
310
|
# @param value [String] text fill color
|
|
147
311
|
# @return [String]
|
|
148
312
|
def fill(value)
|
|
313
|
+
if @current_shape
|
|
314
|
+
@current_shape[:fill] = value.to_s
|
|
315
|
+
mark_shape_schema_version!
|
|
316
|
+
return @current_shape
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
if @current_custom_shape
|
|
320
|
+
current_custom_shape_style[:fill] = value.to_s
|
|
321
|
+
return @current_custom_shape
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
if in_shape_group?
|
|
325
|
+
current_shape_group[:fill] = value.to_s
|
|
326
|
+
return current_shape_group
|
|
327
|
+
end
|
|
328
|
+
|
|
149
329
|
@params[:color] = value.to_s
|
|
150
330
|
end
|
|
151
331
|
|
|
@@ -160,6 +340,20 @@ module Vizcore
|
|
|
160
340
|
return @current_shape
|
|
161
341
|
end
|
|
162
342
|
|
|
343
|
+
if @current_custom_shape
|
|
344
|
+
current_custom_shape_style[:stroke] = normalize_non_negative_param_number(value, :stroke) unless value.equal?(NO_ARGUMENT)
|
|
345
|
+
current_custom_shape_style[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
346
|
+
current_custom_shape_style[:stroke_color] = color.to_s unless color.nil?
|
|
347
|
+
return @current_custom_shape
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
if in_shape_group?
|
|
351
|
+
current_shape_group[:stroke] = normalize_non_negative_param_number(value, :stroke) unless value.equal?(NO_ARGUMENT)
|
|
352
|
+
current_shape_group[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
353
|
+
current_shape_group[:stroke_color] = color.to_s unless color.nil?
|
|
354
|
+
return current_shape_group
|
|
355
|
+
end
|
|
356
|
+
|
|
163
357
|
@params[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
164
358
|
@params[:stroke_color] = color.to_s unless color.nil?
|
|
165
359
|
@params
|
|
@@ -177,9 +371,193 @@ module Vizcore
|
|
|
177
371
|
# @param value [Symbol, String] layer compositing mode
|
|
178
372
|
# @return [Symbol]
|
|
179
373
|
def blend(value)
|
|
374
|
+
if @current_shape
|
|
375
|
+
@current_shape[:blend] = value.to_sym
|
|
376
|
+
mark_shape_schema_version!
|
|
377
|
+
return @current_shape
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if @current_custom_shape
|
|
381
|
+
current_custom_shape_style[:blend] = value.to_sym
|
|
382
|
+
return @current_custom_shape
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
if in_shape_group?
|
|
386
|
+
current_shape_group[:blend] = value.to_sym
|
|
387
|
+
return current_shape_group
|
|
388
|
+
end
|
|
389
|
+
|
|
180
390
|
@params[:blend] = value.to_sym
|
|
181
391
|
end
|
|
182
392
|
|
|
393
|
+
# Set layer or shape opacity.
|
|
394
|
+
#
|
|
395
|
+
# @param value [Numeric]
|
|
396
|
+
# @return [Float, Hash]
|
|
397
|
+
def opacity(value)
|
|
398
|
+
if @current_shape
|
|
399
|
+
@current_shape[:opacity] = normalize_param_number(value, :opacity)
|
|
400
|
+
mark_shape_schema_version!
|
|
401
|
+
return @current_shape
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
if @current_custom_shape
|
|
405
|
+
current_custom_shape_style[:opacity] = normalize_param_number(value, :opacity)
|
|
406
|
+
return @current_custom_shape
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
if in_shape_group?
|
|
410
|
+
current_shape_group[:opacity] = current_shape_group.key?(:opacity) ? normalize_param_number(current_shape_group[:opacity], :opacity) * normalize_param_number(value, :opacity) : normalize_param_number(value, :opacity)
|
|
411
|
+
return current_shape_group
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
@params[:opacity] = normalize_param_number(value, :opacity)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Set a shape/layer translation transform.
|
|
418
|
+
#
|
|
419
|
+
# @param args [Array<Numeric>]
|
|
420
|
+
# @param x [Numeric, nil]
|
|
421
|
+
# @param y [Numeric, nil]
|
|
422
|
+
# @return [Hash]
|
|
423
|
+
def translate(*args, x: nil, y: nil)
|
|
424
|
+
values = normalize_xy_args(args, x: x, y: y, name: :translate)
|
|
425
|
+
if @current_shape
|
|
426
|
+
current_shape_transform[:translate] = values
|
|
427
|
+
return @current_shape
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
if @current_custom_shape
|
|
431
|
+
current_custom_shape_transform[:translate] = add_shape_xy(current_custom_shape_transform[:translate], values)
|
|
432
|
+
return @current_custom_shape
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
if in_shape_group?
|
|
436
|
+
current_shape_group_transform[:translate] = add_shape_xy(current_shape_group_transform[:translate], values)
|
|
437
|
+
return current_shape_group
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
@params[:translate] = values
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Set a shape/layer rotation transform in degrees.
|
|
444
|
+
#
|
|
445
|
+
# @param value [Numeric]
|
|
446
|
+
# @return [Float, Hash]
|
|
447
|
+
def rotate(value)
|
|
448
|
+
rotation = normalize_param_number(value, :rotate)
|
|
449
|
+
if @current_shape
|
|
450
|
+
current_shape_transform[:rotate] = rotation
|
|
451
|
+
return @current_shape
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
if @current_custom_shape
|
|
455
|
+
current_custom_shape_transform[:rotate] = normalize_param_number(current_custom_shape_transform[:rotate] || 0, :rotate) + rotation
|
|
456
|
+
return @current_custom_shape
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
if in_shape_group?
|
|
460
|
+
current_shape_group_transform[:rotate] = normalize_param_number(current_shape_group_transform[:rotate] || 0, :rotate) + rotation
|
|
461
|
+
return current_shape_group
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
@params[:rotate] = rotation
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Set a shape/layer scale transform.
|
|
468
|
+
#
|
|
469
|
+
# @param value [Numeric]
|
|
470
|
+
# @param x [Numeric, nil]
|
|
471
|
+
# @param y [Numeric, nil]
|
|
472
|
+
# @return [Float, Hash]
|
|
473
|
+
def scale(value = NO_ARGUMENT, x: nil, y: nil)
|
|
474
|
+
scale_value = normalize_scale_args(value, x: x, y: y)
|
|
475
|
+
if @current_shape
|
|
476
|
+
current_shape_transform[:scale] = scale_value
|
|
477
|
+
return @current_shape
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
if @current_custom_shape
|
|
481
|
+
current_custom_shape_transform[:scale] = multiply_shape_scale(current_custom_shape_transform[:scale], scale_value)
|
|
482
|
+
return @current_custom_shape
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
if in_shape_group?
|
|
486
|
+
current_shape_group_transform[:scale] = multiply_shape_scale(current_shape_group_transform[:scale], scale_value)
|
|
487
|
+
return current_shape_group
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
@params[:scale] = scale_value
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Set a shape/layer transform origin.
|
|
494
|
+
#
|
|
495
|
+
# @param args [Array<Numeric>]
|
|
496
|
+
# @param x [Numeric, nil]
|
|
497
|
+
# @param y [Numeric, nil]
|
|
498
|
+
# @return [Hash]
|
|
499
|
+
def origin(*args, x: nil, y: nil)
|
|
500
|
+
values = normalize_xy_args(args, x: x, y: y, name: :origin)
|
|
501
|
+
if @current_shape
|
|
502
|
+
current_shape_transform[:origin] = values
|
|
503
|
+
return @current_shape
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
if @current_custom_shape
|
|
507
|
+
current_custom_shape_transform[:origin] = values
|
|
508
|
+
return @current_custom_shape
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
if in_shape_group?
|
|
512
|
+
current_shape_group_transform[:origin] = values
|
|
513
|
+
return current_shape_group
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
@params[:origin] = values
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Return a reference object for mapping to a named shape.
|
|
520
|
+
#
|
|
521
|
+
# @param id [Symbol, String]
|
|
522
|
+
# @return [ShapeReference]
|
|
523
|
+
def shape(id)
|
|
524
|
+
key = id.to_sym
|
|
525
|
+
index = @shape_index_by_id.fetch(key) { raise ArgumentError, "unknown shape id: #{key.inspect}" }
|
|
526
|
+
ShapeReference.new("shapes.#{index}")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def move_to(x, y)
|
|
530
|
+
append_path_command("M", x, y)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def line_to(x, y)
|
|
534
|
+
append_path_command("L", x, y)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def quad_to(cx, cy, x, y)
|
|
538
|
+
append_path_command("Q", cx, cy, x, y)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def cubic_to(c1x, c1y, c2x, c2y, x, y)
|
|
542
|
+
append_path_command("C", c1x, c1y, c2x, c2y, x, y)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def horizontal_to(x)
|
|
546
|
+
append_path_command("H", x)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def vertical_to(y)
|
|
550
|
+
append_path_command("V", y)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def arc_to(rx, ry, rotation, large_arc, sweep, x, y)
|
|
554
|
+
append_path_command("A", rx, ry, rotation, large_arc, sweep, x, y)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def close
|
|
558
|
+
append_path_command("Z")
|
|
559
|
+
end
|
|
560
|
+
|
|
183
561
|
# Store an ordered color palette for this layer.
|
|
184
562
|
#
|
|
185
563
|
# @param colors [Array<String, Array<String>>] color values such as "#00ffff"
|
|
@@ -232,6 +610,7 @@ module Vizcore
|
|
|
232
610
|
# @raise [ArgumentError] when the mapping is empty or invalid
|
|
233
611
|
# @return [void]
|
|
234
612
|
def map(definition = nil, **options, &block)
|
|
613
|
+
definition, options = normalize_custom_shape_mapping(definition, options) if @custom_shape_target_prefix
|
|
235
614
|
definition, options = normalize_shape_mapping(definition, options) if @shape_target_prefix
|
|
236
615
|
|
|
237
616
|
if options.key?(:to)
|
|
@@ -405,6 +784,16 @@ module Vizcore
|
|
|
405
784
|
return args.first
|
|
406
785
|
end
|
|
407
786
|
|
|
787
|
+
if @current_custom_shape && block.nil? && args.length == 1
|
|
788
|
+
@current_custom_shape[:params][method_name.to_sym] = args.first
|
|
789
|
+
return args.first
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
if in_shape_group? && block.nil? && args.length == 1
|
|
793
|
+
current_shape_group[method_name.to_sym] = args.first
|
|
794
|
+
return args.first
|
|
795
|
+
end
|
|
796
|
+
|
|
408
797
|
if block.nil? && args.length == 1
|
|
409
798
|
@params[method_name.to_sym] = args.first
|
|
410
799
|
return args.first
|
|
@@ -414,25 +803,77 @@ module Vizcore
|
|
|
414
803
|
end
|
|
415
804
|
|
|
416
805
|
def respond_to_missing?(method_name, include_private = false)
|
|
417
|
-
@params.key?(method_name.to_sym) || super
|
|
806
|
+
!!@current_custom_shape || @params.key?(method_name.to_sym) || super
|
|
418
807
|
end
|
|
419
808
|
|
|
420
809
|
private
|
|
421
810
|
|
|
422
|
-
def build_shape(kind, options, &block)
|
|
811
|
+
def build_shape(kind, options, schema_version: false, &block)
|
|
423
812
|
@type ||= :shape
|
|
813
|
+
mark_shape_schema_version! if schema_version
|
|
424
814
|
shape = normalize_shape(kind, options)
|
|
425
815
|
@params[:shapes] ||= []
|
|
426
816
|
shape_index = @params[:shapes].length
|
|
817
|
+
register_shape_id!(shape, shape_index)
|
|
818
|
+
@params[:shapes] << shape
|
|
819
|
+
|
|
820
|
+
with_shape_context(shape, shape_index) do
|
|
821
|
+
instance_eval(&block) if block
|
|
822
|
+
end
|
|
823
|
+
apply_current_shape_group!(shape)
|
|
824
|
+
validate_shape!(shape)
|
|
825
|
+
|
|
826
|
+
shape
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def append_expanded_shape(shape, &block)
|
|
830
|
+
shape_index = @params[:shapes].length
|
|
831
|
+
register_shape_id!(shape, shape_index)
|
|
427
832
|
@params[:shapes] << shape
|
|
428
833
|
|
|
429
834
|
with_shape_context(shape, shape_index) do
|
|
430
835
|
instance_eval(&block) if block
|
|
431
836
|
end
|
|
837
|
+
apply_current_shape_group!(shape)
|
|
838
|
+
validate_shape!(shape)
|
|
432
839
|
|
|
433
840
|
shape
|
|
434
841
|
end
|
|
435
842
|
|
|
843
|
+
def append_dynamic_custom_shape(renderer, options, shape_id:, &block)
|
|
844
|
+
definition = custom_shape_definition(renderer)
|
|
845
|
+
@type ||= :shape
|
|
846
|
+
@params[:custom_shapes] ||= []
|
|
847
|
+
descriptor = {
|
|
848
|
+
name: definition.name || renderer,
|
|
849
|
+
renderer: definition.renderer,
|
|
850
|
+
params: deep_dup(options),
|
|
851
|
+
style: {},
|
|
852
|
+
transform: {},
|
|
853
|
+
dynamic: true
|
|
854
|
+
}
|
|
855
|
+
param_schema = custom_shape_param_schema(definition.renderer)
|
|
856
|
+
descriptor[:param_schema] = param_schema unless param_schema.empty?
|
|
857
|
+
descriptor[:shape_id] = shape_id.to_sym if shape_id
|
|
858
|
+
descriptor_index = @params[:custom_shapes].length
|
|
859
|
+
@params[:custom_shapes] << descriptor
|
|
860
|
+
|
|
861
|
+
with_custom_shape_context(descriptor, descriptor_index) do
|
|
862
|
+
instance_eval(&block) if block
|
|
863
|
+
end
|
|
864
|
+
apply_current_shape_group_to_custom_shape!(descriptor)
|
|
865
|
+
|
|
866
|
+
descriptor
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def shape_options(id, options)
|
|
870
|
+
return options if id.nil?
|
|
871
|
+
|
|
872
|
+
raise ArgumentError, "shape id specified twice" if options.key?(:id)
|
|
873
|
+
|
|
874
|
+
options.merge(id: id.to_sym)
|
|
875
|
+
end
|
|
876
|
+
|
|
436
877
|
def normalize_shape(kind, options)
|
|
437
878
|
shape = { kind: kind.to_sym }
|
|
438
879
|
options.each do |key, value|
|
|
@@ -441,6 +882,307 @@ module Vizcore
|
|
|
441
882
|
shape
|
|
442
883
|
end
|
|
443
884
|
|
|
885
|
+
def normalize_shape_group(attrs)
|
|
886
|
+
attrs.each_with_object({}) do |(key, value), group|
|
|
887
|
+
symbol_key = key.to_sym
|
|
888
|
+
if SHAPE_TRANSFORM_KEYS.include?(symbol_key)
|
|
889
|
+
transform_key = symbol_key == :rotation ? :rotate : symbol_key
|
|
890
|
+
group[:transform] ||= {}
|
|
891
|
+
group[:transform][transform_key] = value
|
|
892
|
+
else
|
|
893
|
+
group[symbol_key] = value
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def merge_shape_group(parent, child)
|
|
899
|
+
output = deep_dup(parent)
|
|
900
|
+
child.each do |key, value|
|
|
901
|
+
if key == :transform
|
|
902
|
+
output[:transform] = compose_shape_transform(output[:transform], value)
|
|
903
|
+
elsif key == :opacity && output.key?(:opacity)
|
|
904
|
+
output[:opacity] = normalize_param_number(output[:opacity], :opacity) * normalize_param_number(value, :opacity)
|
|
905
|
+
else
|
|
906
|
+
output[key] = deep_dup(value)
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
output
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def apply_current_shape_group!(shape)
|
|
913
|
+
group = current_shape_group
|
|
914
|
+
return shape if group.empty?
|
|
915
|
+
|
|
916
|
+
SHAPE_STYLE_KEYS.each do |key|
|
|
917
|
+
next unless group.key?(key)
|
|
918
|
+
|
|
919
|
+
if key == :opacity && shape.key?(:opacity)
|
|
920
|
+
shape[:opacity] = normalize_param_number(group[:opacity], :opacity) * normalize_param_number(shape[:opacity], :opacity)
|
|
921
|
+
else
|
|
922
|
+
shape[key] = deep_dup(group[key]) unless shape.key?(key)
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
shape[:transform] = compose_shape_transform(group[:transform], shape[:transform]) if group[:transform]
|
|
926
|
+
shape
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def apply_current_shape_group_to_custom_shape!(descriptor)
|
|
930
|
+
group = current_shape_group
|
|
931
|
+
return descriptor if group.empty?
|
|
932
|
+
|
|
933
|
+
style = descriptor[:style] ||= {}
|
|
934
|
+
SHAPE_STYLE_KEYS.each do |key|
|
|
935
|
+
next unless group.key?(key)
|
|
936
|
+
|
|
937
|
+
if key == :opacity && style.key?(:opacity)
|
|
938
|
+
style[:opacity] = normalize_param_number(group[:opacity], :opacity) * normalize_param_number(style[:opacity], :opacity)
|
|
939
|
+
else
|
|
940
|
+
style[key] = deep_dup(group[key]) unless style.key?(key)
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
descriptor[:transform] = compose_shape_transform(group[:transform], descriptor[:transform]) if group[:transform]
|
|
944
|
+
descriptor
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def compose_shape_transform(parent, child)
|
|
948
|
+
return deep_dup(child || {}) unless parent
|
|
949
|
+
|
|
950
|
+
child ||= {}
|
|
951
|
+
output = deep_dup(parent)
|
|
952
|
+
output[:translate] = add_shape_xy(parent[:translate], child[:translate]) if child.key?(:translate)
|
|
953
|
+
output[:origin] = child[:origin] if child.key?(:origin)
|
|
954
|
+
output[:rotate] = normalize_param_number(parent[:rotate] || 0, :rotate) + normalize_param_number(child[:rotate] || 0, :rotate) if child.key?(:rotate)
|
|
955
|
+
output[:scale] = multiply_shape_scale(parent[:scale], child[:scale]) if child.key?(:scale)
|
|
956
|
+
output
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def add_shape_xy(parent, child)
|
|
960
|
+
parent ||= {}
|
|
961
|
+
child ||= {}
|
|
962
|
+
{
|
|
963
|
+
x: normalize_param_number(parent[:x] || parent["x"] || 0, :"translate.x") + normalize_param_number(child[:x] || child["x"] || 0, :"translate.x"),
|
|
964
|
+
y: normalize_param_number(parent[:y] || parent["y"] || 0, :"translate.y") + normalize_param_number(child[:y] || child["y"] || 0, :"translate.y")
|
|
965
|
+
}
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def multiply_shape_scale(parent, child)
|
|
969
|
+
parent = shape_scale_pair(parent)
|
|
970
|
+
child = shape_scale_pair(child)
|
|
971
|
+
{ x: parent[:x] * child[:x], y: parent[:y] * child[:y] }
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def shape_scale_pair(value)
|
|
975
|
+
return { x: normalize_param_number(value[:x] || value["x"] || 1, :"scale.x"), y: normalize_param_number(value[:y] || value["y"] || 1, :"scale.y") } if value.is_a?(Hash)
|
|
976
|
+
|
|
977
|
+
scale = normalize_param_number(value || 1, :scale)
|
|
978
|
+
{ x: scale, y: scale }
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def current_shape_group
|
|
982
|
+
@shape_group_stack.last
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def current_shape_group_transform
|
|
986
|
+
current_shape_group[:transform] ||= {}
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def current_custom_shape_style
|
|
990
|
+
@current_custom_shape[:style] ||= {}
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def current_custom_shape_transform
|
|
994
|
+
@current_custom_shape[:transform] ||= {}
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
def in_shape_group?
|
|
998
|
+
@shape_group_stack.length > 1
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def validate_shape!(shape)
|
|
1002
|
+
validate_non_negative_shape_numbers!(shape)
|
|
1003
|
+
case shape.fetch(:kind)
|
|
1004
|
+
when :polygon
|
|
1005
|
+
validate_shape_points!(shape, minimum: 3)
|
|
1006
|
+
when :polyline
|
|
1007
|
+
validate_shape_points!(shape, minimum: 2)
|
|
1008
|
+
when :path
|
|
1009
|
+
validate_path_shape!(shape)
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def validate_path_shape!(shape)
|
|
1014
|
+
commands = Array(shape[:commands])
|
|
1015
|
+
raise ArgumentError, "Invalid path#{shape_label(shape)}: commands must not be empty" if commands.empty?
|
|
1016
|
+
|
|
1017
|
+
detail = normalized_path_integer(shape, :detail, PATH_DEFAULT_DETAIL).clamp(PATH_MIN_DETAIL, PATH_MAX_DETAIL)
|
|
1018
|
+
max_segments = normalized_path_integer(shape, :max_segments, PATH_DEFAULT_MAX_SEGMENTS)
|
|
1019
|
+
validate_path_tolerance!(shape)
|
|
1020
|
+
|
|
1021
|
+
segment_count = estimated_path_segments(commands, detail)
|
|
1022
|
+
return if segment_count <= max_segments
|
|
1023
|
+
|
|
1024
|
+
raise ArgumentError,
|
|
1025
|
+
"Invalid path#{shape_label(shape)}: max_segments exceeded (#{segment_count} > #{max_segments})"
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def normalized_path_integer(shape, key, default)
|
|
1029
|
+
value = shape.key?(key) ? shape[key] : default
|
|
1030
|
+
numeric = Integer(value)
|
|
1031
|
+
raise ArgumentError if numeric <= 0
|
|
1032
|
+
|
|
1033
|
+
numeric
|
|
1034
|
+
rescue ArgumentError, TypeError
|
|
1035
|
+
raise ArgumentError, "Invalid path#{shape_label(shape)}: #{key} must be a positive integer"
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def validate_path_tolerance!(shape)
|
|
1039
|
+
return unless shape.key?(:tolerance)
|
|
1040
|
+
|
|
1041
|
+
value = normalize_param_number(shape[:tolerance], :tolerance)
|
|
1042
|
+
return unless value.negative?
|
|
1043
|
+
|
|
1044
|
+
raise ArgumentError, "Invalid path#{shape_label(shape)}: tolerance must be non-negative"
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
def estimated_path_segments(commands, detail)
|
|
1048
|
+
current = false
|
|
1049
|
+
subpath_start = false
|
|
1050
|
+
commands.sum do |entry|
|
|
1051
|
+
command, *values = Array(entry)
|
|
1052
|
+
case command.to_s.upcase
|
|
1053
|
+
when "M"
|
|
1054
|
+
current = values.length >= 2
|
|
1055
|
+
subpath_start = current
|
|
1056
|
+
0
|
|
1057
|
+
when "L"
|
|
1058
|
+
current && values.length >= 2 ? 1 : 0
|
|
1059
|
+
when "H", "V"
|
|
1060
|
+
current && values.length >= 1 ? 1 : 0
|
|
1061
|
+
when "Q"
|
|
1062
|
+
current && values.length >= 4 ? detail : 0
|
|
1063
|
+
when "C"
|
|
1064
|
+
current && values.length >= 6 ? detail : 0
|
|
1065
|
+
when "A"
|
|
1066
|
+
current && values.length >= 7 ? detail : 0
|
|
1067
|
+
when "Z"
|
|
1068
|
+
current && subpath_start ? 1 : 0
|
|
1069
|
+
else
|
|
1070
|
+
0
|
|
1071
|
+
end
|
|
1072
|
+
end
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
def validate_non_negative_shape_numbers!(shape)
|
|
1076
|
+
%i[radius width height stroke_width inner_radius].each do |key|
|
|
1077
|
+
next unless shape.key?(key)
|
|
1078
|
+
|
|
1079
|
+
value = normalize_param_number(shape[key], key)
|
|
1080
|
+
raise ArgumentError, "Invalid #{shape.fetch(:kind)}#{shape_label(shape)}: #{key} must be non-negative" if value.negative?
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
def validate_shape_points!(shape, minimum:)
|
|
1085
|
+
points = Array(shape[:points])
|
|
1086
|
+
valid_points = points.count { |point| Array(point).length >= 2 }
|
|
1087
|
+
return if valid_points >= minimum
|
|
1088
|
+
|
|
1089
|
+
raise ArgumentError, "Invalid #{shape.fetch(:kind)}#{shape_label(shape)}: points must contain at least #{minimum} points"
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def shape_label(shape)
|
|
1093
|
+
shape[:id] ? " `#{shape[:id]}`" : ""
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def expand_custom_shape(renderer, options, shape_id:, cache: false)
|
|
1097
|
+
definition = custom_shape_definition(renderer)
|
|
1098
|
+
Vizcore::Shape.expand_custom_shape(
|
|
1099
|
+
definition.renderer,
|
|
1100
|
+
params: options,
|
|
1101
|
+
shape_id: shape_id,
|
|
1102
|
+
layer_name: @name,
|
|
1103
|
+
palette: Array(@params[:palette]),
|
|
1104
|
+
shape_name: definition.name || renderer,
|
|
1105
|
+
cache: cache
|
|
1106
|
+
)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def custom_shape_definition(renderer)
|
|
1110
|
+
return Vizcore::Shape::Definition.new(name: nil, renderer: renderer) unless renderer.is_a?(Symbol) || renderer.is_a?(String)
|
|
1111
|
+
|
|
1112
|
+
Vizcore.resolve_shape(renderer) || raise(ArgumentError, "Unknown custom shape: #{renderer.inspect}. Register it with `Vizcore.register_shape #{renderer.inspect}, ShapeClass`.")
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
def custom_shape_param_schema(renderer)
|
|
1116
|
+
return [] unless renderer.respond_to?(:shape_param_schema)
|
|
1117
|
+
|
|
1118
|
+
renderer.shape_param_schema.values.map(&:dup)
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
def register_shape_id!(shape, shape_index)
|
|
1122
|
+
id = shape[:id]
|
|
1123
|
+
return if id.nil?
|
|
1124
|
+
|
|
1125
|
+
key = id.to_sym
|
|
1126
|
+
raise ArgumentError, "duplicate shape id: #{key.inspect}" if @shape_index_by_id.key?(key)
|
|
1127
|
+
|
|
1128
|
+
@shape_index_by_id[key] = shape_index
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
def current_shape_transform
|
|
1132
|
+
mark_shape_schema_version!
|
|
1133
|
+
@current_shape[:transform] ||= {}
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def mark_shape_schema_version!
|
|
1137
|
+
@params[:shape_schema_version] ||= SHAPE_SCHEMA_VERSION
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
def normalize_xy_args(args, x:, y:, name:)
|
|
1141
|
+
if args.length == 2
|
|
1142
|
+
return { x: normalize_param_number(args[0], :"#{name}.x"), y: normalize_param_number(args[1], :"#{name}.y") }
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
if args.length == 1 && args.first.is_a?(Hash)
|
|
1146
|
+
values = args.first
|
|
1147
|
+
x = values.fetch(:x, values["x"])
|
|
1148
|
+
y = values.fetch(:y, values["y"])
|
|
1149
|
+
elsif args.any?
|
|
1150
|
+
raise ArgumentError, "#{name} expects x/y keywords or two numeric arguments"
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
{
|
|
1154
|
+
x: normalize_param_number(x || 0, :"#{name}.x"),
|
|
1155
|
+
y: normalize_param_number(y || 0, :"#{name}.y")
|
|
1156
|
+
}
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
def normalize_scale_args(value, x:, y:)
|
|
1160
|
+
if value.equal?(NO_ARGUMENT)
|
|
1161
|
+
return {
|
|
1162
|
+
x: normalize_param_number(x || 1, :"scale.x"),
|
|
1163
|
+
y: normalize_param_number(y || 1, :"scale.y")
|
|
1164
|
+
}
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
raise ArgumentError, "scale accepts either a value or x/y keywords" unless x.nil? && y.nil?
|
|
1168
|
+
|
|
1169
|
+
normalize_param_number(value, :scale)
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def append_path_command(command, *values)
|
|
1173
|
+
raise ArgumentError, "#{command} is only available inside a path shape" unless @current_shape&.fetch(:kind) == :path
|
|
1174
|
+
|
|
1175
|
+
@current_shape[:commands] ||= []
|
|
1176
|
+
@current_shape[:commands] << [command, *values]
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def point_values(value)
|
|
1180
|
+
values = Array(value)
|
|
1181
|
+
raise ArgumentError, "point must contain x and y" unless values.length == 2
|
|
1182
|
+
|
|
1183
|
+
values
|
|
1184
|
+
end
|
|
1185
|
+
|
|
444
1186
|
def with_shape_context(shape, shape_index)
|
|
445
1187
|
previous_shape = @current_shape
|
|
446
1188
|
previous_prefix = @shape_target_prefix
|
|
@@ -452,6 +1194,50 @@ module Vizcore
|
|
|
452
1194
|
@shape_target_prefix = previous_prefix
|
|
453
1195
|
end
|
|
454
1196
|
|
|
1197
|
+
def with_custom_shape_context(descriptor, descriptor_index)
|
|
1198
|
+
previous_custom_shape = @current_custom_shape
|
|
1199
|
+
previous_prefix = @custom_shape_target_prefix
|
|
1200
|
+
@current_custom_shape = descriptor
|
|
1201
|
+
@custom_shape_target_prefix = "custom_shapes.#{descriptor_index}"
|
|
1202
|
+
yield
|
|
1203
|
+
ensure
|
|
1204
|
+
@current_custom_shape = previous_custom_shape
|
|
1205
|
+
@custom_shape_target_prefix = previous_prefix
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def normalize_custom_shape_mapping(definition, options)
|
|
1209
|
+
if options.key?(:to)
|
|
1210
|
+
prefixed_options = options.dup
|
|
1211
|
+
prefixed_options[:to] = prefixed_custom_shape_target(prefixed_options[:to])
|
|
1212
|
+
return [definition, prefixed_options]
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
mapping = definition.nil? ? options : Hash(definition)
|
|
1216
|
+
prefixed_mapping = mapping.each_with_object({}) do |(source, target), output|
|
|
1217
|
+
output[source] = prefix_custom_shape_target_value(target)
|
|
1218
|
+
end
|
|
1219
|
+
[prefixed_mapping, {}]
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def prefix_custom_shape_target_value(target)
|
|
1223
|
+
return prefixed_custom_shape_target(target) unless target.is_a?(Hash)
|
|
1224
|
+
|
|
1225
|
+
target.merge(to: prefixed_custom_shape_target(target.fetch(:to)))
|
|
1226
|
+
rescue KeyError
|
|
1227
|
+
target
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
def prefixed_custom_shape_target(target)
|
|
1231
|
+
target_name = target.to_s
|
|
1232
|
+
return :"#{@custom_shape_target_prefix}.#{target_name}" if target_name.match?(/\A(?:params|style|transform)\./)
|
|
1233
|
+
|
|
1234
|
+
resolved_target = SHAPE_TARGET_ALIASES[target_name]
|
|
1235
|
+
return :"#{@custom_shape_target_prefix}.#{resolved_target}" if resolved_target
|
|
1236
|
+
return :"#{@custom_shape_target_prefix}.style.#{target_name}" if SHAPE_STYLE_KEYS.include?(target_name.to_sym)
|
|
1237
|
+
|
|
1238
|
+
:"#{@custom_shape_target_prefix}.params.#{target_name}"
|
|
1239
|
+
end
|
|
1240
|
+
|
|
455
1241
|
def normalize_shape_mapping(definition, options)
|
|
456
1242
|
if options.key?(:to)
|
|
457
1243
|
prefixed_options = options.dup
|
|
@@ -475,7 +1261,9 @@ module Vizcore
|
|
|
475
1261
|
end
|
|
476
1262
|
|
|
477
1263
|
def prefixed_shape_target(target)
|
|
478
|
-
|
|
1264
|
+
target_name = target.to_s
|
|
1265
|
+
resolved_target = SHAPE_TARGET_ALIASES.fetch(target_name, target_name)
|
|
1266
|
+
:"#{@shape_target_prefix}.#{resolved_target}"
|
|
479
1267
|
end
|
|
480
1268
|
|
|
481
1269
|
def resolved_type
|