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.
- data/.gitignore +6 -0
- data/Gemfile +7 -0
- data/README.markdown +86 -0
- data/Rakefile +28 -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/sketch-in-ruby.gemspec +21 -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 +100 -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,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
|