sketch-in-ruby 0.0.1

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.
@@ -0,0 +1,100 @@
1
+ require_relative 'dsl'
2
+ require_relative 'group'
3
+ require_relative 'builder/polyline'
4
+
5
+ class Sketch
6
+ class Builder
7
+ attr_reader :sketch
8
+
9
+ include Sketch::DSL
10
+
11
+ # @group Convenience constants
12
+ Point = Geometry::Point
13
+ Rectangle = Geometry::Rectangle
14
+ Size = Geometry::Size
15
+ # @endgroup
16
+
17
+ def initialize(sketch=nil, &block)
18
+ @sketch = sketch || Sketch.new
19
+ evaluate(&block) if block_given?
20
+ end
21
+
22
+ # Evaluate a block and return a new {Path}
23
+ # Use the trick found here http://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
24
+ # to allow the DSL block to call methods in the enclosing *lexical* scope
25
+ # @return [Sketch] A new {Sketch} initialized with the given block
26
+ def evaluate(&block)
27
+ if block_given?
28
+ @self_before_instance_eval = eval "self", block.binding
29
+ self.instance_eval &block
30
+ end
31
+ @sketch
32
+ end
33
+
34
+ # The second half of the instance_eval delegation trick mentioned at
35
+ # http://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
36
+ def method_missing(method, *args, &block)
37
+ add_symbol = ('add_' + method.to_s).to_sym
38
+ if @sketch.respond_to? add_symbol
39
+ @sketch.send(add_symbol, *args, &block)
40
+ elsif @sketch.respond_to? method
41
+ @sketch.send method, *args, &block
42
+ elsif @self_before_instance_eval.respond_to? method
43
+ @self_before_instance_eval.send method, *args, &block
44
+ else
45
+ super if defined?(super)
46
+ end
47
+ end
48
+
49
+ # @group Accessors
50
+
51
+ # !@attribute [r] elements
52
+ # @return [Array] The current list of elements
53
+ def elements
54
+ @sketch.elements
55
+ end
56
+
57
+ # @endgroup
58
+
59
+ # Define a named parameter
60
+ # @param [Symbol] name The name of the parameter
61
+ # @param [Proc] block A block that evaluates to the value of the parameter
62
+ def let name, &block
63
+ @sketch.define_parameter name, &block
64
+ end
65
+
66
+ # @group Command handlers
67
+
68
+ # Create a {Group} with an optional name and transformation
69
+ def group(*args, &block)
70
+ @sketch.push Sketch::Builder.new(Group.new(*args)).evaluate(&block)
71
+ end
72
+
73
+ # Use the given block to build a {Polyline} and then append it to the {Sketch}
74
+ def polyline(&block)
75
+ push Builder::Polyline.new.evaluate(&block)
76
+ end
77
+
78
+ # Append a new object (with optional transformation) to the {Sketch}
79
+ # @return [Sketch] the {Sketch} that was appended to
80
+ def push(*args)
81
+ @sketch.push *args
82
+ end
83
+
84
+ # Create a {Rectangle} from the given arguments and append it to the {Sketch}
85
+ def rectangle(*args)
86
+ @sketch.push Rectangle.new(*args)
87
+ last
88
+ end
89
+
90
+ # Create a {Group} using the given translation
91
+ # @param [Point] point The distance by which to translate the enclosed geometry
92
+ def translate(*args, &block)
93
+ point = Point[*args]
94
+ raise ArgumentError, 'Translation is limited to 2 dimensions' if point.size > 2
95
+ group(origin:point, &block)
96
+ end
97
+
98
+ # @endgroup
99
+ end
100
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'polyline'
2
+
3
+ class Sketch
4
+ Path = Geometry::Path
5
+
6
+ class Builder
7
+ class Path < Builder::Polyline
8
+ # Evaluate a block and return a new {Path}
9
+ # @return [Path] A new {Path} initialized with the given block
10
+ def evaluate(&block)
11
+ super
12
+ Sketch::Path.new(*@elements)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'polyline'
2
+
3
+ class Sketch
4
+ Polygon = Geometry::Polygon
5
+
6
+ class Builder
7
+ class Polygon < Builder::Polyline
8
+ # @return [Polygon] the {Polygon} resulting from evaluating the given block
9
+ def evaluate(&block)
10
+ super
11
+ Sketch::Polygon.new *@elements
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ require 'geometry/dsl/polyline'
2
+
3
+ class Sketch
4
+ Polyline = Geometry::Polyline
5
+
6
+ class Builder
7
+ class Polyline
8
+ include Geometry::DSL::Polyline
9
+
10
+ def initialize(*args)
11
+ @elements = args || []
12
+ end
13
+
14
+ # Evaluate a block and return a new {Path}
15
+ # Use the trick found here http://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
16
+ # to allow the DSL block to call methods in the enclosing *lexical* scope
17
+ # @return [Polyline] A new {Polyline} initialized with the given block
18
+ def evaluate(&block)
19
+ if block_given?
20
+ @self_before_instance_eval = eval "self", block.binding
21
+ self.instance_eval &block
22
+ end
23
+ Sketch::Polyline.new(*@elements)
24
+ end
25
+
26
+ # The second half of the instance_eval delegation trick mentioned at
27
+ # http://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
28
+ def method_missing(method, *args, &block)
29
+ @self_before_instance_eval.send method, *args, &block
30
+ end
31
+
32
+ # @return [Point] the first vertex of the {Polyline}
33
+ def first
34
+ @elements.first
35
+ end
36
+
37
+ # @return [Point] the last, or most recently added, vertex of the {Polyline}
38
+ def last
39
+ @elements.last
40
+ end
41
+
42
+ # Push the given object
43
+ # @param [Geometry] arg A {Geometry} object to apped to the {Path}
44
+ # @return [Geometry] The appended object
45
+ def push(arg)
46
+ @elements.push arg
47
+ self
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/sketch/dsl.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'geometry'
2
+
3
+ require_relative 'builder'
4
+ require_relative 'layout'
5
+
6
+ # Syntactic sugar for building a {Sketch}
7
+ class Sketch
8
+ module DSL
9
+ # @group Accessors
10
+ # @attribute [r] first
11
+ # @return [Geometry] the first Geometry element of the {Sketch}
12
+ def first
13
+ elements.first
14
+ end
15
+
16
+ # @attribute [r] last
17
+ # @return [Geometry] the last Geometry element of the {Sketch}
18
+ def last
19
+ elements.last
20
+ end
21
+ # @endgroup
22
+
23
+ # Create a {RegularPolygon} with 6 sides
24
+ # @return [RegularPolygon]
25
+ def hexagon(options={})
26
+ options[:sides] = 6
27
+ Geometry::RegularPolygon.new(options).tap {|a| push a }
28
+ end
29
+
30
+ # Create a layout
31
+ # @param direction [Symbol] The layout direction (either :horizontal or :vertical)
32
+ # @option options [Symbol] align :top, :bottom, :left, or :right
33
+ # @option options [Number] spacing The spacing between each element
34
+ # @return [Group]
35
+ def layout(direction, *args, &block)
36
+ Builder.new(Layout.new(direction, *args)).evaluate(&block).tap {|a| push a}
37
+ end
38
+
39
+ # Create a {Path}
40
+ # @return [Path]
41
+ def path(*args, &block)
42
+ Builder::Path.new(*args).evaluate(&block).tap {|a| push a }
43
+ end
44
+
45
+ # Create a Polygon with the given vertices, or using a block.
46
+ # See {PolygonBuilder}
47
+ def polygon(*args, &block)
48
+ if block_given?
49
+ push Builder::Polygon.new.evaluate(&block)
50
+ else
51
+ push Sketch::Polygon.new(*args)
52
+ end
53
+ last
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ require 'geometry/transformation'
2
+
3
+ =begin
4
+ {Group} is a container for grouping elements of a {Sketch} with an optional
5
+ {Geometry::Transformation Transformation} property.
6
+
7
+ sketch :ExampleModel do
8
+ group origin:[4,2] do
9
+ circle diameter:5.meters
10
+ end
11
+ end
12
+ =end
13
+ class Sketch
14
+ class Group < Sketch
15
+ attr_reader :transformation
16
+
17
+ def initialize(*args, &block)
18
+ super &block
19
+
20
+ options, args = args.partition {|a| a.is_a? Hash}
21
+ options = options.reduce({}, :merge)
22
+
23
+ @transformation = options.delete(:transformation) || Geometry::Transformation.new(options)
24
+ end
25
+
26
+ def rotation
27
+ @transformation.rotation
28
+ end
29
+
30
+ def scale
31
+ @transformation.scale
32
+ end
33
+
34
+ def translation
35
+ @transformation.translation
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,118 @@
1
+ require_relative 'group'
2
+
3
+ =begin
4
+ {Layout} is a container that arranges its child elements in the specified
5
+ direction and, optionally, with the specified spacing.
6
+
7
+ For example, to create two adjacent squares along the X-axis, use:
8
+
9
+ Layout.new :horizontal do
10
+ square size:5
11
+ square size:6
12
+ end
13
+
14
+ =end
15
+ class Sketch
16
+ class Layout < Group
17
+ # @return [Symbol] alignment the layout alignment
18
+ attr_reader :alignment
19
+
20
+ # @return [Symbol] direction the layout direction (either :horizontal or :vertical)
21
+ attr_reader :direction
22
+
23
+ # @return [Number] spacing spacing to add between each element
24
+ attr_reader :spacing
25
+
26
+ def initialize(direction=:horizontal, *args)
27
+ super
28
+
29
+ options, args = args.partition {|a| a.is_a? Hash}
30
+ options = options.reduce({}, :merge)
31
+
32
+ @alignment = options.delete(:align)
33
+ @spacing = options.delete(:spacing) || 0
34
+
35
+ @direction = direction
36
+ end
37
+
38
+ # Any pushed element that doesn't have a transformation property will be wrapped in a {Group}.
39
+ # @param element [Geometry] the geometry element to append
40
+ # @return [Layout]
41
+ def push(element, *args)
42
+ max = last ? last.max : Point.zero
43
+
44
+ offset = make_offset(element, element.min, max)
45
+
46
+ if offset == Point.zero
47
+ super element, *args
48
+ else
49
+ if element.respond_to?(:transformation=)
50
+ super element, *args
51
+ else
52
+ super Group.new.push element, *args
53
+ end
54
+
55
+ last.transformation = Geometry::Transformation.new(origin:offset) + last.transformation
56
+ end
57
+
58
+ self
59
+ end
60
+
61
+ private
62
+
63
+ def make_offset(element, min, max)
64
+ case direction
65
+ when :horizontal
66
+ y_offset = case alignment
67
+ when :bottom
68
+ -min.y
69
+ when :top
70
+ height = element.size.y
71
+ if height > max.y
72
+ # Translate everything up by reparenting into a new group
73
+ if elements.size != 0
74
+ alignment_group = Group.new
75
+ alignment_group.transformation = Geometry::Transformation.new(origin: [0, height - max.y])
76
+ elements.each {|a| alignment_group.push a }
77
+ elements.replace [alignment_group]
78
+ end
79
+
80
+ -min.y # Translate the new element to the x-axis
81
+ else
82
+ # Translate the new element to align with the previous element
83
+ max.y - height
84
+ end
85
+ else
86
+ 0
87
+ end
88
+
89
+ Point[max.x - min.x + ((elements.size != 0) ? spacing : 0), y_offset]
90
+ when :vertical
91
+ x_offset = case alignment
92
+ when :left
93
+ -min.x
94
+ when :right
95
+ width = element.size.x
96
+ if width > max.x
97
+ # Translate everything right by reparenting into a new group
98
+ if elements.size != 0
99
+ alignment_group = Group.new
100
+ alignment_group.transformation = Geometry::Transformation.new(origin: [width - max.x, 0])
101
+ elements.each {|a| alignment_group.push a }
102
+ elements.replace [alignment_group]
103
+ end
104
+
105
+ -min.x # Translate the new element to the y-axis
106
+ else
107
+ # Translate the new element to align with the previous element
108
+ max.x - width
109
+ end
110
+ else
111
+ 0
112
+ end
113
+
114
+ Point[x_offset, max.y - min.y + ((elements.size != 0) ? spacing : 0)]
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,7 @@
1
+ require 'geometry'
2
+ require 'matrix'
3
+
4
+ class Sketch
5
+ class Point < Geometry::Point
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "sketch-in-ruby"
6
+ s.version = '0.0.1'
7
+ s.authors = ["Brandon Fosdick", "Meseker Yohannes"]
8
+ s.email = ["meseker.yohannes@gmail.com"]
9
+ s.homepage = "http://github.com/meseker/sketch"
10
+ s.summary = %q{2D mechanical sketches}
11
+ s.description = %q{Sketches used in the creation of mechanical designs}
12
+
13
+ s.rubyforge_project = "sketch"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency 'geometry-in-ruby'
21
+ end
@@ -0,0 +1,81 @@
1
+ require 'minitest/autorun'
2
+ require 'geometry/dsl/polyline'
3
+
4
+ class Fake
5
+ attr_accessor :elements
6
+
7
+ include Geometry::DSL::Polyline
8
+
9
+ def initialize
10
+ @elements = []
11
+ end
12
+
13
+ def first
14
+ @elements.first
15
+ end
16
+
17
+ def last
18
+ @elements.last
19
+ end
20
+
21
+ def push(arg)
22
+ @elements.push arg
23
+ end
24
+ end
25
+
26
+ describe Geometry::DSL::Polyline do
27
+ subject { Fake.new }
28
+
29
+ it 'must have a start command' do
30
+ subject.start_at [1,2]
31
+ subject.last.must_equal Point[1,2]
32
+ end
33
+
34
+ describe 'when started' do
35
+ before do
36
+ subject.start_at [1,2]
37
+ end
38
+
39
+ it 'must refuse to start again' do
40
+ -> { subject.start_at [2,3] }.must_raise Geometry::DSL::Polyline::BuildError
41
+ end
42
+
43
+ it 'must have a close command' do
44
+ subject.move_to [3,4]
45
+ subject.move_to [4,4]
46
+ subject.close
47
+ subject.last.must_equal Point[1,2]
48
+ end
49
+
50
+ it 'must have a move_to command' do
51
+ subject.move_to [3,4]
52
+ subject.last.must_equal Point[3,4]
53
+ end
54
+
55
+ it 'must have a relative horizontal move command' do
56
+ subject.move_x 3
57
+ subject.last.must_equal Point[4,2]
58
+ end
59
+
60
+ it 'must have a relative vertical move command' do
61
+ subject.move_y 4
62
+ subject.last.must_equal Point[1,6]
63
+ end
64
+
65
+ it 'must have an up command' do
66
+ subject.up(3).last.must_equal Point[1,5]
67
+ end
68
+
69
+ it 'must have a down command' do
70
+ subject.down(3).last.must_equal Point[1,-1]
71
+ end
72
+
73
+ it 'must have a left command' do
74
+ subject.left(3).last.must_equal Point[-2,2]
75
+ end
76
+
77
+ it 'must have a right command' do
78
+ subject.right(3).last.must_equal Point[4,2]
79
+ end
80
+ end
81
+ end