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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +55 -1
  4. data/lib/sevgi/graphics/attribute.rb +15 -10
  5. data/lib/sevgi/graphics/auxiliary/canvas.rb +4 -4
  6. data/lib/sevgi/graphics/auxiliary/content.rb +5 -16
  7. data/lib/sevgi/graphics/auxiliary/paper.rb +52 -1
  8. data/lib/sevgi/graphics/{xml.rb → auxiliary/xml.rb} +4 -9
  9. data/lib/sevgi/graphics/auxiliary.rb +1 -2
  10. data/lib/sevgi/graphics/document/base.rb +3 -2
  11. data/lib/sevgi/graphics/document.rb +15 -27
  12. data/lib/sevgi/graphics/element.rb +23 -8
  13. data/lib/sevgi/graphics/mixtures/call.rb +9 -24
  14. data/lib/sevgi/graphics/mixtures/core.rb +19 -23
  15. data/lib/sevgi/graphics/mixtures/duplicate.rb +4 -16
  16. data/lib/sevgi/graphics/mixtures/export.rb +8 -2
  17. data/lib/sevgi/graphics/mixtures/hatch.rb +5 -5
  18. data/lib/sevgi/graphics/mixtures/identify.rb +1 -1
  19. data/lib/sevgi/graphics/mixtures/inkscape.rb +23 -10
  20. data/lib/sevgi/graphics/mixtures/rdf.rb +1 -1
  21. data/lib/sevgi/graphics/mixtures/render.rb +11 -95
  22. data/lib/sevgi/graphics/mixtures/save.rb +4 -4
  23. data/lib/sevgi/graphics/mixtures/style/declaration.rb +117 -0
  24. data/lib/sevgi/graphics/mixtures/style.rb +36 -0
  25. data/lib/sevgi/graphics/mixtures/symbols.rb +2 -2
  26. data/lib/sevgi/graphics/mixtures/tile.rb +4 -4
  27. data/lib/sevgi/graphics/mixtures/transform.rb +22 -3
  28. data/lib/sevgi/graphics/mixtures/underscore.rb +2 -2
  29. data/lib/sevgi/graphics/mixtures/wrappers.rb +49 -0
  30. data/lib/sevgi/graphics/mixtures.rb +1 -0
  31. data/lib/sevgi/graphics/version.rb +1 -1
  32. data/lib/sevgi/graphics.rb +23 -19
  33. metadata +21 -7
  34. data/lib/sevgi/graphics/auxiliary/path.rb +0 -44
  35. 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; unchanged saves leave both files untouched.
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(:minimal).Save("build/drawing.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 = Path.resolve(path, default:, context: "Save")
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 = Path.(path, context: "Write 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; String and Symbol names are normalized and must not collide
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/sundries/#choose-a-layout-model Choosing a layout model
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; the callback may mutate the element and its return value is ignored
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; the callback may mutate the element and its return value is ignored
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; the callback may mutate the element and its return value is ignored
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
- Translate((ow - iw) / 2.0, (oh - ih) / 2.0)
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 would form malformed markup
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; sibling subtrees are ignored. When the same key is
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"
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Graphics
5
5
  # Current graphics component version.
6
- VERSION = "0.98.2"
6
+ VERSION = "1.1.0"
7
7
  end
8
8
  end
@@ -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; a {Canvas} controls
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/svg/ SVG construction and profile guide
29
- # @see https://sevgi.roktas.dev/library-mode/ Library composition guide
30
- # @see https://sevgi.roktas.dev/showcase/ Runnable drawing examples
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; attribute names and nested Hash keys are normalized, nil attributes are
67
- # omitted, update suffixes are retained for document-class inheritance, and mutable non-container values are
68
- # stringified once before registration. Concurrent identical definitions return the same canonical registered
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; nil means an empty Hash
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; nil means an empty Hash
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; this
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; nil means an empty Hash
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. Registration is process-global and thread-atomic;
111
- # an identical concurrent definition is idempotent and a conflicting definition is rejected.
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; the second supplies physical
139
- # canvas attributes. Keyword attributes are applied to the root after both.
140
- # @param document [Symbol, String, Class] document profile name or document class
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: 0.98.2
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: 0.98.2
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: 0.98.2
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/sizes.rb
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.16
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