scad-btw 0.1.0.pre1

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +17 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +13 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +31 -0
  9. data/Rakefile +12 -0
  10. data/exe/scad-btw +6 -0
  11. data/flake.lock +306 -0
  12. data/flake.nix +63 -0
  13. data/lib/extensions/jenncad.rb +91 -0
  14. data/lib/extensions/kernel.rb +17 -0
  15. data/lib/extensions/thor.rb +11 -0
  16. data/lib/scad/btw/base.rb +224 -0
  17. data/lib/scad/btw/cli/base.rb +31 -0
  18. data/lib/scad/btw/cli/concerns/export_formats.rb +43 -0
  19. data/lib/scad/btw/cli/concerns/export_options.rb +62 -0
  20. data/lib/scad/btw/cli/presets.rb +66 -0
  21. data/lib/scad/btw/cli/solid.rb +33 -0
  22. data/lib/scad/btw/cli/spoked.rb +33 -0
  23. data/lib/scad/btw/component/axle_hole.rb +53 -0
  24. data/lib/scad/btw/component/center.rb +58 -0
  25. data/lib/scad/btw/component/counterweight.rb +117 -0
  26. data/lib/scad/btw/component/flange.rb +96 -0
  27. data/lib/scad/btw/component/joint.rb +57 -0
  28. data/lib/scad/btw/component/joint_base.rb +34 -0
  29. data/lib/scad/btw/component/o_ring_groove.rb +95 -0
  30. data/lib/scad/btw/component/solid.rb +67 -0
  31. data/lib/scad/btw/component/spokes.rb +118 -0
  32. data/lib/scad/btw/component/tread.rb +109 -0
  33. data/lib/scad/btw/concerns/component_args.rb +26 -0
  34. data/lib/scad/btw/concerns/flange_heights.rb +24 -0
  35. data/lib/scad/btw/concerns/joint_args.rb +58 -0
  36. data/lib/scad/btw/concerns/part_commons.rb +29 -0
  37. data/lib/scad/btw/concerns/tread_width.rb +21 -0
  38. data/lib/scad/btw/converters/openscad_2_ascii_stl.rb +39 -0
  39. data/lib/scad/btw/presets/scale_1_38/cargo_wheel_thin_36_inch.rb +21 -0
  40. data/lib/scad/btw/presets/scale_1_38/v20.rb +83 -0
  41. data/lib/scad/btw/solid.rb +13 -0
  42. data/lib/scad/btw/spoked.rb +13 -0
  43. data/lib/scad/btw/version.rb +7 -0
  44. data/lib/scad/btw.rb +29 -0
  45. data/sig/scad/btw.rbs +6 -0
  46. metadata +150 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extensions/jenncad'
