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 ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ .yardoc
4
+ Gemfile.lock
5
+ pkg/*
6
+ doc
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rake'
7
+ end
data/README.markdown ADDED
@@ -0,0 +1,86 @@
1
+ Sketching with Ruby
2
+ ===================
3
+
4
+ [![Build Status](https://travis-ci.org/bfoz/sketch.png)](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