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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
Sketching with Ruby
|
2
|
+
===================
|
3
|
+
|
4
|
+
[](https://travis-ci.org/bfoz/sketch)
|
5
|
+
|
6
|
+
Classes and methods for programmatically creating, manipulating, and exporting
|
7
|
+
simple geometric drawings. This gem is primarily intended to support mechanical
|
8
|
+
design generation, but it can also handle the doodling that you used to do in
|
9
|
+
your notebook while stuck in that really boring class (you know the one).
|
10
|
+
|
11
|
+
At its most basic, Sketch is a container for Geometry objects. The classes in
|
12
|
+
this gem are based on the classes provided by the Geometry gem, but have some
|
13
|
+
extra magic applied to support transformations, constraints, etc. Like the
|
14
|
+
Geometry module, Sketch assumes that primitives lie in 2D space, but doesn't
|
15
|
+
enforce that constraint. Please let me know if you find cases that don't work in
|
16
|
+
higher dimensions and I'll do my best to fix them.
|
17
|
+
|
18
|
+
License
|
19
|
+
-------
|
20
|
+
|
21
|
+
Copyright 2012-2014 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
|
22
|
+
|
23
|
+
Examples
|
24
|
+
--------
|
25
|
+
|
26
|
+
A basic sketch with a single circle
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'sketch'
|
30
|
+
|
31
|
+
sketch = Sketch.new do
|
32
|
+
circle center:[0,0], diameter:5 # Center = [0,0], Radius = 5
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
The same sketch again, but a little more square
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Sketch.new { rectangle origin:[0,0], size:[1,1] }
|
40
|
+
```
|
41
|
+
|
42
|
+
You can also group elements for convenience
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
Sketch.new do
|
46
|
+
group origin:[0,2] do
|
47
|
+
circle center:[-2, 0], radius:1
|
48
|
+
circle center:[2, 0], radius:1
|
49
|
+
end
|
50
|
+
circle center:[0, -1], radius:1
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
There's a shortcut for when you're only creating a group to translate some elements
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
Sketch.new do
|
58
|
+
translate [0,2] do
|
59
|
+
circle center:[-2, 0], radius:1
|
60
|
+
circle center:[2, 0], radius:1
|
61
|
+
end
|
62
|
+
circle center:[0, -1], radius:1
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Sometimes you feel like a group, sometimes you feel like a layout.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Sketch.new do
|
70
|
+
layout :horizontal do
|
71
|
+
circle center:[-2, 0], radius:1
|
72
|
+
circle center:[2, 0], radius:1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
The layout command also takes options for spacing and alignment. For example, to add one unit of extra space between each element, and align them with the X-axis:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
Sketch.new do
|
81
|
+
layout :horizontal, spacing:1, align: :bottom do
|
82
|
+
circle center:[-2, 0], radius:1
|
83
|
+
circle center:[2, 0], radius:1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs.push "lib"
|
8
|
+
t.test_files = FileList['test/**/*.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
task :fixdates do
|
13
|
+
branch = `git branch --no-color -r --merged`.strip
|
14
|
+
`git fix-dates #{branch}..HEAD`
|
15
|
+
end
|
16
|
+
|
17
|
+
task :fixdates_f do
|
18
|
+
branch = `git branch --no-color -r --merged`.strip
|
19
|
+
`git fix-dates -f #{branch}..HEAD`
|
20
|
+
end
|
21
|
+
|
22
|
+
task :trim_whitespace do
|
23
|
+
system(%Q[git status --short | awk '{if ($1 != "D" && $1 != "R") print $2}' | grep -e '.*\.rb$' | xargs sed -i '' -e 's/[ \t]*$//g;'])
|
24
|
+
end
|
25
|
+
|
26
|
+
task :docs do
|
27
|
+
`yard`
|
28
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Geometry
|
2
|
+
module DSL
|
3
|
+
=begin rdoc
|
4
|
+
When you want to draw things that are made of lots and lots of lines, this is how you do it.
|
5
|
+
|
6
|
+
== Requirements
|
7
|
+
This module is intended to be included into a Class, and that Class must provide
|
8
|
+
some infrastructure. It must provide a push method for pushing new elements, a
|
9
|
+
first method that returns the first vertex in the {Polyline}, and a last method
|
10
|
+
that returns the last vertex.
|
11
|
+
|
12
|
+
== Usage
|
13
|
+
begin
|
14
|
+
start_at [0,0]
|
15
|
+
move_y 1 # Go up 1 unit
|
16
|
+
right 1
|
17
|
+
down 1
|
18
|
+
left 1 # Close the box
|
19
|
+
close # Unnecessary in this case
|
20
|
+
end
|
21
|
+
=end
|
22
|
+
module Polyline
|
23
|
+
BuildError = Class.new(StandardError)
|
24
|
+
|
25
|
+
# Close the {Polyline} with a {Line}, if it isn't already closed
|
26
|
+
def close
|
27
|
+
move_to(first) unless closed?
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Bool] True if the {Polyline} is closed, otherwise false
|
31
|
+
def closed?
|
32
|
+
first == last
|
33
|
+
end
|
34
|
+
|
35
|
+
# Draw a line to the given {Point}
|
36
|
+
# @param [Point] The {Point} to draw a line to
|
37
|
+
def move_to(point)
|
38
|
+
push Point[point]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Move the specified distance along the X axis
|
42
|
+
# @param [Number] distance The distance to move
|
43
|
+
def move_x(distance)
|
44
|
+
push (last || PointZero) + Point[distance, 0]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Move the specified distance along the Y axis
|
48
|
+
# @param [Number] distance The distance to move
|
49
|
+
def move_y(distance)
|
50
|
+
push (last || PointZero) + Point[0, distance]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Specify a starting point. Can't be specified twice, and only required if no other entities have been added.
|
54
|
+
# #param [Point] point The starting point
|
55
|
+
def start_at(point)
|
56
|
+
raise BuildError, "Can't specify a start point more than once" if first
|
57
|
+
push Point[point]
|
58
|
+
end
|
59
|
+
|
60
|
+
# @group Relative Movement
|
61
|
+
|
62
|
+
# Move the specified distance along the +Y axis
|
63
|
+
# @param [Number] distance The distance to move in the +Y direction
|
64
|
+
def up(distance)
|
65
|
+
move_y distance
|
66
|
+
end
|
67
|
+
|
68
|
+
# Move the specified distance along the -Y axis
|
69
|
+
# @param [Number] distance The distance to move in the -Y direction
|
70
|
+
def down(distance)
|
71
|
+
move_y -distance
|
72
|
+
end
|
73
|
+
|
74
|
+
# Move the specified distance along the -X axis
|
75
|
+
# @param [Number] distance The distance to move in the -X direction
|
76
|
+
def left(distance)
|
77
|
+
move_x -distance
|
78
|
+
end
|
79
|
+
|
80
|
+
# Move the specified distance along the +X axis
|
81
|
+
# @param [Number] distance The distance to move in the +X direction
|
82
|
+
def right(distance)
|
83
|
+
move_x distance
|
84
|
+
end
|
85
|
+
|
86
|
+
# Draw a vertical line to the given y-coordinate while preserving the
|
87
|
+
# x-coordinate of the previous {Point}
|
88
|
+
# @param y [Number] the y-coordinate to move to
|
89
|
+
def vertical_to(y)
|
90
|
+
push Point[last.x, y]
|
91
|
+
end
|
92
|
+
alias :down_to :vertical_to
|
93
|
+
alias :up_to :vertical_to
|
94
|
+
|
95
|
+
# Draw a horizontal line to the given x-coordinate while preserving the
|
96
|
+
# y-coordinate of the previous {Point}
|
97
|
+
# @param x [Number] the x-coordinate to move to
|
98
|
+
def horizontal_to(x)
|
99
|
+
push [x, last.y]
|
100
|
+
end
|
101
|
+
alias :left_to :horizontal_to
|
102
|
+
alias :right_to :horizontal_to
|
103
|
+
|
104
|
+
# @endgroup
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Geometry
|
2
|
+
module DSL
|
3
|
+
=begin
|
4
|
+
An implementation of {http://en.wikipedia.org/wiki/Turtle_graphics Logo Turtle-style} commands
|
5
|
+
|
6
|
+
== Examples
|
7
|
+
Draw a square...
|
8
|
+
|
9
|
+
PolygonBuilder.new.evaluate do
|
10
|
+
start_at [0,0] # Every journey begins with a single point...
|
11
|
+
move_to [1,0] # Draw a line to a new point
|
12
|
+
turn_left 90
|
13
|
+
move 1 # Same as forward 1
|
14
|
+
turn_left 90
|
15
|
+
forward 1
|
16
|
+
end
|
17
|
+
|
18
|
+
The same thing, but more succint:
|
19
|
+
|
20
|
+
PolygonBuilder.new.evaluate do
|
21
|
+
start_at [0,0]
|
22
|
+
move [1,0] # Move and draw using a vector distance
|
23
|
+
move [0,1]
|
24
|
+
move [-1,0]
|
25
|
+
end
|
26
|
+
=end
|
27
|
+
module Turtle
|
28
|
+
# Turn left by the given number of degrees
|
29
|
+
def turn_left(angle)
|
30
|
+
@direction += angle if @direction
|
31
|
+
@direction ||= angle
|
32
|
+
end
|
33
|
+
|
34
|
+
# Turn right by the given number of degrees
|
35
|
+
def turn_right(angle)
|
36
|
+
turn_left -angle
|
37
|
+
end
|
38
|
+
|
39
|
+
# Draw a line by moving a given distance
|
40
|
+
# @overload move(Numeric)
|
41
|
+
# Same as forward(Numeric)
|
42
|
+
# @overload move(Array)
|
43
|
+
# @overload move(x,y)
|
44
|
+
def move(*distance)
|
45
|
+
return forward(*distance) if (1 == distance.size) && distance[0].is_a?(Numeric)
|
46
|
+
|
47
|
+
if distance[0].is_a?(Vector)
|
48
|
+
distance = distance[0]
|
49
|
+
elsif distance[0].is_a?(Array)
|
50
|
+
distance = Vector[*(distance[0])]
|
51
|
+
end
|
52
|
+
|
53
|
+
push(last + distance)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Move the specified distance in the current direction
|
57
|
+
def forward(distance)
|
58
|
+
@direction ||= 0 # direction defaults to 0
|
59
|
+
radians = @direction * Math::PI / 180
|
60
|
+
push(last + Vector[distance*Math.cos(radians),distance*Math.sin(radians)])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/sketch.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'geometry'
|
2
|
+
require_relative 'sketch/builder'
|
3
|
+
require_relative 'sketch/point.rb'
|
4
|
+
|
5
|
+
=begin
|
6
|
+
A Sketch is a container for Geometry objects.
|
7
|
+
=end
|
8
|
+
|
9
|
+
class Sketch
|
10
|
+
attr_reader :elements
|
11
|
+
attr_accessor :transformation
|
12
|
+
|
13
|
+
Arc = Geometry::Arc
|
14
|
+
Circle = Geometry::Circle
|
15
|
+
Line = Geometry::Line
|
16
|
+
Rectangle = Geometry::Rectangle
|
17
|
+
Square = Geometry::Square
|
18
|
+
|
19
|
+
def initialize(&block)
|
20
|
+
@elements = []
|
21
|
+
instance_eval(&block) if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Define a class parameter
|
25
|
+
# @param [Symbol] name The name of the parameter
|
26
|
+
# @param [Proc] block A block that evaluates to the desired value of the parameter
|
27
|
+
def self.define_parameter name, &block
|
28
|
+
define_method name do
|
29
|
+
@parameters ||= {}
|
30
|
+
@parameters.fetch(name) { |k| @parameters[k] = instance_eval(&block) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Define an instance parameter
|
35
|
+
# @param [Symbol] name The name of the parameter
|
36
|
+
# @param [Proc] block A block that evaluates to the desired value of the parameter
|
37
|
+
def define_parameter name, &block
|
38
|
+
singleton_class.send :define_method, name do
|
39
|
+
@parameters ||= {}
|
40
|
+
@parameters.fetch(name) { |k| @parameters[k] = instance_eval(&block) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @group Accessors
|
45
|
+
# @attribute [r] bounds
|
46
|
+
# @return [Rectangle] The smallest axis-aligned {Rectangle} that encloses all of the elements
|
47
|
+
def bounds
|
48
|
+
Rectangle.new(*minmax)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @attribute [r] first
|
52
|
+
# @return [Geometry] first the first Geometry element of the {Sketch}
|
53
|
+
def first
|
54
|
+
elements.first
|
55
|
+
end
|
56
|
+
|
57
|
+
# @attribute [r] geometry
|
58
|
+
# @return [Array] All elements rendered into Geometry objects
|
59
|
+
def geometry
|
60
|
+
@elements
|
61
|
+
end
|
62
|
+
|
63
|
+
# @attribute [r] last
|
64
|
+
# @return [Geometry] the last Geometry element of the {Sketch}
|
65
|
+
def last
|
66
|
+
elements.last
|
67
|
+
end
|
68
|
+
|
69
|
+
# @attribute [r] max
|
70
|
+
# @return [Point]
|
71
|
+
def max
|
72
|
+
minmax.last
|
73
|
+
end
|
74
|
+
|
75
|
+
# @attribute [r] min
|
76
|
+
# @return [Point]
|
77
|
+
def min
|
78
|
+
minmax.first
|
79
|
+
end
|
80
|
+
|
81
|
+
# @attribute [r] minmax
|
82
|
+
# @return [Array<Point>]
|
83
|
+
def minmax
|
84
|
+
return [nil, nil] unless @elements.size != 0
|
85
|
+
|
86
|
+
memo = @elements.map {|e| e.minmax }.reduce {|memo, e| [Point[[memo.first.x, e.first.x].min, [memo.first.y, e.first.y].min], Point[[memo.last.x, e.last.x].max, [memo.last.y, e.last.y].max]] }
|
87
|
+
if self.transformation
|
88
|
+
if self.transformation.has_rotation?
|
89
|
+
# If the transformation has a rotation, convert the minmax into a bounding rectangle, rotate it, then find the new minmax
|
90
|
+
point1, point3 = Point[memo.last.x, memo.first.y], Point[memo.first.x, memo.last.y]
|
91
|
+
points = [memo.first, point1, memo.last, point3].map {|point| self.transformation.transform(point) }
|
92
|
+
points.reduce([points[0], points[2]]) {|memo, e| [Point[[memo.first.x, e.x].min, [memo.first.y, e.y].min], Point[[memo.last.x, e.x].max, [memo.last.y, e.y].max]] }
|
93
|
+
else
|
94
|
+
memo.map {|point| self.transformation.transform(point) }
|
95
|
+
end
|
96
|
+
else
|
97
|
+
memo
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @attribute [r] size
|
102
|
+
# @return [Size] The size of the {Rectangle} that bounds all of the {Sketch}'s elements
|
103
|
+
def size
|
104
|
+
Size[self.minmax.reverse.reduce(:-).to_a]
|
105
|
+
end
|
106
|
+
|
107
|
+
# @endgroup
|
108
|
+
|
109
|
+
# Append the given {Geometry} element and return the {Sketch}
|
110
|
+
# @param element [Geometry] the {Geometry} element to append
|
111
|
+
# @param args [Array] optional transformation parameters
|
112
|
+
# @return [Sketch]
|
113
|
+
def push(element, *args)
|
114
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
115
|
+
options = options.reduce({}, :merge)
|
116
|
+
|
117
|
+
if options and (options.size != 0) and (element.respond_to? :transformation)
|
118
|
+
element.transformation = Geometry::Transformation.new options
|
119
|
+
end
|
120
|
+
|
121
|
+
@elements.push(element)
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# @group Geometry creation
|
126
|
+
|
127
|
+
# Create and append a new {Arc} object
|
128
|
+
# @param (see Arc#initialize)
|
129
|
+
# @return [Arc]
|
130
|
+
def add_arc(*args)
|
131
|
+
@elements.push(Arc.new(*args)).last
|
132
|
+
end
|
133
|
+
|
134
|
+
# Create and append a new {Circle} object given a center point and radius
|
135
|
+
# @param [Point] center The circle's center point
|
136
|
+
# @param [Number] radius The circle's radius
|
137
|
+
# @return [Circle] A new {Circle}
|
138
|
+
def add_circle(*args)
|
139
|
+
@elements.push Circle.new(*args)
|
140
|
+
@elements.last
|
141
|
+
end
|
142
|
+
|
143
|
+
# Create a Line using any arguments that work for {Geometry::Line}
|
144
|
+
def add_line(*args)
|
145
|
+
@elements.push Line[*args]
|
146
|
+
@elements.last
|
147
|
+
end
|
148
|
+
|
149
|
+
# Create a Point with any arguments that work for {Geometry::Point}
|
150
|
+
def add_point(*args)
|
151
|
+
@elements.push Point[*args]
|
152
|
+
@elements.last
|
153
|
+
end
|
154
|
+
|
155
|
+
# Create a {Rectangle}
|
156
|
+
def add_rectangle(*args)
|
157
|
+
@elements.push Rectangle.new(*args)
|
158
|
+
@elements.last
|
159
|
+
end
|
160
|
+
|
161
|
+
# Create a Square with sides of the given length
|
162
|
+
# @param [Numeric] length The length of the sides of the square
|
163
|
+
def add_square(length)
|
164
|
+
push Geometry::CenteredSquare.new [0,0], length
|
165
|
+
@elements.last
|
166
|
+
end
|
167
|
+
|
168
|
+
# Create and add a {Triangle}
|
169
|
+
# @param (see Triangle::new)
|
170
|
+
def add_triangle(*args)
|
171
|
+
push Geometry::Triangle.new *args
|
172
|
+
end
|
173
|
+
|
174
|
+
# @endgroup
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
def Sketch(&block)
|
179
|
+
Sketch::Builder.new &block
|
180
|
+
end
|