fiber_pattern 0.3.0 → 0.5.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: 97a576e2a84dd27f7a1690c4bddbe30adfa4eea6bd323aa19dd3389a1e10e5cb
4
- data.tar.gz: fa3d3634bab8fa1cdd9f64b2c2f9027d4afc1361a67a26284ed71df6ec8b353e
3
+ metadata.gz: 86098b154ca5d374ea82112b74c4bcfde885d6da22aa071c4c0f8b87fc2c89bd
4
+ data.tar.gz: 6744cff995e936ff00774ffa69e1829c1992cfe5901d491ba233fcd5e734d5fe
5
5
  SHA512:
6
- metadata.gz: bfc2843e21944d714cfee9270802e629ab1d08e340f1bfb7a7b59f2e5f8bd30469369b6108ad61f3429cfc306150f7af463d53dd43665d572ed6025a33df7c3d
7
- data.tar.gz: 6f065951604cbcf7612ea115b2aceeff0978f71b0620d61ddafe4e0013dfa2f400f9baa084e45d0da432c5ed999913f240e9c53f1c3a79c013537667f743f83a
6
+ metadata.gz: c9daa9034c4a2a6b5d46c3e09a999a78c11245424163ac792194d8e9d575c95f7146d4e447de386c642ea51cc17b9a8b7477533a04ba7080fc0c09b4b5df3c49
7
+ data.tar.gz: a7595ab55c4c0fa5861a39af96d373958cc6f39359f1bb2feddfa3b94187e4de7cf81c403cb3b17ac92c0fcdf395a04526507c22a25c91d04c832b182f828c3d
@@ -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,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
@@ -9,11 +9,16 @@ module FiberPattern
9
9
  # @return [FiberPattern::Repeat, nil] optional stitch repeat to round stitch counts to
10
10
  attr_reader :repeat
11
11
 
12
+ # @return [FiberPattern::StitchPattern, nil] optional stitch pattern for width adjustment
13
+ attr_reader :stitch_pattern
14
+
12
15
  # @param gauge [Object] gauge object used to derive stitch counts
13
16
  # @param repeat [FiberPattern::Repeat, nil] optional stitch repeat to round stitch counts to
14
- def initialize(gauge:, repeat: nil)
17
+ # @param stitch_pattern [FiberPattern::StitchPattern, nil] optional stitch pattern for width adjustment
18
+ def initialize(gauge:, repeat: nil, stitch_pattern: nil)
15
19
  @gauge = gauge
16
20
  @repeat = repeat
21
+ @stitch_pattern = stitch_pattern
17
22
  end
18
23
 
19
24
  # Calculates the number of stitches required to reach a given width, based on the provided gauge.
@@ -21,7 +26,8 @@ module FiberPattern
21
26
  # @param width [Object] desired finished width in units accepted by the gauge
22
27
  # @return [Integer] number of stitches required to reach the requested width
23
28
  def cast_on_for(width)
24
- stitches = gauge.required_stitches(width)
29
+ adjusted_width = stitch_pattern ? stitch_pattern.adjust_width(width) : width
30
+ stitches = gauge.required_stitches(adjusted_width)
25
31
 
26
32
  return stitches unless repeat
