fiber_pattern 0.2.0 → 0.4.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/body_measurements.rb +61 -0
- data/lib/fiber_pattern/ease.rb +74 -0
- data/lib/fiber_pattern/garment_sizing.rb +76 -0
- data/lib/fiber_pattern/grade_rules.rb +54 -0
- data/lib/fiber_pattern/grader.rb +74 -0
- data/lib/fiber_pattern/shaping.rb +105 -0
- data/lib/fiber_pattern/size_chart.rb +86 -0
- data/lib/fiber_pattern/version.rb +1 -1
- data/lib/fiber_pattern.rb +7 -0
- metadata +36 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0ed0351785dcc4a977f481938aecf3b3d787b91752558bc307cf2b72ad962e8
|
|
4
|
+
data.tar.gz: c16dbb1de69fa26de99bc7dc12504db55c1707bf5a9e7388973fdf1954998e18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9fc5ede7f7838208da2e8382414bebf621af83d557df70d5222531b8087b389dda3d213608149d4f478c146ef803b676c93925d4c0749e2cd889bd71132ba3e
|
|
7
|
+
data.tar.gz: '0090202feec59f5190da9296132831e0ffaa12389cf0fed5a66c4222662c5700aec07a42ef2b0b1fa0c748261a801eccf6bc63baf8130be985f7b98a4d882699'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Value object representing key body dimensions.
|
|
5
|
+
#
|
|
6
|
+
# Accepts any set of named measurements as keyword arguments.
|
|
7
|
+
# Each value should be a FiberUnits::Length.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# body = FiberPattern::BodyMeasurements.new(
|
|
11
|
+
# bust: 36.inches,
|
|
12
|
+
# waist: 30.inches,
|
|
13
|
+
# hip: 38.inches,
|
|
14
|
+
# arm_length: 24.inches
|
|
15
|
+
# )
|
|
16
|
+
# body.bust # => 36.inches
|
|
17
|
+
# body[:waist] # => 30.inches
|
|
18
|
+
# body.measurements # => [:bust, :waist, :hip, :arm_length]
|
|
19
|
+
class BodyMeasurements
|
|
20
|
+
# @return [Hash<Symbol, FiberUnits::Length>] all measurements
|
|
21
|
+
attr_reader :data
|
|
22
|
+
|
|
23
|
+
# @param measurements [Hash<Symbol, FiberUnits::Length>] named body measurements
|
|
24
|
+
def initialize(**measurements)
|
|
25
|
+
raise ArgumentError, "at least one measurement is required" if measurements.empty?
|
|
26
|
+
|
|
27
|
+
@data = measurements.freeze
|
|
28
|
+
define_accessors!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the measurement names.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<Symbol>]
|
|
34
|
+
def measurements
|
|
35
|
+
data.keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Hash-style access to a measurement.
|
|
39
|
+
#
|
|
40
|
+
# @param name [Symbol]
|
|
41
|
+
# @return [FiberUnits::Length, nil]
|
|
42
|
+
def [](name)
|
|
43
|
+
data[name]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns measurements as a plain hash.
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash<Symbol, FiberUnits::Length>]
|
|
49
|
+
def to_h
|
|
50
|
+
data.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def define_accessors!
|
|
56
|
+
data.each_key do |name|
|
|
57
|
+
define_singleton_method(name) { data[name] }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Value object representing ease preferences per measurement.
|
|
5
|
+
#
|
|
6
|
+
# Ease is the difference between body measurements and finished garment
|
|
7
|
+
# measurements. Positive ease creates a looser fit; negative ease creates
|
|
8
|
+
# a tighter fit.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# ease = FiberPattern::Ease.new(
|
|
12
|
+
# bust: 4.inches,
|
|
13
|
+
# waist: 2.inches,
|
|
14
|
+
# hip: 2.inches
|
|
15
|
+
# )
|
|
16
|
+
# ease.bust # => 4.inches
|
|
17
|
+
# ease[:waist] # => 2.inches
|
|
18
|
+
# ease.for(:bust) # => 4.inches
|
|
19
|
+
class Ease
|
|
20
|
+
# @return [Hash<Symbol, FiberUnits::Length>] all ease values
|
|
21
|
+
attr_reader :data
|
|
22
|
+
|
|
23
|
+
# @param ease_values [Hash<Symbol, FiberUnits::Length>] named ease values
|
|
24
|
+
def initialize(**ease_values)
|
|
25
|
+
raise ArgumentError, "at least one ease value is required" if ease_values.empty?
|
|
26
|
+
|
|
27
|
+
@data = ease_values.freeze
|
|
28
|
+
define_accessors!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the ease value for a given measurement.
|
|
32
|
+
#
|
|
33
|
+
# @param name [Symbol]
|
|
34
|
+
# @return [FiberUnits::Length]
|
|
35
|
+
# @raise [ArgumentError] if no ease is defined for the measurement
|
|
36
|
+
def for(name)
|
|
37
|
+
raise ArgumentError, "no ease defined for #{name.inspect}" unless data.key?(name)
|
|
38
|
+
|
|
39
|
+
data[name]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Hash-style access to an ease value.
|
|
43
|
+
#
|
|
44
|
+
# @param name [Symbol]
|
|
45
|
+
# @return [FiberUnits::Length, nil]
|
|
46
|
+
def [](name)
|
|
47
|
+
data[name]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns all measurement names that have ease defined.
|
|
51
|
+
#
|
|
52
|
+
# @return [Array<Symbol>]
|
|
53
|
+
def measurements
|
|
54
|
+
data.keys
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns ease values as a plain hash.
|
|
58
|
+
#
|
|
59
|
+
# @return [Hash<Symbol, FiberUnits::Length>]
|
|
60
|
+
def to_h
|
|
61
|
+
data.dup
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def define_accessors!
|
|
67
|
+
data.each_key do |name|
|
|
68
|
+
next if name == :for # avoid overriding #for
|
|
69
|
+
|
|
70
|
+
define_singleton_method(name) { data[name] }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Combines body measurements with ease to produce finished garment dimensions.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# body = FiberPattern::BodyMeasurements.new(bust: 36.inches, waist: 30.inches)
|
|
8
|
+
# ease = FiberPattern::Ease.new(bust: 4.inches, waist: 2.inches)
|
|
9
|
+
# garment = FiberPattern::GarmentSizing.new(body: body, ease: ease)
|
|
10
|
+
# garment.bust # => 40.inches
|
|
11
|
+
# garment.waist # => 32.inches
|
|
12
|
+
class GarmentSizing
|
|
13
|
+
# @return [FiberPattern::BodyMeasurements]
|
|
14
|
+
attr_reader :body
|
|
15
|
+
|
|
16
|
+
# @return [FiberPattern::Ease]
|
|
17
|
+
attr_reader :ease
|
|
18
|
+
|
|
19
|
+
# @param body [FiberPattern::BodyMeasurements] body measurements
|
|
20
|
+
# @param ease [FiberPattern::Ease] ease preferences
|
|
21
|
+
def initialize(body:, ease:)
|
|
22
|
+
validate!(body, ease)
|
|
23
|
+
@body = body
|
|
24
|
+
@ease = ease
|
|
25
|
+
define_accessors!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the finished garment dimension for a given measurement.
|
|
29
|
+
#
|
|
30
|
+
# @param name [Symbol]
|
|
31
|
+
# @return [FiberUnits::Length] body measurement + ease
|
|
32
|
+
# @raise [ArgumentError] if the measurement is not defined
|
|
33
|
+
def dimension(name)
|
|
34
|
+
raise ArgumentError, "unknown measurement #{name.inspect}" unless body.data.key?(name)
|
|
35
|
+
|
|
36
|
+
body[name] + (ease[name] || zero_length(body[name]))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Hash-style access to finished dimensions.
|
|
40
|
+
#
|
|
41
|
+
# @param name [Symbol]
|
|
42
|
+
# @return [FiberUnits::Length]
|
|
43
|
+
def [](name)
|
|
44
|
+
dimension(name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns all finished garment dimensions.
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash<Symbol, FiberUnits::Length>]
|
|
50
|
+
def dimensions
|
|
51
|
+
body.measurements.each_with_object({}) do |name, result|
|
|
52
|
+
result[name] = dimension(name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def validate!(body, ease)
|
|
59
|
+
ease.measurements.each do |name|
|
|
60
|
+
unless body.measurements.include?(name)
|
|
61
|
+
raise ArgumentError, "ease defines #{name.inspect} but body measurements does not"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def define_accessors!
|
|
67
|
+
body.measurements.each do |name|
|
|
68
|
+
define_singleton_method(name) { dimension(name) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def zero_length(reference)
|
|
73
|
+
reference * 0
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Defines per-measurement step values used to grade a pattern across sizes.
|
|
5
|
+
#
|
|
6
|
+
# Each rule maps a measurement name to a step value (a FiberUnits::Length)
|
|
7
|
+
# that represents how much that measurement changes between adjacent sizes.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# rules = FiberPattern::GradeRules.new(
|
|
11
|
+
# bust: { step: 2.inches },
|
|
12
|
+
# waist: { step: 2.inches },
|
|
13
|
+
# sleeve_length: { step: 0.5.inches }
|
|
14
|
+
# )
|
|
15
|
+
# rules.step_for(:bust) # => 2.inches
|
|
16
|
+
class GradeRules
|
|
17
|
+
# @return [Hash<Symbol, Hash>] raw rules keyed by measurement name
|
|
18
|
+
attr_reader :rules
|
|
19
|
+
|
|
20
|
+
# @param rules [Hash<Symbol, Hash>] measurement rules, each with a :step key
|
|
21
|
+
def initialize(**rules)
|
|
22
|
+
validate!(rules)
|
|
23
|
+
@rules = rules.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the step value for a given measurement.
|
|
27
|
+
#
|
|
28
|
+
# @param measurement [Symbol] measurement name
|
|
29
|
+
# @return [FiberUnits::Length] step between adjacent sizes
|
|
30
|
+
# @raise [ArgumentError] if the measurement has no rule defined
|
|
31
|
+
def step_for(measurement)
|
|
32
|
+
raise ArgumentError, "no rule defined for #{measurement.inspect}" unless rules.key?(measurement)
|
|
33
|
+
|
|
34
|
+
rules[measurement][:step]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns all measurement names that have rules defined.
|
|
38
|
+
#
|
|
39
|
+
# @return [Array<Symbol>]
|
|
40
|
+
def measurements
|
|
41
|
+
rules.keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def validate!(rules)
|
|
47
|
+
rules.each do |name, config|
|
|
48
|
+
unless config.is_a?(Hash) && config.key?(:step)
|
|
49
|
+
raise ArgumentError, "rule for #{name.inspect} must be a Hash with a :step key"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Applies grade rules to a base size's measurements to produce a full size range.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# grader = FiberPattern::Grader.new(
|
|
8
|
+
# base_size: :m,
|
|
9
|
+
# measurements: { bust: 36.inches, waist: 30.inches },
|
|
10
|
+
# rules: grade_rules
|
|
11
|
+
# )
|
|
12
|
+
# grader.size(:l) # => { bust: 38.inches, waist: 32.inches }
|
|
13
|
+
# grader.all_sizes # => { xs: {...}, s: {...}, ... }
|
|
14
|
+
class Grader
|
|
15
|
+
# Standard size progression from smallest to largest.
|
|
16
|
+
SIZES = %i[xs s m l xl xxl xxxl xxxxl xxxxxl].freeze
|
|
17
|
+
|
|
18
|
+
# @return [Symbol] the base size used as the grading anchor
|
|
19
|
+
# @return [Hash<Symbol, FiberUnits::Length>] base measurements
|
|
20
|
+
# @return [FiberPattern::GradeRules] grading rules
|
|
21
|
+
attr_reader :base_size, :measurements, :rules
|
|
22
|
+
|
|
23
|
+
# @param base_size [Symbol] one of the standard SIZES
|
|
24
|
+
# @param measurements [Hash<Symbol, FiberUnits::Length>] base size measurements
|
|
25
|
+
# @param rules [FiberPattern::GradeRules] grade rules to apply
|
|
26
|
+
# @param sizes [Array<Symbol>] optional custom size list (defaults to SIZES)
|
|
27
|
+
def initialize(base_size:, measurements:, rules:, sizes: SIZES)
|
|
28
|
+
@sizes = sizes
|
|
29
|
+
validate!(base_size, measurements, rules)
|
|
30
|
+
@base_size = base_size
|
|
31
|
+
@measurements = measurements.freeze
|
|
32
|
+
@rules = rules
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns graded measurements for a single size.
|
|
36
|
+
#
|
|
37
|
+
# @param target [Symbol] the size to compute
|
|
38
|
+
# @return [Hash<Symbol, FiberUnits::Length>]
|
|
39
|
+
# @raise [ArgumentError] if the target size is not in the size list
|
|
40
|
+
def size(target)
|
|
41
|
+
raise ArgumentError, "unknown size #{target.inspect}" unless @sizes.include?(target)
|
|
42
|
+
|
|
43
|
+
offset = @sizes.index(target) - @sizes.index(base_size)
|
|
44
|
+
|
|
45
|
+
measurements.each_with_object({}) do |(name, base_value), result|
|
|
46
|
+
step = rules.step_for(name)
|
|
47
|
+
result[name] = base_value + (step * offset)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns graded measurements for all sizes.
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash<Symbol, Hash<Symbol, FiberUnits::Length>>]
|
|
54
|
+
def all_sizes
|
|
55
|
+
@sizes.each_with_object({}) do |size_name, result|
|
|
56
|
+
result[size_name] = size(size_name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def validate!(base_size, measurements, rules)
|
|
63
|
+
unless @sizes.include?(base_size)
|
|
64
|
+
raise ArgumentError, "base_size #{base_size.inspect} is not in the size list"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
measurements.each_key do |name|
|
|
68
|
+
unless rules.measurements.include?(name)
|
|
69
|
+
raise ArgumentError, "no grade rule for measurement #{name.inspect}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Calculates evenly distributed shaping (increases or decreases) across a span of rows.
|
|
5
|
+
#
|
|
6
|
+
# @example Decreasing from 60 to 40 stitches over 30 rows
|
|
7
|
+
# shaping = FiberPattern::Shaping.new(
|
|
8
|
+
# from: 60.stitches,
|
|
9
|
+
# to: 40.stitches,
|
|
10
|
+
# over: 30.rows,
|
|
11
|
+
# method: :decrease
|
|
12
|
+
# )
|
|
13
|
+
# shaping.total_changes # => 10
|
|
14
|
+
# shaping.every_n_rows # => 3
|
|
15
|
+
# shaping.schedule # => [{row: 1, action: :dec}, {row: 4, action: :dec}, ...]
|
|
16
|
+
class Shaping
|
|
17
|
+
# @return [FiberUnits::Stitches] starting stitch count
|
|
18
|
+
# @return [FiberUnits::Stitches] ending stitch count
|
|
19
|
+
# @return [FiberUnits::Rows] total rows available for shaping
|
|
20
|
+
# @return [Symbol] shaping method (:increase or :decrease)
|
|
21
|
+
# @return [Integer] stitches changed per shaping row (default 2 for paired shaping)
|
|
22
|
+
attr_reader :from, :to, :over, :method, :stitches_per_event
|
|
23
|
+
|
|
24
|
+
# @param from [FiberUnits::Stitches] starting stitch count
|
|
25
|
+
# @param to [FiberUnits::Stitches] target stitch count
|
|
26
|
+
# @param over [FiberUnits::Rows] number of rows available for shaping
|
|
27
|
+
# @param method [Symbol] :increase or :decrease
|
|
28
|
+
# @param stitches_per_event [Integer] stitches changed per shaping row (default 2 for paired shaping)
|
|
29
|
+
def initialize(from:, to:, over:, method:, stitches_per_event: 2)
|
|
30
|
+
validate!(from, to, over, method)
|
|
31
|
+
@from = from
|
|
32
|
+
@to = to
|
|
33
|
+
@over = over
|
|
34
|
+
@method = method
|
|
35
|
+
@stitches_per_event = stitches_per_event
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Total number of shaping events needed.
|
|
39
|
+
#
|
|
40
|
+
# @return [Integer]
|
|
41
|
+
def total_changes
|
|
42
|
+
stitch_difference / stitches_per_event
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Base interval between shaping rows.
|
|
46
|
+
#
|
|
47
|
+
# @return [Integer]
|
|
48
|
+
def every_n_rows
|
|
49
|
+
return 0 if total_changes.zero?
|
|
50
|
+
|
|
51
|
+
over.value / total_changes
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Row-by-row schedule of shaping events, distributing any remainder rows
|
|
55
|
+
# evenly across the span.
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Hash>] each entry has :row and :action keys
|
|
58
|
+
def schedule
|
|
59
|
+
return [] if total_changes.zero?
|
|
60
|
+
|
|
61
|
+
changes = total_changes
|
|
62
|
+
rows_available = over.value
|
|
63
|
+
action = (method == :decrease) ? :dec : :inc
|
|
64
|
+
|
|
65
|
+
base_interval = rows_available / changes
|
|
66
|
+
remainder = rows_available % changes
|
|
67
|
+
|
|
68
|
+
schedule = []
|
|
69
|
+
current_row = 0
|
|
70
|
+
|
|
71
|
+
changes.times do |i|
|
|
72
|
+
# Spread remainder evenly: the first `remainder` intervals get +1 row
|
|
73
|
+
interval = base_interval + ((i < remainder) ? 1 : 0)
|
|
74
|
+
current_row += interval
|
|
75
|
+
schedule << {row: current_row, action: action}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
schedule
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def stitch_difference
|
|
84
|
+
(from.value - to.value).abs
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate!(from, to, over, method)
|
|
88
|
+
unless %i[increase decrease].include?(method)
|
|
89
|
+
raise ArgumentError, "method must be :increase or :decrease, got #{method.inspect}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if method == :decrease && from.value < to.value
|
|
93
|
+
raise ArgumentError, "from must be greater than to for :decrease shaping"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if method == :increase && from.value > to.value
|
|
97
|
+
raise ArgumentError, "from must be less than to for :increase shaping"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if over.value <= 0
|
|
101
|
+
raise ArgumentError, "over must be a positive row count"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberPattern
|
|
4
|
+
# Standard size chart based on Craft Yarn Council (CYC) guidelines.
|
|
5
|
+
#
|
|
6
|
+
# Provides lookup of standard body measurements by size, and a closest-size
|
|
7
|
+
# finder that matches body measurements to the nearest standard size.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# chart = FiberPattern::SizeChart.new
|
|
11
|
+
# chart.size(:m) # => { bust: 36.inches, waist: 28.inches, hip: 38.inches }
|
|
12
|
+
# chart.closest_size(bust: 37.inches, waist: 29.inches, hip: 39.inches) # => :m
|
|
13
|
+
class SizeChart
|
|
14
|
+
# CYC standard women's body measurements (inches).
|
|
15
|
+
# Source: Craft Yarn Council Standards & Guidelines
|
|
16
|
+
CYC_WOMEN = {
|
|
17
|
+
xs: {bust: 28, waist: 20, hip: 30},
|
|
18
|
+
s: {bust: 32, waist: 24, hip: 34},
|
|
19
|
+
m: {bust: 36, waist: 28, hip: 38},
|
|
20
|
+
l: {bust: 40, waist: 32, hip: 42},
|
|
21
|
+
xl: {bust: 44, waist: 36, hip: 46},
|
|
22
|
+
xxl: {bust: 48, waist: 40, hip: 50},
|
|
23
|
+
xxxl: {bust: 52, waist: 44, hip: 54},
|
|
24
|
+
xxxxl: {bust: 56, waist: 48, hip: 58},
|
|
25
|
+
xxxxxl: {bust: 60, waist: 52, hip: 62}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# @return [Hash<Symbol, Hash<Symbol, FiberUnits::Length>>] size chart data
|
|
29
|
+
attr_reader :chart
|
|
30
|
+
|
|
31
|
+
# @return [Array<Symbol>] ordered size names
|
|
32
|
+
attr_reader :sizes
|
|
33
|
+
|
|
34
|
+
# Creates a new size chart.
|
|
35
|
+
#
|
|
36
|
+
# @param chart [Hash<Symbol, Hash<Symbol, Numeric>>] raw chart data mapping size names
|
|
37
|
+
# to measurement hashes. Values are converted to inches. Defaults to CYC_WOMEN.
|
|
38
|
+
def initialize(chart: CYC_WOMEN)
|
|
39
|
+
@chart = chart.each_with_object({}) do |(size_name, measurements), result|
|
|
40
|
+
result[size_name] = measurements.transform_values { |v| v.is_a?(Numeric) ? v.inches : v }
|
|
41
|
+
end.freeze
|
|
42
|
+
@sizes = @chart.keys.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns body measurements for a standard size.
|
|
46
|
+
#
|
|
47
|
+
# @param name [Symbol] size name
|
|
48
|
+
# @return [Hash<Symbol, FiberUnits::Length>]
|
|
49
|
+
# @raise [ArgumentError] if the size is not in the chart
|
|
50
|
+
def size(name)
|
|
51
|
+
raise ArgumentError, "unknown size #{name.inspect}" unless chart.key?(name)
|
|
52
|
+
|
|
53
|
+
chart[name]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Finds the closest standard size to the given body measurements.
|
|
57
|
+
#
|
|
58
|
+
# Compares using the sum of squared differences across all provided
|
|
59
|
+
# measurements. Only measurements present in both the query and the
|
|
60
|
+
# chart are compared.
|
|
61
|
+
#
|
|
62
|
+
# @param measurements [Hash<Symbol, FiberUnits::Length>] body measurements to match
|
|
63
|
+
# @return [Symbol] the closest size name
|
|
64
|
+
# @raise [ArgumentError] if no measurements overlap with the chart
|
|
65
|
+
def closest_size(**measurements)
|
|
66
|
+
comparable_keys = measurements.keys & chart.values.first.keys
|
|
67
|
+
raise ArgumentError, "no comparable measurements provided" if comparable_keys.empty?
|
|
68
|
+
|
|
69
|
+
sizes.min_by do |size_name|
|
|
70
|
+
size_data = chart[size_name]
|
|
71
|
+
comparable_keys.sum do |key|
|
|
72
|
+
diff = measurements[key].value - size_data[key].value
|
|
73
|
+
diff * diff
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns a BodyMeasurements object for a standard size.
|
|
79
|
+
#
|
|
80
|
+
# @param name [Symbol] size name
|
|
81
|
+
# @return [FiberPattern::BodyMeasurements]
|
|
82
|
+
def body_measurements_for(name)
|
|
83
|
+
BodyMeasurements.new(**size(name))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/fiber_pattern.rb
CHANGED
|
@@ -6,6 +6,13 @@ require_relative "fiber_pattern/version"
|
|
|
6
6
|
require_relative "fiber_pattern/sizing"
|
|
7
7
|
require_relative "fiber_pattern/repeat"
|
|
8
8
|
require_relative "fiber_pattern/scaling"
|
|
9
|
+
require_relative "fiber_pattern/shaping"
|
|
10
|
+
require_relative "fiber_pattern/grade_rules"
|
|
11
|
+
require_relative "fiber_pattern/grader"
|
|
12
|
+
require_relative "fiber_pattern/body_measurements"
|
|
13
|
+
require_relative "fiber_pattern/ease"
|
|
14
|
+
require_relative "fiber_pattern/garment_sizing"
|
|
15
|
+
require_relative "fiber_pattern/size_chart"
|
|
9
16
|
|
|
10
17
|
# Utilities for generating fiber pattern measurements from gauge data.
|
|
11
18
|
module FiberPattern
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Meagan Waller
|
|
@@ -37,6 +37,34 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
40
68
|
description: A Ruby gem for calculating pattern sizes and stitch counts for knitting
|
|
41
69
|
and crochet projects.
|
|
42
70
|
email:
|
|
@@ -48,8 +76,15 @@ files:
|
|
|
48
76
|
- LICENSE.txt
|
|
49
77
|
- README.md
|
|
50
78
|
- lib/fiber_pattern.rb
|
|
79
|
+
- lib/fiber_pattern/body_measurements.rb
|
|
80
|
+
- lib/fiber_pattern/ease.rb
|
|
81
|
+
- lib/fiber_pattern/garment_sizing.rb
|
|
82
|
+
- lib/fiber_pattern/grade_rules.rb
|
|
83
|
+
- lib/fiber_pattern/grader.rb
|
|
51
84
|
- lib/fiber_pattern/repeat.rb
|
|
52
85
|
- lib/fiber_pattern/scaling.rb
|
|
86
|
+
- lib/fiber_pattern/shaping.rb
|
|
87
|
+
- lib/fiber_pattern/size_chart.rb
|
|
53
88
|
- lib/fiber_pattern/sizing.rb
|
|
54
89
|
- lib/fiber_pattern/version.rb
|
|
55
90
|
- sig/fiber_pattern.rbs
|