aurora-sketch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,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