27
33
 
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberPattern
4
+ # Models how a stitch pattern affects fabric dimensions and yarn consumption
5
+ # relative to stockinette. Cables compress width and consume more yarn;
6
+ # lace opens up fabric and uses less yarn per area.
7
+ class StitchPattern
8
+ # @return [String] human-readable name of the stitch pattern
9
+ attr_reader :name
10
+
11
+ # @return [FiberPattern::Repeat, nil] optional stitch repeat for this pattern
12
+ attr_reader :repeat
13
+
14
+ # @return [Float] multiplier vs stockinette for fabric width (< 1 = pulls in)
15
+ attr_reader :width_factor
16
+
17
+ # @return [Float] multiplier vs stockinette for yarn consumption (> 1 = uses more)
18
+ attr_reader :yarn_factor
19
+
20
+ # @param name [String] human-readable name of the stitch pattern
21
+ # @param repeat [FiberPattern::Repeat, nil] optional stitch repeat
22
+ # @param width_factor [Float] width multiplier vs stockinette (default: 1.0)
23
+ # @param yarn_factor [Float] yarn consumption multiplier vs stockinette (default: 1.0)
24
+ def initialize(name:, repeat: nil, width_factor: 1.0, yarn_factor: 1.0)
25
+ @name = name.freeze
26
+ @repeat = repeat
27
+ @width_factor = width_factor.to_f
28
+ @yarn_factor = yarn_factor.to_f
29
+ validate!
30
+ end
31
+
32
+ # Returns the stockinette-equivalent width needed to achieve the desired
33
+ # finished width in this stitch pattern.
34
+ #
35
+ # @param width [FiberUnits::Length] desired finished width
36
+ # @return [FiberUnits::Length] stockinette-equivalent width to knit
37
+ def adjust_width(width)
38
+ (width.value / width_factor).round(2).public_send(width.unit)
39
+ end
40
+
41
+ # Adjusts a base yardage estimate to account for this pattern's yarn consumption.
42
+ #
43
+ # @param yardage [FiberUnits::Length] base yardage assuming stockinette
44
+ # @return [FiberUnits::Length] adjusted yardage for this stitch pattern
45
+ def adjust_yardage(yardage)
46
+ (yardage.value * yarn_factor).round(2).public_send(yardage.unit)
47
+ end
48
+
49
+ # Preset definitions for common stitch patterns.
50
+
51
+ def self.stockinette
52
+ new(name: "Stockinette", width_factor: 1.0, yarn_factor: 1.0)
53
+ end
54
+
55
+ def self.garter
56
+ new(name: "Garter", width_factor: 1.0, yarn_factor: 1.05)
57
+ end
58
+
59
+ def self.rib_1x1
60
+ new(
61
+ name: "1x1 Rib",
62
+ repeat: Repeat.new(multiple: 2.stitches),
63
+ width_factor: 0.90,
64
+ yarn_factor: 1.10
65
+ )
66
+ end
67
+
68
+ def self.rib_2x2
69
+ new(
70
+ name: "2x2 Rib",
71
+ repeat: Repeat.new(multiple: 4.stitches),
72
+ width_factor: 0.85,
73
+ yarn_factor: 1.12
74
+ )
75
+ end
76
+
77
+ def self.seed
78
+ new(
79
+ name: "Seed Stitch",
80
+ repeat: Repeat.new(multiple: 2.stitches),
81
+ width_factor: 0.95,
82
+ yarn_factor: 1.05
83
+ )
84
+ end
85
+
86
+ # Crochet preset definitions.
87
+
88
+ def self.single_crochet
89
+ new(name: "Single Crochet", width_factor: 1.0, yarn_factor: 1.0)
90
+ end
91
+
92
+ def self.half_double_crochet
93
+ new(name: "Half Double Crochet", width_factor: 1.05, yarn_factor: 1.15)
94
+ end
95
+
96
+ def self.double_crochet
97
+ new(name: "Double Crochet", width_factor: 1.10, yarn_factor: 1.25)
98
+ end
99
+
100
+ def self.treble_crochet
101
+ new(name: "Treble Crochet", width_factor: 1.15, yarn_factor: 1.35)
102
+ end
103
+
104
+ def self.moss_stitch
105
+ new(
106
+ name: "Moss Stitch",
107
+ repeat: Repeat.new(multiple: 2.stitches),
108
+ width_factor: 1.05,
109
+ yarn_factor: 1.10
110
+ )
111
+ end
112
+
113
+ def self.shell_stitch
114
+ new(
115
+ name: "Shell Stitch",
116
+ repeat: Repeat.new(multiple: 6.stitches, offset: 1.stitches),
117
+ width_factor: 1.15,
118
+ yarn_factor: 1.30
119
+ )
120
+ end
121
+
122
+ def self.v_stitch
123
+ new(
124
+ name: "V-Stitch",
125
+ repeat: Repeat.new(multiple: 2.stitches),
126
+ width_factor: 1.10,
127
+ yarn_factor: 0.90
128
+ )
129
+ end
130
+
131
+ private
132
+
133
+ def validate!
134
+ raise ArgumentError, "name must be a non-empty string" if name.nil? || name.empty?
135
+ raise ArgumentError, "width_factor must be positive" unless width_factor.positive?
136
+ raise ArgumentError, "yarn_factor must be positive" unless yarn_factor.positive?
137
+ end
138
+ end
139
+ end
@@ -3,6 +3,6 @@
3
3
  # :nocov:
4
4
  module FiberPattern
5
5
  # Current gem version.
6
- VERSION = "0.3.0"
6
+ VERSION = "0.5.0"
7
7
  end
8
8
  # :nocov:
data/lib/fiber_pattern.rb CHANGED
@@ -5,10 +5,15 @@ require "fiber_gauge"
5
5
  require_relative "fiber_pattern/version"
6
6
  require_relative "fiber_pattern/sizing"
7
7
  require_relative "fiber_pattern/repeat"
8
+ require_relative "fiber_pattern/stitch_pattern"
8
9
  require_relative "fiber_pattern/scaling"
9
10
  require_relative "fiber_pattern/shaping"
10
11
  require_relative "fiber_pattern/grade_rules"
11
12
  require_relative "fiber_pattern/grader"
13
+ require_relative "fiber_pattern/body_measurements"
14
+ require_relative "fiber_pattern/ease"
15
+ require_relative "fiber_pattern/garment_sizing"
16
+ require_relative "fiber_pattern/size_chart"
12
17
 
13
18
  # Utilities for generating fiber pattern measurements from gauge data.
14
19
  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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Meagan Waller
@@ -76,12 +76,17 @@ files:
76
76
  - LICENSE.txt
77
77
  - README.md
78
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
79
82
  - lib/fiber_pattern/grade_rules.rb
80
83
  - lib/fiber_pattern/grader.rb
81
84
  - lib/fiber_pattern/repeat.rb
82
85
  - lib/fiber_pattern/scaling.rb
83
86
  - lib/fiber_pattern/shaping.rb
87
+ - lib/fiber_pattern/size_chart.rb
84
88
  - lib/fiber_pattern/sizing.rb
89
+ - lib/fiber_pattern/stitch_pattern.rb
85
90
  - lib/fiber_pattern/version.rb
86
91
  - sig/fiber_pattern.rbs
87
92
  homepage: https://github.com/meaganewaller/craftos/tree/main/gems/fiber_pattern