sevgi-graphics 0.98.2 → 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/CHANGELOG.md +115 -1
- data/README.md +55 -1
- data/lib/sevgi/graphics/attribute.rb +15 -10
- data/lib/sevgi/graphics/auxiliary/canvas.rb +4 -4
- data/lib/sevgi/graphics/auxiliary/content.rb +5 -16
- data/lib/sevgi/graphics/auxiliary/paper.rb +52 -1
- data/lib/sevgi/graphics/{xml.rb → auxiliary/xml.rb} +4 -9
- data/lib/sevgi/graphics/auxiliary.rb +1 -2
- data/lib/sevgi/graphics/document/base.rb +3 -2
- data/lib/sevgi/graphics/document.rb +15 -27
- data/lib/sevgi/graphics/element.rb +23 -8
- data/lib/sevgi/graphics/mixtures/call.rb +9 -24
- data/lib/sevgi/graphics/mixtures/core.rb +19 -23
- data/lib/sevgi/graphics/mixtures/duplicate.rb +4 -16
- data/lib/sevgi/graphics/mixtures/export.rb +8 -2
- data/lib/sevgi/graphics/mixtures/hatch.rb +5 -5
- data/lib/sevgi/graphics/mixtures/identify.rb +1 -1
- data/lib/sevgi/graphics/mixtures/inkscape.rb +23 -10
- data/lib/sevgi/graphics/mixtures/rdf.rb +1 -1
- data/lib/sevgi/graphics/mixtures/render.rb +11 -95
- data/lib/sevgi/graphics/mixtures/save.rb +4 -4
- data/lib/sevgi/graphics/mixtures/style/declaration.rb +117 -0
- data/lib/sevgi/graphics/mixtures/style.rb +36 -0
- data/lib/sevgi/graphics/mixtures/symbols.rb +2 -2
- data/lib/sevgi/graphics/mixtures/tile.rb +4 -4
- data/lib/sevgi/graphics/mixtures/transform.rb +22 -3
- data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
- data/lib/sevgi/graphics/mixtures/wrappers.rb +49 -0
- data/lib/sevgi/graphics/mixtures.rb +1 -0
- data/lib/sevgi/graphics/version.rb +1 -1
- data/lib/sevgi/graphics.rb +23 -19
- metadata +21 -7
- data/lib/sevgi/graphics/auxiliary/path.rb +0 -44
- data/lib/sevgi/graphics/auxiliary/sizes.rb +0 -62
|
@@ -54,10 +54,10 @@ module Sevgi
|
|
|
54
54
|
|
|
55
55
|
# Saves rendered SVG when its content differs from the destination.
|
|
56
56
|
# Relative destinations are expanded before being returned. When a non-empty backup suffix is given, an
|
|
57
|
-
# existing destination is copied immediately before replacement
|
|
57
|
+
# existing destination is copied immediately before replacement. Unchanged saves leave both files untouched.
|
|
58
58
|
# Missing parent directories are created. An existing directory target uses the default file name.
|
|
59
59
|
# @example Save to a relative destination
|
|
60
|
-
# path = Sevgi::Graphics.SVG
|
|
60
|
+
# path = Sevgi::Graphics.SVG.Save("build/drawing.svg")
|
|
61
61
|
# path == File.expand_path("build/drawing.svg") # => true
|
|
62
62
|
# @param path [String, #to_path, nil] output path or existing directory
|
|
63
63
|
# @param default [String, #to_path, nil] default output path
|
|
@@ -74,7 +74,7 @@ module Sevgi
|
|
|
74
74
|
# @see Sevgi::Graphics::Document::Proto#call
|
|
75
75
|
def Save(path = nil, default: nil, backup_suffix: nil, **kwargs, &filter)
|
|
76
76
|
default = F.subext(EXT, caller_locations(1..1).first.path) if default.nil?
|
|
77
|
-
path =
|
|
77
|
+
path = F.output_path(path, default:, context: "Save")
|
|
78
78
|
|
|
79
79
|
Writer.(path, self.(**kwargs), backup_suffix:, &filter)
|
|
80
80
|
end
|
|
@@ -94,7 +94,7 @@ module Sevgi
|
|
|
94
94
|
# @raise [SystemCallError] when the destination cannot be read or written
|
|
95
95
|
# @see Sevgi::Graphics::Document::Proto#call
|
|
96
96
|
def Write(path, **kwargs, &filter)
|
|
97
|
-
path =
|
|
97
|
+
path = F.path(path, context: "Write path")
|
|
98
98
|
ArgumentError.("Write path must name a file") if ::File.directory?(path)
|
|
99
99
|
|
|
100
100
|
Writer.(path, self.(**kwargs), &filter)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Sevgi
|
|
6
|
+
module Graphics
|
|
7
|
+
module Mixtures
|
|
8
|
+
module Style
|
|
9
|
+
# Immutable authoring declaration for editable PDF paint properties.
|
|
10
|
+
# Source selectors stay here; the producer resolves them to PDF targets and baselines.
|
|
11
|
+
# @api private
|
|
12
|
+
class Declaration
|
|
13
|
+
# Captures a declaration without changing the SVG or loading a PDF backend.
|
|
14
|
+
# @param definition [Hash] targets, parameters, optional groups and profileSets
|
|
15
|
+
# @yield evaluates authoring operations in a private builder
|
|
16
|
+
# @yieldreturn [Object] ignored block result
|
|
17
|
+
# @return [void]
|
|
18
|
+
# @raise [Sevgi::ArgumentError] when the declaration is not finite, acyclic JSON data
|
|
19
|
+
def initialize(definition = {}, &block)
|
|
20
|
+
ArgumentError.("Style declaration must be a Hash") unless definition.is_a?(::Hash)
|
|
21
|
+
ArgumentError.("Style accepts a declaration or a block") if block && !definition.empty?
|
|
22
|
+
|
|
23
|
+
definition = Builder.new.call(&block) if block
|
|
24
|
+
@definition = Snapshot.capture(definition)
|
|
25
|
+
validate
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the owned, deeply frozen declaration with JSON String keys.
|
|
30
|
+
# @return [Hash{String => Object}] producer authoring declaration
|
|
31
|
+
def to_h = @definition
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate
|
|
36
|
+
unknown = @definition.keys - %w[targets parameters groups profileSets]
|
|
37
|
+
ArgumentError.("Unknown Style field: #{unknown.first}") unless unknown.empty?
|
|
38
|
+
return if %w[targets parameters].all? { @definition[it].is_a?(::Hash) }
|
|
39
|
+
ArgumentError.("Style requires targets and parameters")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Authoring operations kept outside the SVG method namespace.
|
|
43
|
+
# @api private
|
|
44
|
+
class Builder
|
|
45
|
+
def initialize
|
|
46
|
+
@definition = {targets: {}, parameters: {}, groups: [], profileSets: []}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def call(&block)
|
|
50
|
+
instance_exec(&block)
|
|
51
|
+
@definition
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def Target(id, selectors:, fill: false, display: Undefined)
|
|
55
|
+
record = {selectors:, fill:}
|
|
56
|
+
record[:display] = display unless display.equal?(Undefined)
|
|
57
|
+
add(:targets, id, record)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def Param(id, targets:, property:, schema:, value: Undefined)
|
|
61
|
+
record = {schema:, binding: {targets:, property:}}
|
|
62
|
+
record[:value] = value unless value.equal?(Undefined)
|
|
63
|
+
add(:parameters, id, record)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def Group(title, parameters:, description: nil)
|
|
67
|
+
record = {title:, parameters:}
|
|
68
|
+
record[:description] = description unless description.nil?
|
|
69
|
+
@definition[:groups] << record
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def Profiles(id, name:, parameters:, profiles:, description: nil)
|
|
73
|
+
record = {id:, name:, parameters:, profiles:}
|
|
74
|
+
record[:description] = description unless description.nil?
|
|
75
|
+
@definition[:profileSets] << record
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def add(field, id, value)
|
|
81
|
+
unless (id.is_a?(::String) || id.is_a?(::Symbol)) && !id.to_s.empty?
|
|
82
|
+
ArgumentError.("Style id must be nonempty text")
|
|
83
|
+
end
|
|
84
|
+
id = id.to_s
|
|
85
|
+
ArgumentError.("Duplicate Style id: #{id}") if @definition[field].key?(id)
|
|
86
|
+
@definition[field][id] = value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# JSON ownership rules differ from XML profile metadata.
|
|
91
|
+
# @api private
|
|
92
|
+
module Snapshot
|
|
93
|
+
def self.capture(value)
|
|
94
|
+
json = JSON.generate(value, strict: true, max_nesting: 32)
|
|
95
|
+
freeze_tree(JSON.parse(json, max_nesting: 32))
|
|
96
|
+
rescue JSON::JSONError => e
|
|
97
|
+
ArgumentError.("Invalid Style JSON: #{e.message}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.freeze_tree(value)
|
|
101
|
+
case value
|
|
102
|
+
when ::Hash then value.each_value { freeze_tree(it) }
|
|
103
|
+
when ::Array then value.each { freeze_tree(it) }
|
|
104
|
+
end
|
|
105
|
+
value.freeze
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private_class_method :freeze_tree
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private_constant :Builder, :Snapshot
|
|
112
|
+
end
|
|
113
|
+
private_constant :Declaration
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "style/declaration"
|
|
4
|
+
|
|
5
|
+
module Sevgi
|
|
6
|
+
module Graphics
|
|
7
|
+
module Mixtures
|
|
8
|
+
# Document-local declarations for editable PDF export.
|
|
9
|
+
module Style
|
|
10
|
+
# Reads or replaces this document's Style declaration.
|
|
11
|
+
# A declaration changes export metadata, not rendered SVG appearance.
|
|
12
|
+
# Ordinary document copies safely share the immutable declaration.
|
|
13
|
+
# @param definition [Hash, Sevgi::Undefined] declaration, or omitted for a read
|
|
14
|
+
# @yield defines Target, Param, Group, and Profiles records
|
|
15
|
+
# @yieldreturn [Object] ignored block result
|
|
16
|
+
# @return [Hash, nil, Sevgi::Graphics::Element] frozen JSON declaration on read, self on assignment
|
|
17
|
+
# @raise [Sevgi::ArgumentError] when called outside a document root or with an invalid declaration
|
|
18
|
+
# @example Declare an editable outline
|
|
19
|
+
# drawing.Style do
|
|
20
|
+
# Target "ink", selectors: ["#outline"]
|
|
21
|
+
# Param "display", targets: ["ink"], property: "display",
|
|
22
|
+
# schema: {type: "string", enum: %w[inline none]}
|
|
23
|
+
# Param "opacity", targets: ["ink"], property: "stroke-opacity",
|
|
24
|
+
# schema: {type: "number", minimum: 0, maximum: 1}
|
|
25
|
+
# end
|
|
26
|
+
def Style(definition = Undefined, &block)
|
|
27
|
+
ArgumentError.("Style belongs to a document root") unless Root?()
|
|
28
|
+
return @style&.to_h if definition.equal?(Undefined) && !block
|
|
29
|
+
definition = {} if definition.equal?(Undefined)
|
|
30
|
+
@style = Declaration.new(definition, &block)
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -65,7 +65,7 @@ module Sevgi
|
|
|
65
65
|
|
|
66
66
|
private_constant :Expansion
|
|
67
67
|
|
|
68
|
-
# Renders module callables as symbols under defs. Named modules default the defs id to their final constant name
|
|
68
|
+
# Renders module callables as symbols under defs. Named modules default the defs id to their final constant name.
|
|
69
69
|
# anonymous modules omit the id unless supplied.
|
|
70
70
|
# @param mod [Module] module extended with {Sevgi::Graphics::Module}
|
|
71
71
|
# @param args [Array<Object>] callable arguments
|
|
@@ -78,7 +78,7 @@ module Sevgi
|
|
|
78
78
|
# def tick = path d: "M 0 2 L 2 4 L 6 0"
|
|
79
79
|
# end
|
|
80
80
|
# Sevgi::Graphics.SVG(:minimal) { Symbols icons }
|
|
81
|
-
# @param attributes [Hash] defs attributes
|
|
81
|
+
# @param attributes [Hash] defs attributes. String and Symbol names are normalized and must not collide
|
|
82
82
|
# @param ids [#call, nil] optional callable mapping each method name to a symbol id
|
|
83
83
|
# @param kwargs [Hash] callable keyword arguments
|
|
84
84
|
# @yield forwarded to each callable
|
|
@@ -8,7 +8,7 @@ module Sevgi
|
|
|
8
8
|
# Use {Sevgi::Sundries::Tile} instead when Ruby code needs inspectable repeated geometry or row/column bounds
|
|
9
9
|
# rather than SVG references.
|
|
10
10
|
# @see Sevgi::Sundries::Tile
|
|
11
|
-
# @see https://sevgi.roktas.dev/
|
|
11
|
+
# @see https://sevgi.roktas.dev/layout/#choose-a-layout-model Choosing a layout model
|
|
12
12
|
module Tile
|
|
13
13
|
# Stable prefix used for generated tile CSS classes.
|
|
14
14
|
PREFIX = "tile"
|
|
@@ -30,7 +30,7 @@ module Sevgi
|
|
|
30
30
|
# @param dy [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
31
31
|
# @param oy [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
32
32
|
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, y:, nx:, ny:)`, with
|
|
33
|
-
# zero-based coordinates and total counts
|
|
33
|
+
# zero-based coordinates and total counts. The callback can mutate the element. Its return value is ignored
|
|
34
34
|
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
35
35
|
# @yieldreturn [Object] ignored block result
|
|
36
36
|
# @return [Sevgi::Graphics::Element] self
|
|
@@ -83,7 +83,7 @@ module Sevgi
|
|
|
83
83
|
# @param d [Numeric] finite horizontal spacing, normalized before coordinates are rendered
|
|
84
84
|
# @param o [Numeric] finite horizontal offset, normalized before coordinates are rendered
|
|
85
85
|
# @param proc [Proc, nil] optional callback invoked for each use as `(element, x:, n:)`, with a zero-based column
|
|
86
|
-
# and total count
|
|
86
|
+
# and total count. The callback can mutate the element. Its return value is ignored
|
|
87
87
|
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
88
88
|
# @yieldreturn [Object] ignored block result
|
|
89
89
|
# @return [Sevgi::Graphics::Element] self
|
|
@@ -117,7 +117,7 @@ module Sevgi
|
|
|
117
117
|
# @param d [Numeric] finite vertical spacing, normalized before coordinates are rendered
|
|
118
118
|
# @param o [Numeric] finite vertical offset, normalized before coordinates are rendered
|
|
119
119
|
# @param proc [Proc, nil] optional callback invoked for each use as `(element, y:, n:)`, with a zero-based row and
|
|
120
|
-
# total count
|
|
120
|
+
# total count. The callback can mutate the element. Its return value is ignored
|
|
121
121
|
# @yield evaluates the template drawing DSL in a generated `defs` group named by id
|
|
122
122
|
# @yieldreturn [Object] ignored block result
|
|
123
123
|
# @return [Sevgi::Graphics::Element] self
|
|
@@ -11,13 +11,18 @@ module Sevgi
|
|
|
11
11
|
# rect(width: 8, height: 4).Translate(12, 6).Rotate(15, 4, 2)
|
|
12
12
|
# end
|
|
13
13
|
module Transform
|
|
14
|
+
# Keep box validation here: mixture helper methods would also enter the SVG DSL namespace.
|
|
15
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
16
|
+
|
|
14
17
|
# Aligns an inner box inside an outer box.
|
|
15
18
|
# @param position [Symbol, String, nil] alignment name
|
|
16
|
-
# @param inner [#width, #height, nil] inner box
|
|
17
|
-
# @param outer [#width, #height, nil] outer box
|
|
19
|
+
# @param inner [#width, #height, nil] inner box, with optional position exposing x and y
|
|
20
|
+
# @param outer [#width, #height, nil] outer box, with optional position exposing x and y
|
|
18
21
|
# @return [Sevgi::Graphics::Element] self
|
|
19
22
|
# @raise [Sevgi::ArgumentError] when alignment is unsupported
|
|
20
23
|
# @raise [Sevgi::ArgumentError] when a box dimension is not a finite real number
|
|
24
|
+
# @raise [Sevgi::ArgumentError] when a supplied box position does not expose finite real x and y coordinates
|
|
25
|
+
# @note Size-only boxes have origin (0, 0). Positions describe known box geometry, not renderer-computed bounds.
|
|
21
26
|
def Align(position, inner:, outer:)
|
|
22
27
|
return self unless position && inner && outer
|
|
23
28
|
|
|
@@ -25,12 +30,26 @@ module Sevgi
|
|
|
25
30
|
when :center
|
|
26
31
|
dimensions = [inner.width, inner.height, outer.width, outer.height]
|
|
27
32
|
iw, ih, ow, oh = dimensions.map { Scalar.number(it, context: "alignment", field: :dimension) }
|
|
28
|
-
|
|
33
|
+
origins = [inner, outer].map do |box|
|
|
34
|
+
next [0, 0] unless box.respond_to?(:position)
|
|
35
|
+
|
|
36
|
+
origin = box.position
|
|
37
|
+
unless origin.respond_to?(:x) && origin.respond_to?(:y)
|
|
38
|
+
ArgumentError.("Alignment box position must expose x and y")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
[origin.x, origin.y].map { Scalar.number(it, context: "alignment", field: :position) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
(ix, iy), (ox, oy) = origins
|
|
45
|
+
Translate(ox - ix + ((ow - iw) / 2.0), oy - iy + ((oh - ih) / 2.0))
|
|
29
46
|
else
|
|
30
47
|
ArgumentError.("Unsupported alignment: #{position}")
|
|
31
48
|
end
|
|
32
49
|
end
|
|
33
50
|
|
|
51
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
52
|
+
|
|
34
53
|
# Appends a scale(-1, -1) transform.
|
|
35
54
|
# @return [Sevgi::Graphics::Element] self
|
|
36
55
|
def Flip
|
|
@@ -18,7 +18,7 @@ module Sevgi
|
|
|
18
18
|
# Adds an XML comment.
|
|
19
19
|
# @param comment [Object] comment text
|
|
20
20
|
# @return [Sevgi::Graphics::Element] floating comment element
|
|
21
|
-
# @raise [Sevgi::ArgumentError] when comment cannot be stringified as valid XML or
|
|
21
|
+
# @raise [Sevgi::ArgumentError] when comment cannot be stringified as valid XML or forms malformed markup
|
|
22
22
|
def Comment(comment)
|
|
23
23
|
comment = XML.text(comment, context: "XML comment")
|
|
24
24
|
|
|
@@ -29,7 +29,7 @@ module Sevgi
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Merges `-context` metadata from the document root, ancestors, and this element.
|
|
32
|
-
# Only the direct root-to-self ancestor chain participates
|
|
32
|
+
# Only the direct root-to-self ancestor chain participates. Sibling subtrees are ignored. When the same key is
|
|
33
33
|
# present on multiple chain elements, the nearest element to the receiver wins. Context remains available in
|
|
34
34
|
# memory but is omitted from rendered SVG. It is unrelated to the `_:` XML namespace syntax preserved by
|
|
35
35
|
# Derender.
|
|
@@ -5,6 +5,55 @@ module Sevgi
|
|
|
5
5
|
module Mixtures
|
|
6
6
|
# DSL wrappers for common SVG shapes and content patterns.
|
|
7
7
|
module Wrappers
|
|
8
|
+
# rubocop:disable Metrics/ParameterLists
|
|
9
|
+
|
|
10
|
+
# Builds an elliptical arc path ending at an absolute point.
|
|
11
|
+
# SVG resolves the ellipse from its endpoints, radii, and flags, including radius correction and zero radii.
|
|
12
|
+
# @example Draw an upper semicircle in screen coordinates
|
|
13
|
+
# Sevgi::Graphics.SVG { ArcTo x1: 0, y1: 10, x2: 20, y2: 10, rx: 10, ry: 10, sweep: true }
|
|
14
|
+
# @param x2 [Numeric] finite ending x coordinate
|
|
15
|
+
# @param y2 [Numeric] finite ending y coordinate
|
|
16
|
+
# @param rx [Numeric] finite local x radius
|
|
17
|
+
# @param ry [Numeric] finite local y radius
|
|
18
|
+
# @param x1 [Numeric] finite starting x coordinate
|
|
19
|
+
# @param y1 [Numeric] finite starting y coordinate
|
|
20
|
+
# @param rotation [Numeric] clockwise ellipse rotation in degrees
|
|
21
|
+
# @param large [Boolean] select the arc exceeding a half turn
|
|
22
|
+
# @param sweep [Boolean] select increasing parameter angles
|
|
23
|
+
# @return [Sevgi::Graphics::Element] path element
|
|
24
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or a flag is not Boolean
|
|
25
|
+
def ArcTo(x2:, y2:, rx:, ry:, x1: 0, y1: 0, rotation: 0, large: false, sweep: false, **)
|
|
26
|
+
x1, y1, x2, y2, rx, ry, rotation = Scalar.numbers([x1, y1, x2, y2, rx, ry, rotation], context: "absolute arc")
|
|
27
|
+
unless [large, sweep].all? { it.equal?(true) || it.equal?(false) }
|
|
28
|
+
ArgumentError.("Arc flags must be Boolean")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
path(d: "M #{x1} #{y1} A #{rx} #{ry} #{rotation} #{large ? 1 : 0} #{sweep ? 1 : 0} #{x2} #{y2}", **)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Builds an elliptical arc path ending at a relative offset.
|
|
35
|
+
# @param dx [Numeric] finite x displacement from the starting point
|
|
36
|
+
# @param dy [Numeric] finite y displacement from the starting point
|
|
37
|
+
# @param rx [Numeric] finite local x radius
|
|
38
|
+
# @param ry [Numeric] finite local y radius
|
|
39
|
+
# @param x [Numeric] finite starting x coordinate
|
|
40
|
+
# @param y [Numeric] finite starting y coordinate
|
|
41
|
+
# @param rotation [Numeric] clockwise ellipse rotation in degrees
|
|
42
|
+
# @param large [Boolean] select the arc exceeding a half turn
|
|
43
|
+
# @param sweep [Boolean] select increasing parameter angles
|
|
44
|
+
# @return [Sevgi::Graphics::Element] path element
|
|
45
|
+
# @raise [Sevgi::ArgumentError] when an operand is not finite real or a flag is not Boolean
|
|
46
|
+
# @see #ArcTo
|
|
47
|
+
def ArcBy(dx:, dy:, rx:, ry:, x: 0, y: 0, rotation: 0, large: false, sweep: false, **)
|
|
48
|
+
x, y, dx, dy, rx, ry, rotation = Scalar.numbers([x, y, dx, dy, rx, ry, rotation], context: "relative arc")
|
|
49
|
+
unless [large, sweep].all? { it.equal?(true) || it.equal?(false) }
|
|
50
|
+
ArgumentError.("Arc flags must be Boolean")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
path(d: "M #{x} #{y} a #{rx} #{ry} #{rotation} #{large ? 1 : 0} #{sweep ? 1 : 0} #{dx} #{dy}", **)
|
|
54
|
+
end
|
|
55
|
+
# rubocop:enable Metrics/ParameterLists
|
|
56
|
+
|
|
8
57
|
# Builds a line path ending at an absolute point.
|
|
9
58
|
# @example Render a rational coordinate as an SVG number
|
|
10
59
|
# Sevgi::Graphics.SVG { LineTo(x2: Rational(1, 2), y2: 1) }
|
|
@@ -63,6 +63,7 @@ require_relative "mixtures/polyfills"
|
|
|
63
63
|
require_relative "mixtures/rdf"
|
|
64
64
|
require_relative "mixtures/render"
|
|
65
65
|
require_relative "mixtures/save"
|
|
66
|
+
require_relative "mixtures/style"
|
|
66
67
|
require_relative "mixtures/symbols"
|
|
67
68
|
require_relative "mixtures/tile"
|
|
68
69
|
require_relative "mixtures/transform"
|
data/lib/sevgi/graphics.rb
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "sevgi/function"
|
|
4
4
|
|
|
5
|
-
require_relative "graphics/xml"
|
|
6
|
-
require_relative "graphics/attribute"
|
|
7
5
|
require_relative "graphics/auxiliary"
|
|
6
|
+
require_relative "graphics/attribute"
|
|
8
7
|
require_relative "graphics/element"
|
|
9
8
|
require_relative "graphics/mixtures"
|
|
10
9
|
|
|
@@ -15,7 +14,7 @@ require_relative "graphics/version"
|
|
|
15
14
|
module Sevgi
|
|
16
15
|
# SVG document builder and DSL namespace.
|
|
17
16
|
#
|
|
18
|
-
# A document profile controls root metadata and preambles
|
|
17
|
+
# A document profile controls root metadata and preambles. A {Canvas} controls
|
|
19
18
|
# physical size, margins, viewport, and viewBox. {Graphics.SVG} combines them
|
|
20
19
|
# into an element tree. Library code keeps that tree as an object until it
|
|
21
20
|
# explicitly renders, validates, saves, or exports it.
|
|
@@ -25,9 +24,9 @@ module Sevgi
|
|
|
25
24
|
# circle cx: 5, cy: 5, r: 4
|
|
26
25
|
# end
|
|
27
26
|
# drawing.Render #=> "<svg width=\"10\" height=\"10\">\n <circle cx=\"5\" cy=\"5\" r=\"4\"/>\n</svg>"
|
|
28
|
-
# @see https://sevgi.roktas.dev/
|
|
29
|
-
# @see https://sevgi.roktas.dev/
|
|
30
|
-
# @see https://sevgi.roktas.dev/
|
|
27
|
+
# @see https://sevgi.roktas.dev/documents/ SVG construction and profile guide
|
|
28
|
+
# @see https://sevgi.roktas.dev/compose/ Composition guide
|
|
29
|
+
# @see https://sevgi.roktas.dev/examples/ Runnable drawing examples
|
|
31
30
|
# @see Sevgi::Graphics::Document
|
|
32
31
|
# @see Sevgi::Graphics::Canvas
|
|
33
32
|
module Graphics
|
|
@@ -63,19 +62,18 @@ module Sevgi
|
|
|
63
62
|
# Looks up a named profile when both definition keywords are omitted. Supplying `preambles:` or `attributes:`
|
|
64
63
|
# defines a named profile, or returns an existing profile when every explicitly supplied field matches. Omitted
|
|
65
64
|
# fields are ignored during existing-profile comparison. Profile containers and strings are copied into the
|
|
66
|
-
# process-global, thread-atomic registry
|
|
67
|
-
# omitted
|
|
68
|
-
# stringified once before registration. Concurrent identical definitions return the same
|
|
69
|
-
# class.
|
|
65
|
+
# process-global, thread-atomic registry. Attribute names and nested Hash keys are normalized. Nil attributes are
|
|
66
|
+
# omitted. Update suffixes remain available for document-class inheritance. Mutable non-container values are
|
|
67
|
+
# stringified once before registration. Concurrent identical definitions return the same registered class.
|
|
70
68
|
# @param name [Symbol, String] profile name
|
|
71
69
|
# @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
|
|
72
|
-
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes
|
|
70
|
+
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes. Nil means an empty Hash
|
|
73
71
|
# @return [Class] document class
|
|
74
72
|
# @raise [Sevgi::ArgumentError] when a profile conflicts or metadata is invalid XML, cyclic, or cannot be stringified
|
|
75
73
|
# @overload document(preambles: Undefined, attributes: Undefined)
|
|
76
74
|
# Defines an anonymous document profile without registering it globally.
|
|
77
75
|
# @param preambles [Array<String>, nil, Sevgi::Undefined] document preamble lines
|
|
78
|
-
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes
|
|
76
|
+
# @param attributes [Hash, nil, Sevgi::Undefined] default root attributes. Nil means an empty Hash
|
|
79
77
|
# @return [Class] anonymous document class
|
|
80
78
|
# @raise [Sevgi::ArgumentError] when metadata is invalid XML, cyclic, or cannot be stringified
|
|
81
79
|
# @return [Class] document class
|
|
@@ -96,19 +94,20 @@ module Sevgi
|
|
|
96
94
|
|
|
97
95
|
# Defines or replaces a document profile class.
|
|
98
96
|
# Validation and snapshot capture complete before an existing registration is atomically replaced.
|
|
99
|
-
# Use the non-bang {#document} for ordinary idempotent definitions
|
|
97
|
+
# Use the non-bang {#document} for ordinary idempotent definitions. This
|
|
100
98
|
# bang form is an explicit process-global replacement.
|
|
101
99
|
# @param name [Symbol, String] profile name
|
|
102
100
|
# @param preambles [Array<String>, nil] document preamble lines
|
|
103
|
-
# @param attributes [Hash, nil] default root attributes
|
|
101
|
+
# @param attributes [Hash, nil] default root attributes. Nil means an empty Hash
|
|
104
102
|
# @return [Class] document class
|
|
105
103
|
# @raise [Sevgi::ArgumentError] when the name or metadata is invalid XML, cyclic, or cannot be stringified
|
|
106
104
|
def document!(name, preambles: [], attributes: {})
|
|
107
105
|
Graphics::Document.define(name, preambles:, attributes:, overwrite: true)
|
|
108
106
|
end
|
|
109
107
|
|
|
110
|
-
# Defines a paper profile unless the same profile already exists.
|
|
111
|
-
#
|
|
108
|
+
# Defines a paper profile unless the same profile already exists.
|
|
109
|
+
# Registration is process-global and thread-atomic. Identical concurrent definitions are idempotent. Conflicting
|
|
110
|
+
# definitions are rejected.
|
|
112
111
|
# @param width [Numeric] paper width
|
|
113
112
|
# @param height [Numeric] paper height
|
|
114
113
|
# @param name [Symbol, String] profile name
|
|
@@ -135,15 +134,20 @@ module Sevgi
|
|
|
135
134
|
|
|
136
135
|
# Builds an SVG root element tree without rendering it.
|
|
137
136
|
#
|
|
138
|
-
# The first argument selects root metadata
|
|
139
|
-
# canvas attributes. Keyword attributes are applied
|
|
140
|
-
# @param document [Symbol, String, Class] document profile
|
|
137
|
+
# The first argument selects root metadata, or supplies the canvas while using the default profile. The second
|
|
138
|
+
# supplies canvas attributes when the first argument is a profile. Keyword attributes are applied after both.
|
|
139
|
+
# @param document [Symbol, String, Class, Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper] document profile,
|
|
140
|
+
# document class, or canvas input that uses the default profile
|
|
141
141
|
# @param canvas [Sevgi::Graphics::Canvas, Sevgi::Graphics::Paper, Symbol, String, Sevgi::Undefined, nil] canvas input
|
|
142
142
|
# @yield evaluates the drawing DSL in the root element
|
|
143
143
|
# @yieldreturn [Object] ignored block result
|
|
144
144
|
# @return [Sevgi::Graphics::Document::Proto] SVG root element
|
|
145
145
|
# @raise [Sevgi::ArgumentError] when the document/canvas profile or root XML attributes are invalid
|
|
146
146
|
def SVG(document = :default, canvas = Undefined, **, &block)
|
|
147
|
+
if canvas.equal?(Undefined) && (document.is_a?(Canvas) || document.is_a?(Paper))
|
|
148
|
+
document, canvas = :default, document
|
|
149
|
+
end
|
|
150
|
+
|
|
147
151
|
Graphics::Document.(document, canvas, **, &block)
|
|
148
152
|
end
|
|
149
153
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sevgi-graphics
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Recai Oktaş
|
|
@@ -9,20 +9,34 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: sevgi-function
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
15
29
|
requirements:
|
|
16
30
|
- - '='
|
|
17
31
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
32
|
+
version: 1.1.0
|
|
19
33
|
type: :runtime
|
|
20
34
|
prerelease: false
|
|
21
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
36
|
requirements:
|
|
23
37
|
- - '='
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
39
|
+
version: 1.1.0
|
|
26
40
|
description: Builds and renders SVG document trees from Ruby.
|
|
27
41
|
email: roktas@gmail.com
|
|
28
42
|
executables: []
|
|
@@ -39,9 +53,8 @@ files:
|
|
|
39
53
|
- lib/sevgi/graphics/auxiliary/content.rb
|
|
40
54
|
- lib/sevgi/graphics/auxiliary/margin.rb
|
|
41
55
|
- lib/sevgi/graphics/auxiliary/paper.rb
|
|
42
|
-
- lib/sevgi/graphics/auxiliary/path.rb
|
|
43
56
|
- lib/sevgi/graphics/auxiliary/scalar.rb
|
|
44
|
-
- lib/sevgi/graphics/auxiliary/
|
|
57
|
+
- lib/sevgi/graphics/auxiliary/xml.rb
|
|
45
58
|
- lib/sevgi/graphics/document.rb
|
|
46
59
|
- lib/sevgi/graphics/document/base.rb
|
|
47
60
|
- lib/sevgi/graphics/document/default.rb
|
|
@@ -63,6 +76,8 @@ files:
|
|
|
63
76
|
- lib/sevgi/graphics/mixtures/rdf.rb
|
|
64
77
|
- lib/sevgi/graphics/mixtures/render.rb
|
|
65
78
|
- lib/sevgi/graphics/mixtures/save.rb
|
|
79
|
+
- lib/sevgi/graphics/mixtures/style.rb
|
|
80
|
+
- lib/sevgi/graphics/mixtures/style/declaration.rb
|
|
66
81
|
- lib/sevgi/graphics/mixtures/symbols.rb
|
|
67
82
|
- lib/sevgi/graphics/mixtures/tile.rb
|
|
68
83
|
- lib/sevgi/graphics/mixtures/transform.rb
|
|
@@ -70,7 +85,6 @@ files:
|
|
|
70
85
|
- lib/sevgi/graphics/mixtures/validate.rb
|
|
71
86
|
- lib/sevgi/graphics/mixtures/wrappers.rb
|
|
72
87
|
- lib/sevgi/graphics/version.rb
|
|
73
|
-
- lib/sevgi/graphics/xml.rb
|
|
74
88
|
homepage: https://sevgi.roktas.dev
|
|
75
89
|
licenses:
|
|
76
90
|
- GPL-3.0-or-later
|
|
@@ -93,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
93
107
|
- !ruby/object:Gem::Version
|
|
94
108
|
version: '0'
|
|
95
109
|
requirements: []
|
|
96
|
-
rubygems_version: 4.0.
|
|
110
|
+
rubygems_version: 4.0.20
|
|
97
111
|
specification_version: 4
|
|
98
112
|
summary: Core SVG DSL for Sevgi.
|
|
99
113
|
test_files: []
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Sevgi
|
|
4
|
-
module Graphics
|
|
5
|
-
# Output path normalization shared by Graphics writers and exporters.
|
|
6
|
-
# @api private
|
|
7
|
-
module Path
|
|
8
|
-
# Converts a non-blank path to an expanded String.
|
|
9
|
-
# @param value [String, #to_path] raw path value
|
|
10
|
-
# @param context [String] public operation named in errors
|
|
11
|
-
# @return [String] expanded path
|
|
12
|
-
# @raise [Sevgi::ArgumentError] when value is blank, has an invalid type, or path conversion fails
|
|
13
|
-
def self.call(value, context:)
|
|
14
|
-
path = value.respond_to?(:to_path) ? value.to_path : value
|
|
15
|
-
ArgumentError.("#{context} must be a String or path-like object") unless path.is_a?(::String)
|
|
16
|
-
ArgumentError.("#{context} must be provided") if path.strip.empty?
|
|
17
|
-
|
|
18
|
-
::File.expand_path(path)
|
|
19
|
-
rescue ::Sevgi::ArgumentError
|
|
20
|
-
raise
|
|
21
|
-
rescue ::StandardError => e
|
|
22
|
-
ArgumentError.("#{context} must be a String or path-like object: #{e.message}")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Resolves an optional path with the writer/exporter directory convention.
|
|
26
|
-
# @param value [String, #to_path, nil] explicit path or directory
|
|
27
|
-
# @param default [String, #to_path] default output path
|
|
28
|
-
# @param context [String] public operation named in errors
|
|
29
|
-
# @return [String] expanded output file path
|
|
30
|
-
# @raise [Sevgi::ArgumentError] when a selected path/default is blank, invalid, or cannot be converted
|
|
31
|
-
def self.resolve(value, default:, context:)
|
|
32
|
-
return call(default, context: "#{context} default") if value.nil?
|
|
33
|
-
|
|
34
|
-
path = call(value, context: "#{context} path")
|
|
35
|
-
return path unless ::File.directory?(path)
|
|
36
|
-
|
|
37
|
-
default = call(default, context: "#{context} default")
|
|
38
|
-
::File.join(path, ::File.basename(default))
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private_constant :Path
|
|
43
|
-
end
|
|
44
|
-
end
|