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 +4 -4
- data/lib/fiber_pattern/piece.rb +79 -0
- data/lib/fiber_pattern/schematic.rb +63 -0
- data/lib/fiber_pattern/svg_renderer.rb +211 -0
- data/lib/fiber_pattern/version.rb +1 -1
- data/lib/fiber_pattern.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb09b823e0e912ede8fcc93a8981b2321ef65494e5ffe74a49153e5b3c947337
|
|
4
|
+
data.tar.gz: 9f0057c277cc499d97c3b011c1b42c8de0dc5e76bfa04c64759911556e2b7057
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)}""
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def escape_xml(str)
|
|
195
|
+
str.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
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
|
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.
|
|
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
|