4
+
5
+ module Extensions
6
+ module Kernel
7
+ def require(path)
8
+ ret = super
9
+
10
+ if path == 'jenncad/profile_loader'
11
+ ::JennCad::ProfileLoader.prepend Extensions::JennCad::ProfileLoader
12
+ end
13
+
14
+ ret
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Extensions
4
+ module Thor
5
+ module Option
6
+ def show_default?
7
+ super || [TrueClass, FalseClass].any? { |c| default.is_a?(c) }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'scad/btw/concerns/component_args'
4
+ require 'scad/btw/concerns/part_commons'
5
+
6
+ require 'scad/btw/component/axle_hole'
7
+ require 'scad/btw/component/center'
8
+ require 'scad/btw/component/counterweight'
9
+ require 'scad/btw/component/flange'
10
+ require 'scad/btw/component/joint_base'
11
+ require 'scad/btw/component/joint'
12
+ require 'scad/btw/component/o_ring_groove'
13
+ require 'scad/btw/component/solid'
14
+ require 'scad/btw/component/spokes'
15
+ require 'scad/btw/component/tread'
16
+
17
+ module Scad
18
+ module Btw
19
+ class Base < JennCad::Part
20
+ include Scad::Btw::Concerns::PartCommons
21
+
22
+ # Wheel size mapping
23
+ # See eurobricks.com forum post https://bit.ly/3ZARZUZ for details
24
+ OLD_2_NEW_SIZE_MAP = {
25
+ 'XXL' => 13,
26
+ 'XLL' => 12,
27
+ 'XL' => 11,
28
+ 'LL' => 10,
29
+ 'L' => 9,
30
+ 'ML' => 8,
31
+ 'M' => 7,
32
+ 'MS' => 6,
33
+ 'S' => 5
34
+ }.freeze
35
+
36
+ DEFAULTS = {
37
+ COLOR: :gray,
38
+ HEIGHT: 8,
39
+ INNER_HEIGHT_DIFF: -0.5,
40
+ IS_BLIND: false,
41
+ IS_THIN: true,
42
+ THIN_HEIGHT: 5,
43
+ WITH_COUNTERWEIGHT: false,
44
+ WITH_JOINT: false,
45
+ WITH_O_RING_GROOVE: false
46
+ }.freeze
47
+
48
+ COMPONENT_ARGS_MAPPING = {
49
+ axle: { height: :height },
50
+ center: { height: :height },
51
+ counterweight: { height: :inner_height, diameter: :size_2_diameter },
52
+ flange: { diameter: :size_2_diameter },
53
+ joint: { height: :height },
54
+ o_ring_groove: { diameter: :size_2_diameter, thin: :thin? },
55
+ solid: { height: :inner_height, diameter: :size_2_diameter },
56
+ spokes: { height: :inner_height, diameter: :size_2_diameter },
57
+ tread: { diameter: :size_2_diameter, height: :height }
58
+ }.freeze
59
+
60
+ ARGS = {
61
+ prefix: :wheel,
62
+ args: {
63
+ size: {
64
+ description: 'Wheel size.', required: true, type: :string,
65
+ enum: OLD_2_NEW_SIZE_MAP.keys + OLD_2_NEW_SIZE_MAP.values.map(&:to_s)
66
+ },
67
+ is_thin: {
68
+ description: 'Whether wheel is thin (5 mm) or not (8 mm).', default: DEFAULTS[:IS_THIN], type: :boolean
69
+ },
70
+ inner_height: {
71
+ description: "Height of spokes or web. Defaults to: Wheel thickness #{DEFAULTS[:INNER_HEIGHT_DIFF]}",
72
+ type: :numeric
73
+ },
74
+ with_o_ring_groove: {
75
+ description: 'Whether wheel has o-ring groove.', default: DEFAULTS[:WITH_O_RING_GROOVE], type: :boolean
76
+ },
77
+ with_joint: {
78
+ description: 'Whether wheel has joint.', default: DEFAULTS[:WITH_JOINT], type: :boolean
79
+ },
80
+ with_counterweight: {
81
+ description: 'Whether wheel has counterweight.', default: DEFAULTS[:WITH_COUNTERWEIGHT], type: :boolean
82
+ }
83
+ }
84
+ }.freeze
85
+
86
+ def initialize(args = {})
87
+ args = {}.merge(args.transform_keys(&:to_sym))
88
+
89
+ super(args)
90
+
91
+ @args = args
92
+ end
93
+
94
+ def part
95
+ (parts_additions - parts_substractions).color(color)
96
+ end
97
+
98
+ def self.component_classes
99
+ Scad::Btw::Component.constants.map(&Scad::Btw::Component.method(:const_get)).select { |c| c.is_a? Class }
100
+ end
101
+
102
+ def self.skip_components
103
+ []
104
+ end
105
+
106
+ private
107
+
108
+ attr_reader :args
109
+
110
+ def skip_components
111
+ self.class.skip_components
112
+ end
113
+
114
+ def parts_additions
115
+ res = nil
116
+
117
+ component_classes.each do |c|
118
+ next if skip_component?(c, :addition)
119
+
120
+ res ? res += component_part(c) : res = component_part(c)
121
+ end
122
+
123
+ res
124
+ end
125
+
126
+ def parts_substractions
127
+ res = nil
128
+
129
+ component_classes.each do |c|
130
+ next if skip_component?(c, :substraction)
131
+
132
+ res ? res += component_part(c) : res = component_part(c)
133
+ end
134
+
135
+ res
136
+ end
137
+
138
+ # components
139
+
140
+ def component_part(component_class)
141
+ component_class.new(component_args(component_class)).part
142
+ end
143
+
144
+ def component_args(component_class)
145
+ component_global_args(component_class).merge(component_class.component_args(args)).merge(misc_args)
146
+ end
147
+
148
+ def component_global_args(component_class)
149
+ prefix = component_class::ARGS[:prefix]
150
+
151
+ COMPONENT_ARGS_MAPPING[prefix].transform_values { |v| send(v) }
152
+ end
153
+
154
+ def skip_component?(component_class, type)
155
+ return true unless type == component_class::TYPE
156
+ return true if skip_components.include?(component_class::ARGS[:prefix])
157
+ return true if not_with?(component_class::ARGS[:prefix])
158
+
159
+ false
160
+ end
161
+
162
+ def not_with?(component)
163
+ return false unless %i[counterweight joint o_ring_groove].include?(component)
164
+
165
+ !send("with_#{component}?".to_sym)
166
+ end
167
+
168
+ def component_classes
169
+ self.class.component_classes
170
+ end
171
+
172
+ # functions
173
+
174
+ def misc_args
175
+ args.except do |key, _|
176
+ component_classes.join(self.class).map { |c| c.component_args_key_map.values }.flatten.include?(key)
177
+ end
178
+ end
179
+
180
+ def height
181
+ thin? ? DEFAULTS[:THIN_HEIGHT] : DEFAULTS[:HEIGHT]
182
+ end
183
+
184
+ def thin?
185
+ args.fetch(:wheel_is_thin, DEFAULTS[:IS_THIN])
186
+ end
187
+
188
+ def inner_height
189
+ args[:wheel_inner_height] || (height + DEFAULTS[:INNER_HEIGHT_DIFF])
190
+ end
191
+
192
+ # See eurobricks.com forum post https://bit.ly/3ZARZUZ for details
193
+ def size_2_diameter
194
+ (size + 0.5) * 3.2
195
+ end
196
+
197
+ def size
198
+ provided_size = args.fetch(:wheel_size)
199
+
200
+ mapped_size = OLD_2_NEW_SIZE_MAP[provided_size] || provided_size
201
+
202
+ raise "Size #{provided_size} not supportet!" unless OLD_2_NEW_SIZE_MAP.values.include?(mapped_size.to_i)
203
+
204
+ mapped_size.to_f
205
+ end
206
+
207
+ def color
208
+ args[:wheel_color] || DEFAULTS[:COLOR]
209
+ end
210
+
211
+ def with_counterweight?
212
+ args[:wheel_with_counterweight] || with_joint? || DEFAULTS[:WITH_COUNTERWEIGHT]
213
+ end
214
+
215
+ def with_o_ring_groove?
216
+ args[:wheel_with_o_ring_groove] || DEFAULTS[:WITH_O_RING_GROOVE]
217
+ end
218
+
219
+ def with_joint?
220
+ args[:wheel_with_joint] || DEFAULTS[:WITH_JOINT]
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'extensions/thor'
5
+ Thor::Option.prepend Extensions::Thor::Option
6
+
7
+ require 'scad/btw'
8
+ require_relative 'presets'
9
+ require_relative 'solid'
10
+ require_relative 'spoked'
11
+
12
+ module Scad
13
+ module Btw
14
+ module Cli
15
+ class Base < Thor
16
+ desc 'preset SUBCOMMAND ...ARGS', 'Generate from presets'
17
+ subcommand 'preset', Scad::Btw::Cli::Presets
18
+
19
+ desc 'solid SUBCOMMAND ...ARGS', 'Generate solid wheel'
20
+ subcommand 'solid', Scad::Btw::Cli::Solid
21
+
22
+ desc 'spoked SUBCOMMAND ...ARGS', 'Generate spoked wheel'
23
+ subcommand 'spoked', Scad::Btw::Cli::Spoked
24
+
25
+ def self.exit_on_failure?
26
+ true
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module Scad
6
+ module Btw
7
+ module Cli
8
+ module Concerns
9
+ module ExportFormats
10
+ extend ActiveSupport::Concern
11
+
12
+ private
13
+
14
+ def export_part(part, file, format = 'scad')
15
+ case format
16
+ when 'scad'
17
+ export_part_to_scad(part, file)
18
+ when 'stl'
19
+ export_stl(part, file)
20
+ else
21
+ puts "Unknown export format `#{format}`. Choose one of `scad` and `stl`."
22
+ end
23
+ end
24
+
25
+ def export_part_to_scad(part, file)
26
+ JennCad::Exporters::OpenScad.new(part).save(file)
27
+ end
28
+
29
+ def export_stl(part, file)
30
+ tempfile = Tempfile.new('scad')
31
+
32
+ export_part_to_scad(part, tempfile.path)
33
+
34
+ Scad::Btw::Converters::Openscad2AsciiStl.convert(in_path: tempfile.path, out_path: file)
35
+ ensure
36
+ tempfile.close
37
+ tempfile.unlink
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scad
4
+ module Btw
5
+ module Cli
6
+ module Concerns
7
+ module ExportOptions
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def _gen_options
12
+ _wheel_options + _component_options
13
+ end
14
+
15
+ def _wheel_options
16
+ _part_options(klass: Object.const_get(self::WHEEL_CLASS_NAME))
17
+ end
18
+
19
+ def _part_options(klass:)
20
+ klass::ARGS[:args].keys.map do |key|
21
+ _option_args(
22
+ prefix: klass::ARGS[:prefix],
23
+ name: key,
24
+ arg: klass::ARGS[:args][key]
25
+ )
26
+ end
27
+ end
28
+
29
+ def _component_options
30
+ Object.const_get(self::WHEEL_CLASS_NAME).component_classes
31
+ .map do |cc|
32
+ _component_option(klass: cc)
33
+ end.flatten
34
+ end
35
+
36
+ def _component_option(klass:)
37
+ if Object.const_get(self::WHEEL_CLASS_NAME).skip_components
38
+ .include?(klass::ARGS[:prefix])
39
+ []
40
+ else
41
+ _part_options(klass: klass)
42
+ end
43
+ end
44
+
45
+ def _option_args(prefix:, name:, arg:)
46
+ args = {
47
+ name: "#{prefix}_#{name}".to_sym,
48
+ kwargs: { desc: arg[:description] }
49
+ }
50
+
51
+ %i[type default required enum hide].each do |option|
52
+ args[:kwargs][option] = arg[option] if arg.key?(option)
53
+ end
54
+
55
+ args
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'scad/btw/cli/concerns/export_formats'
5
+
6
+ module Scad
7
+ module Btw
8
+ module Cli
9
+ class Presets < Thor
10
+ include ActiveSupport::Inflector
11
+ include Scad::Btw::Cli::Concerns::ExportFormats
12
+
13
+ PRESETS_NAMESPACE = 'Scad::Btw::Presets'
14
+
15
+ desc 'list', 'List available presets'
16
+ def list
17
+ print list_output
18
+ end
19
+
20
+ option :preset, required: true, type: :string
21
+ desc 'export FILE [FORMAT]', 'Export preset to file in either `scad` (default) or `stl` format'
22
+ def export(file, format = 'scad')
23
+ export_part(part, file, format)
24
+ end
25
+
26
+ private
27
+
28
+ def part
29
+ preset_class_name = "#{PRESETS_NAMESPACE}::#{camelize(underscore(options[:preset]))}"
30
+
31
+ constantize(preset_class_name).to_openscad
32
+ end
33
+
34
+ def list_output
35
+ <<~LIST_OUTPUT
36
+ Available presets:
37
+
38
+ #{presets.map { |p| " - #{p}" }.join("\n")}
39
+
40
+ Use `scad-btw preset export --preset <preset> <file>` to export a
41
+ preset to openscad.
42
+ LIST_OUTPUT
43
+ end
44
+
45
+ def presets
46
+ get_classes(constantize(PRESETS_NAMESPACE)).map do |c|
47
+ dasherize(underscore(c.name.delete_prefix("#{PRESETS_NAMESPACE}::")))
48
+ end
49
+ end
50
+
51
+ def get_classes(mod)
52
+ mod.constants.map do |c|
53
+ const = mod.const_get(c)
54
+ if const.is_a? Class
55
+ const
56
+ elsif const.is_a? Module
57
+ get_classes(const)
58
+ else
59
+ next
60
+ end
61
+ end.flatten
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'scad/btw/cli/concerns/export_options'
5
+ require 'scad/btw/cli/concerns/export_formats'
6
+
7
+ module Scad
8
+ module Btw
9
+ module Cli
10
+ class Solid < Thor
11
+ include Scad::Btw::Cli::Concerns::ExportOptions
12
+ include Scad::Btw::Cli::Concerns::ExportFormats
13
+ include ActiveSupport::Inflector
14
+
15
+ WHEEL_CLASS_NAME = 'Scad::Btw::Solid'
16
+
17
+ _gen_options.each do |conf|
18
+ option(conf[:name], conf[:kwargs])
19
+ end
20
+ desc 'export FILE [FORMAT]', 'Export solid wheel to file in either `scad` (default) or `stl` format'
21
+ def export(file, format = 'scad')
22
+ export_part(part, file, format)
23
+ end
24
+
25
+ private
26
+
27
+ def part
28
+ constantize(WHEEL_CLASS_NAME).to_openscad(options)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'scad/btw/cli/concerns/export_options'
5
+ require 'scad/btw/cli/concerns/export_formats'
6
+
7
+ module Scad
8
+ module Btw
9
+ module Cli
10
+ class Spoked < Thor
11
+ include Scad::Btw::Cli::Concerns::ExportOptions
12
+ include Scad::Btw::Cli::Concerns::ExportFormats
13
+ include ActiveSupport::Inflector
14
+
15
+ WHEEL_CLASS_NAME = 'Scad::Btw::Spoked'
16
+
17
+ _gen_options.each do |conf|
18
+ option(conf[:name], conf[:kwargs])
19
+ end
20
+ desc 'export FILE [FORMAT]', 'Export solid wheel to file in either `scad` (default) or `stl` format'
21
+ def export(file, format = 'scad')
22
+ export_part(part, file, format)
23
+ end
24
+
25
+ private
26
+
27
+ def part
28
+ constantize(WHEEL_CLASS_NAME).to_openscad(options)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'scad/btw/concerns/component_args'
4
+ require 'scad/btw/concerns/part_commons'
5
+
6
+ module Scad
7
+ module Btw
8
+ module Component
9
+ class AxleHole < JennCad::Part
10
+ include Scad::Btw::Concerns::PartCommons
11
+ include Scad::Btw::Concerns::ComponentArgs
12
+
13
+ TYPE = :substraction
14
+ SPLINE_WIDTH = 2
15
+ DIAMETER = 5
16
+
17
+ ARGS = {
18
+ prefix: :axle,
19
+ args: {
20
+ height: {
21
+ description: 'Wheel height. Defaults to: Wheel height defined ' \
22
+ 'by wheel thickness (`wheel_is_thin` flag).',
23
+ type: :numeric,
24
+ hide: true
25
+ }
26
+ }
27
+ }.freeze
28
+
29
+ def initialize(args = {})
30
+ super
31
+
32
+ @height = args.fetch(:height)
33
+ end
34
+
35
+ def part
36
+ res = spline
37
+
38
+ res + spline.rz(90)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :height
44
+
45
+ # components
46
+
47
+ def spline
48
+ cube(x: DIAMETER, y: SPLINE_WIDTH, z: height + 1).mz(-0.5)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'scad/btw/concerns/component_args'
4
+ require 'scad/btw/concerns/part_commons'
5
+
6
+ module Scad
7
+ module Btw
8
+ module Component
9
+ class Center < JennCad::Part
10
+ include Scad::Btw::Concerns::PartCommons
11
+ include Scad::Btw::Concerns::ComponentArgs
12
+
13
+ TYPE = :addition
14
+ MIN_DIAMETER = 7.4
15
+
16
+ ARGS = {
17
+ prefix: :center,
18
+ args: {
19
+ height: {
20
+ description: 'Center height. Defaults to: Wheel height defined ' \
21
+ 'by wheel thickness (`wheel_is_thin` flag).',
22
+ type: :numeric,
23
+ hide: true
24
+ },
25
+ diameter: {
26
+ description: 'Center diameter. Minimum diameter of ' \
27
+ "#{MIN_DIAMETER} will be used if provided value " \
28
+ 'is smaller.',
29
+ default: MIN_DIAMETER,
30
+ type: :numeric
31
+ }
32
+ }
33
+ }.freeze
34
+
35
+ def initialize(args = {})
36
+ super
37
+
38
+ @diameter = args[:diameter] || 0
39
+ @height = args.fetch(:height)
40
+ end
41
+
42
+ def part
43
+ cylinder(d: calculated_diameter, h: height)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :diameter, :height
49
+
50
+ # functions
51
+
52
+ def calculated_diameter
53
+ [diameter, MIN_DIAMETER].max
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end