aurora-sketch 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +7 -0
- data/README.markdown +86 -0
- data/Rakefile +28 -0
- data/aurora-sketch.gemspec +21 -0
- data/lib/geometry/dsl/polyline.rb +107 -0
- data/lib/geometry/dsl/turtle.rb +64 -0
- data/lib/sketch.rb +180 -0
- data/lib/sketch/builder.rb +100 -0
- data/lib/sketch/builder/path.rb +16 -0
- data/lib/sketch/builder/polygon.rb +15 -0
- data/lib/sketch/builder/polyline.rb +51 -0
- data/lib/sketch/dsl.rb +56 -0
- data/lib/sketch/group.rb +38 -0
- data/lib/sketch/layout.rb +118 -0
- data/lib/sketch/point.rb +7 -0
- data/test/geometry/dsl/polyline.rb +81 -0
- data/test/sketch.rb +189 -0
- data/test/sketch/builder.rb +167 -0
- data/test/sketch/builder/path.rb +13 -0
- data/test/sketch/builder/polygon.rb +12 -0
- data/test/sketch/builder/polyline.rb +12 -0
- data/test/sketch/dsl.rb +104 -0
- data/test/sketch/group.rb +23 -0
- data/test/sketch/layout.rb +167 -0
- data/test/sketch/point.rb +44 -0
- data/test/sketch/polygon.rb +42 -0
- metadata +96 -0
@@ -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
|
data/lib/sketch/group.rb
ADDED
@@ -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
|
data/lib/sketch/point.rb
ADDED
@@ -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
|