fiber_pattern 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86098b154ca5d374ea82112b74c4bcfde885d6da22aa071c4c0f8b87fc2c89bd
4
- data.tar.gz: 6744cff995e936ff00774ffa69e1829c1992cfe5901d491ba233fcd5e734d5fe
3
+ metadata.gz: eb09b823e0e912ede8fcc93a8981b2321ef65494e5ffe74a49153e5b3c947337
4
+ data.tar.gz: 9f0057c277cc499d97c3b011c1b42c8de0dc5e76bfa04c64759911556e2b7057
5
5
  SHA512:
6
- metadata.gz: c9daa9034c4a2a6b5d46c3e09a999a78c11245424163ac792194d8e9d575c95f7146d4e447de386c642ea51cc17b9a8b7477533a04ba7080fc0c09b4b5df3c49
7
- data.tar.gz: a7595ab55c4c0fa5861a39af96d373958cc6f39359f1bb2feddfa3b94187e4de7cf81c403cb3b17ac92c0fcdf395a04526507c22a25c91d04c832b182f828c3d
6
+ metadata.gz: 5b5b8772fcd851ceade728e70d3f404941fa857bf69b1c0a4cd0632d38864d1da9eef8e3186c2c24166ff40efe37ab3495e3e63b49c80f85e7eca715566465bd
7
+ data.tar.gz: e24b3c78870091cbc1e533e873725d8f5a671aecdf10cea1e26da83205b58ad2da27d3170f9231bfe6b0e8f8ea77a703904279577d884bf79d026879233b54b7
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberPattern
4
+ # A value object representing a single garment piece with stitch/row counts
5
+ # and a gauge for deriving physical measurements.
6
+ #
7
+ # @example
8
+ # piece = FiberPattern::Piece.new(
9
+ # name: :front,
10
+ # cast_on: 90.stitches,
11
+ # bind_off: 70.stitches,
12
+ # rows: 120.rows,
13
+ # gauge: gauge
14
+ # )
15
+ # piece.bottom_width # => FiberUnits::Length in inches
16
+ # piece.shape # => :trapezoid
17
+ class Piece
18
+ # @return [Symbol] piece name (e.g. :front, :back, :sleeve)
19
+ attr_reader :name
20
+
21
+ # @return [FiberUnits::StitchCount] cast-on stitch count (bottom edge)
22
+ attr_reader :cast_on
23
+
24
+ # @return [FiberUnits::StitchCount] bind-off stitch count (top edge)
25
+ attr_reader :bind_off
26
+
27
+ # @return [FiberUnits::RowCount] total rows
28
+ attr_reader :rows
29
+
30
+ # @return [FiberGauge::Gauge] gauge for measurement conversion
31
+ attr_reader :gauge
32
+
33
+ # @return [FiberPattern::Shaping, nil] optional shaping schedule
34
+ attr_reader :shaping
35
+
36
+ # @param name [Symbol] piece identifier
37
+ # @param cast_on [FiberUnits::StitchCount] cast-on stitch count
38
+ # @param bind_off [FiberUnits::StitchCount] bind-off stitch count
39
+ # @param rows [FiberUnits::RowCount] total row count
40
+ # @param gauge [FiberGauge::Gauge] gauge for converting stitches/rows to measurements
41
+ # @param shaping [FiberPattern::Shaping, nil] optional shaping data
42
+ def initialize(name:, cast_on:, bind_off:, rows:, gauge:, shaping: nil)
43
+ @name = name
44
+ @cast_on = cast_on
45
+ @bind_off = bind_off
46
+ @rows = rows
47
+ @gauge = gauge
48
+ @shaping = shaping
49
+ end
50
+
51
+ # Physical width at the bottom (cast-on) edge.
52
+ #
53
+ # @return [FiberUnits::Length]
54
+ def bottom_width
55
+ gauge.width_for_stitches(cast_on)
56
+ end
57
+
58
+ # Physical width at the top (bind-off) edge.
59
+ #
60
+ # @return [FiberUnits::Length]
61
+ def top_width
62
+ gauge.width_for_stitches(bind_off)
63
+ end
64
+
65
+ # Physical height of the piece.
66
+ #
67
+ # @return [FiberUnits::Length]
68
+ def height
69
+ (rows.value.to_f / gauge.rpi).inches
70
+ end
71
+
72
+ # Whether this piece is a rectangle or trapezoid based on edge widths.
73
+ #
74
+ # @return [Symbol] :rectangle or :trapezoid
75
+ def shape
76
+ (cast_on.value == bind_off.value) ? :rectangle : :trapezoid
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberPattern
4
+ # Composes named garment pieces and renders them as a schematic.
5
+ #
6
+ # @example
7
+ # schematic = FiberPattern::Schematic.new
8
+ # schematic.add_piece(:front,
9
+ # cast_on: 90.stitches, bind_off: 70.stitches,
10
+ # rows: 120.rows, gauge: gauge
11
+ # )
12
+ # schematic.render(:svg) # => SVG string
13
+ class Schematic
14
+ # @return [Hash{Symbol => FiberPattern::Piece}] named pieces
15
+ attr_reader :pieces
16
+
17
+ def initialize
18
+ @pieces = {}
19
+ end
20
+
21
+ # Adds a piece to the schematic.
22
+ #
23
+ # @param name [Symbol] piece identifier (e.g. :front, :back, :sleeve)
24
+ # @param cast_on [FiberUnits::StitchCount] cast-on stitch count
25
+ # @param bind_off [FiberUnits::StitchCount] bind-off stitch count
26
+ # @param rows [FiberUnits::RowCount] total row count
27
+ # @param gauge [FiberGauge::Gauge] gauge for measurement conversion
28
+ # @param shaping [FiberPattern::Shaping, nil] optional shaping data
29
+ # @return [FiberPattern::Piece]
30
+ def add_piece(name, cast_on:, bind_off:, rows:, gauge:, shaping: nil)
31
+ @pieces[name] = Piece.new(
32
+ name: name,
33
+ cast_on: cast_on,
34
+ bind_off: bind_off,
35
+ rows: rows,
36
+ gauge: gauge,
37
+ shaping: shaping
38
+ )
39
+ end
40
+
41
+ # Fetches a piece by name.
42
+ #
43
+ # @param name [Symbol] piece identifier
44
+ # @return [FiberPattern::Piece]
45
+ # @raise [KeyError] if the piece is not found
46
+ def piece(name)
47
+ @pieces.fetch(name)
48
+ end
49
+
50
+ # Renders the schematic in the given format.
51
+ #
52
+ # @param format [Symbol] output format (:svg)
53
+ # @return [String] rendered output
54
+ def render(format = :svg)
55
+ case format
56
+ when :svg
57
+ SvgRenderer.new(self).render
58
+ else
59
+ raise ArgumentError, "unsupported format: #{format.inspect}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberPattern
4
+ # Renders a Schematic as an SVG string with piece outlines, dimension lines,
5
+ # measurement labels, and shaping markers.
6
+ class SvgRenderer
7
+ PADDING = 40
8
+ PIECE_SPACING = 60
9
+ DEFAULT_SCALE = 10
10
+ LABEL_OFFSET = 25
11
+ FONT_SIZE = 12
12
+ MARKER_RADIUS = 3
13
+
14
+ # @param schematic [FiberPattern::Schematic] schematic to render
15
+ # @param scale [Numeric] pixels per inch (default 10)
16
+ def initialize(schematic, scale: DEFAULT_SCALE)
17
+ @schematic = schematic
18
+ @scale = scale
19
+ end
20
+
21
+ # @return [String] complete SVG document
22
+ def render
23
+ pieces = @schematic.pieces.values
24
+ return empty_svg if pieces.empty?
25
+
26
+ layouts = compute_layouts(pieces)
27
+ total_width = layouts.last[:x] + layouts.last[:w] + PADDING + LABEL_OFFSET + FONT_SIZE * 3
28
+ total_height = layouts.map { |l| l[:h] }.max + (PADDING + LABEL_OFFSET + FONT_SIZE) * 2
29
+
30
+ parts = [svg_header(total_width, total_height)]
31
+ layouts.each do |layout|
32
+ parts << render_piece(layout[:piece], layout[:x], PADDING + LABEL_OFFSET + FONT_SIZE)
33
+ end
34
+ parts << svg_footer
35
+
36
+ parts.join("\n")
37
+ end
38
+
39
+ private
40
+
41
+ def compute_layouts(pieces)
42
+ layouts = []
43
+ x = PADDING
44
+
45
+ pieces.each do |piece|
46
+ w = (piece.bottom_width.to(:inches).value * @scale).round
47
+ top_w = (piece.top_width.to(:inches).value * @scale).round
48
+ w = [w, top_w].max
49
+ h = (piece.height.to(:inches).value * @scale).round
50
+
51
+ layouts << {piece: piece, x: x, w: w, h: h}
52
+ x += w + PIECE_SPACING
53
+ end
54
+
55
+ layouts
56
+ end
57
+
58
+ def render_piece(piece, x_offset, y_offset)
59
+ bottom_w = (piece.bottom_width.to(:inches).value * @scale).round
60
+ top_w = (piece.top_width.to(:inches).value * @scale).round
61
+ h = (piece.height.to(:inches).value * @scale).round
62
+ max_w = [bottom_w, top_w].max
63
+
64
+ parts = []
65
+ parts << %(<g data-piece="#{escape_xml(piece.name.to_s)}">)
66
+ parts << render_outline(piece, x_offset, y_offset, max_w, bottom_w, top_w, h)
67
+ parts << render_label(piece, x_offset, y_offset, max_w, h)
68
+ parts << render_dimension_lines(piece, x_offset, y_offset, max_w, bottom_w, top_w, h)
69
+ parts << render_shaping_markers(piece, x_offset, y_offset, max_w, bottom_w, top_w, h) if piece.shaping
70
+ parts << "</g>"
71
+
72
+ parts.join("\n")
73
+ end
74
+
75
+ def render_outline(piece, x, y, max_w, bottom_w, top_w, h)
76
+ if piece.shape == :rectangle
77
+ %(<rect x="#{x}" y="#{y}" width="#{bottom_w}" height="#{h}" ) +
78
+ %(fill="none" stroke="#333" stroke-width="2"/>)
79
+ else
80
+ bottom_offset = (max_w - bottom_w) / 2
81
+ top_offset = (max_w - top_w) / 2
82
+
83
+ x1 = x + bottom_offset
84
+ x2 = x + bottom_offset + bottom_w
85
+ x3 = x + top_offset + top_w
86
+ x4 = x + top_offset
87
+
88
+ %(<polygon points="#{x1},#{y + h} #{x2},#{y + h} #{x3},#{y} #{x4},#{y}" ) +
89
+ %(fill="none" stroke="#333" stroke-width="2"/>)
90
+ end
91
+ end
92
+
93
+ def render_label(piece, x, y, max_w, h)
94
+ cx = x + max_w / 2
95
+ cy = y + h / 2
96
+
97
+ %(<text x="#{cx}" y="#{cy}" text-anchor="middle" dominant-baseline="middle" ) +
98
+ %(font-family="sans-serif" font-size="#{FONT_SIZE + 2}" fill="#666">) +
99
+ %(#{escape_xml(piece.name.to_s)}</text>)
100
+ end
101
+
102
+ def render_dimension_lines(piece, x, y, max_w, bottom_w, top_w, h)
103
+ parts = []
104
+
105
+ # Bottom width dimension (below the piece)
106
+ bottom_offset = (max_w - bottom_w) / 2
107
+ dim_y = y + h + LABEL_OFFSET
108
+ bx1 = x + bottom_offset
109
+ bx2 = x + bottom_offset + bottom_w
110
+
111
+ parts << dimension_line(bx1, dim_y, bx2, dim_y,
112
+ format_measurement(piece.bottom_width))
113
+
114
+ # Top width dimension (above the piece)
115
+ if piece.shape == :trapezoid
116
+ top_offset = (max_w - top_w) / 2
117
+ dim_y_top = y - LABEL_OFFSET
118
+ tx1 = x + top_offset
119
+ tx2 = x + top_offset + top_w
120
+
121
+ parts << dimension_line(tx1, dim_y_top, tx2, dim_y_top,
122
+ format_measurement(piece.top_width))
123
+ end
124
+
125
+ # Height dimension (right side)
126
+ dim_x = x + max_w + LABEL_OFFSET
127
+ parts << dimension_line(dim_x, y, dim_x, y + h,
128
+ format_measurement(piece.height))
129
+
130
+ parts.join("\n")
131
+ end
132
+
133
+ def render_shaping_markers(piece, x, y, max_w, bottom_w, top_w, h)
134
+ return "" unless piece.shaping
135
+
136
+ total_rows = piece.rows.value
137
+ parts = []
138
+
139
+ piece.shaping.schedule.each do |event|
140
+ row = event[:row]
141
+ ratio = row.to_f / total_rows
142
+ marker_y = y + h - (ratio * h).round
143
+
144
+ # Width at this row (linear interpolation)
145
+ width_at_row = bottom_w + (top_w - bottom_w) * ratio
146
+ bottom_offset = (max_w - bottom_w) / 2
147
+ top_offset = (max_w - top_w) / 2
148
+ left_x = x + bottom_offset + (top_offset - bottom_offset) * ratio
149
+ right_x = left_x + width_at_row
150
+
151
+ parts << %(<circle cx="#{left_x.round}" cy="#{marker_y}" r="#{MARKER_RADIUS}" fill="#e74c3c"/>)
152
+ parts << %(<circle cx="#{right_x.round}" cy="#{marker_y}" r="#{MARKER_RADIUS}" fill="#e74c3c"/>)
153
+ end
154
+
155
+ parts.join("\n")
156
+ end
157
+
158
+ def dimension_line(x1, y1, x2, y2, label)
159
+ # Determine if horizontal or vertical
160
+ horizontal = y1 == y2
161
+ mid_x = (x1 + x2) / 2
162
+ mid_y = (y1 + y2) / 2
163
+
164
+ tick_size = 5
165
+ parts = []
166
+
167
+ # Main line
168
+ parts << %(<line x1="#{x1}" y1="#{y1}" x2="#{x2}" y2="#{y2}" stroke="#999" stroke-width="1"/>)
169
+
170
+ if horizontal
171
+ # End ticks (vertical)
172
+ parts << %(<line x1="#{x1}" y1="#{y1 - tick_size}" x2="#{x1}" y2="#{y1 + tick_size}" stroke="#999" stroke-width="1"/>)
173
+ parts << %(<line x1="#{x2}" y1="#{y2 - tick_size}" x2="#{x2}" y2="#{y2 + tick_size}" stroke="#999" stroke-width="1"/>)
174
+ # Label
175
+ parts << %(<text x="#{mid_x}" y="#{mid_y - 6}" text-anchor="middle" ) +
176
+ %(font-family="sans-serif" font-size="#{FONT_SIZE}" fill="#333">#{label}</text>)
177
+ else
178
+ # End ticks (horizontal)
179
+ parts << %(<line x1="#{x1 - tick_size}" y1="#{y1}" x2="#{x1 + tick_size}" y2="#{y1}" stroke="#999" stroke-width="1"/>)
180
+ parts << %(<line x1="#{x2 - tick_size}" y1="#{y2}" x2="#{x2 + tick_size}" y2="#{y2}" stroke="#999" stroke-width="1"/>)
181
+ # Label (rotated)
182
+ parts << %(<text x="#{mid_x + FONT_SIZE}" y="#{mid_y}" text-anchor="middle" ) +
183
+ %(font-family="sans-serif" font-size="#{FONT_SIZE}" fill="#333">#{label}</text>)
184
+ end
185
+
186
+ parts.join("\n")
187
+ end
188
+
189
+ def format_measurement(length)
190
+ value = length.to(:inches).value
191
+ "#{value.round(1)}&quot;"
192
+ end
193
+
194
+ def escape_xml(str)
195
+ str.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
196
+ end
197
+
198
+ def svg_header(width, height)
199
+ %(<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}" ) +
200
+ %(viewBox="0 0 #{width} #{height}">)
201
+ end
202
+
203
+ def svg_footer
204
+ "</svg>"
205
+ end
206
+
207
+ def empty_svg
208
+ svg_header(0, 0) + "\n" + svg_footer
209
+ end
210
+ end
211
+ end
@@ -3,6 +3,6 @@
3
3
  # :nocov:
4
4
  module FiberPattern
5
5
  # Current gem version.
6
- VERSION = "0.5.0"
6
+ VERSION = "0.6.0"
7
7
  end
8
8
  # :nocov:
data/lib/fiber_pattern.rb CHANGED
@@ -7,7 +7,10 @@ require_relative "fiber_pattern/sizing"
7
7
  require_relative "fiber_pattern/repeat"
8
8
  require_relative "fiber_pattern/stitch_pattern"
9
9
  require_relative "fiber_pattern/scaling"
10
+ require_relative "fiber_pattern/piece"
11
+ require_relative "fiber_pattern/schematic"
10
12
  require_relative "fiber_pattern/shaping"
13
+ require_relative "fiber_pattern/svg_renderer"
11
14
  require_relative "fiber_pattern/grade_rules"
12
15
  require_relative "fiber_pattern/grader"
13
16
  require_relative "fiber_pattern/body_measurements"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fiber_pattern
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Meagan Waller
@@ -81,12 +81,15 @@ files:
81
81
  - lib/fiber_pattern/garment_sizing.rb
82
82
  - lib/fiber_pattern/grade_rules.rb
83
83
  - lib/fiber_pattern/grader.rb
84
+ - lib/fiber_pattern/piece.rb
84
85
  - lib/fiber_pattern/repeat.rb
85
86
  - lib/fiber_pattern/scaling.rb
87
+ - lib/fiber_pattern/schematic.rb
86
88
  - lib/fiber_pattern/shaping.rb
87
89
  - lib/fiber_pattern/size_chart.rb
88
90
  - lib/fiber_pattern/sizing.rb
89
91
  - lib/fiber_pattern/stitch_pattern.rb
92
+ - lib/fiber_pattern/svg_renderer.rb
90
93
  - lib/fiber_pattern/version.rb
91
94
  - sig/fiber_pattern.rbs
92
95
  homepage: https://github.com/meaganewaller/craftos/tree/main/gems/fiber_pattern