draught 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +62 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/draught.gemspec +28 -0
- data/lib/draught.rb +4 -0
- data/lib/draught/arc_builder.rb +161 -0
- data/lib/draught/bounding_box.rb +76 -0
- data/lib/draught/boxlike.rb +148 -0
- data/lib/draught/container.rb +39 -0
- data/lib/draught/corner.rb +163 -0
- data/lib/draught/cubic_bezier.rb +62 -0
- data/lib/draught/curve.rb +66 -0
- data/lib/draught/line.rb +272 -0
- data/lib/draught/path.rb +89 -0
- data/lib/draught/path_builder.rb +39 -0
- data/lib/draught/path_cleaner.rb +77 -0
- data/lib/draught/pathlike.rb +57 -0
- data/lib/draught/point.rb +53 -0
- data/lib/draught/pointlike.rb +35 -0
- data/lib/draught/renderer.rb +93 -0
- data/lib/draught/sheet.rb +45 -0
- data/lib/draught/sheet_builder.rb +144 -0
- data/lib/draught/transformations.rb +63 -0
- data/lib/draught/transformations/affine.rb +33 -0
- data/lib/draught/transformations/common.rb +21 -0
- data/lib/draught/transformations/composer.rb +51 -0
- data/lib/draught/transformations/composition.rb +31 -0
- data/lib/draught/transformations/proclike.rb +43 -0
- data/lib/draught/vector.rb +48 -0
- data/lib/draught/version.rb +3 -0
- metadata +153 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative 'boxlike'
|
2
|
+
require_relative 'point'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
class BoundingBox
|
6
|
+
include Boxlike
|
7
|
+
|
8
|
+
attr_reader :paths
|
9
|
+
|
10
|
+
def initialize(*paths)
|
11
|
+
@paths = paths
|
12
|
+
end
|
13
|
+
|
14
|
+
def width
|
15
|
+
x_max - x_min
|
16
|
+
end
|
17
|
+
|
18
|
+
def height
|
19
|
+
y_max - y_min
|
20
|
+
end
|
21
|
+
|
22
|
+
def lower_left
|
23
|
+
@lower_left ||= Point.new(x_min, y_min)
|
24
|
+
end
|
25
|
+
|
26
|
+
def translate(point)
|
27
|
+
self.class.new(*paths.map { |path| path.translate(point) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def transform(transformer)
|
31
|
+
self.class.new(*paths.map { |path| path.transform(transformer) })
|
32
|
+
end
|
33
|
+
|
34
|
+
def zero_origin
|
35
|
+
move_to(Point::ZERO)
|
36
|
+
end
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
paths == other.paths
|
40
|
+
end
|
41
|
+
|
42
|
+
def containers
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
|
46
|
+
def box_type
|
47
|
+
[:container]
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def x_max
|
53
|
+
@x_max ||= upper_rights.map(&:x).max
|
54
|
+
end
|
55
|
+
|
56
|
+
def x_min
|
57
|
+
@x_min ||= lower_lefts.map(&:x).min
|
58
|
+
end
|
59
|
+
|
60
|
+
def y_max
|
61
|
+
@y_max ||= upper_rights.map(&:y).max
|
62
|
+
end
|
63
|
+
|
64
|
+
def y_min
|
65
|
+
@y_min ||= lower_lefts.map(&:y).min
|
66
|
+
end
|
67
|
+
|
68
|
+
def lower_lefts
|
69
|
+
@lower_lefts ||= paths.map(&:lower_left)
|
70
|
+
end
|
71
|
+
|
72
|
+
def upper_rights
|
73
|
+
@upper_rights ||= paths.map(&:upper_right)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require_relative 'point'
|
2
|
+
require_relative 'vector'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
module Boxlike
|
6
|
+
POSITION_METHODS = [:lower_left, :lower_centre, :lower_right, :centre_right, :upper_right, :upper_centre, :upper_left, :centre_left, :centre]
|
7
|
+
|
8
|
+
def lower_left
|
9
|
+
raise NotImplementedError, "includers of Boxlike must implement #lower_left"
|
10
|
+
end
|
11
|
+
|
12
|
+
def width
|
13
|
+
raise NotImplementedError, "includers of Boxlike must implement #width"
|
14
|
+
end
|
15
|
+
|
16
|
+
def height
|
17
|
+
raise NotImplementedError, "includers of Boxlike must implement #height"
|
18
|
+
end
|
19
|
+
|
20
|
+
def box_type
|
21
|
+
raise NotImplementedError, "includers of Boxlike must implement #box_type"
|
22
|
+
end
|
23
|
+
|
24
|
+
def lower_right
|
25
|
+
@lower_right ||= lower_left.translate(Draught::Vector.new(width, 0))
|
26
|
+
end
|
27
|
+
|
28
|
+
def upper_right
|
29
|
+
@upper_right ||= lower_left.translate(Draught::Vector.new(width, height))
|
30
|
+
end
|
31
|
+
|
32
|
+
def upper_left
|
33
|
+
@upper_left ||= lower_left.translate(Draught::Vector.new(0, height))
|
34
|
+
end
|
35
|
+
|
36
|
+
def centre_left
|
37
|
+
@centre_left ||= lower_left.translate(Draught::Vector.new(0, height/2.0))
|
38
|
+
end
|
39
|
+
|
40
|
+
def lower_centre
|
41
|
+
@lower_centre ||= lower_left.translate(Draught::Vector.new(width/2.0, 0))
|
42
|
+
end
|
43
|
+
|
44
|
+
def centre_right
|
45
|
+
@centre_right ||= lower_right.translate(Draught::Vector.new(0, height / 2.0))
|
46
|
+
end
|
47
|
+
|
48
|
+
def upper_centre
|
49
|
+
@upper_centre ||= upper_left.translate(Draught::Vector.new(width/2.0, 0))
|
50
|
+
end
|
51
|
+
|
52
|
+
def centre
|
53
|
+
@centre ||= lower_left.translate(Draught::Vector.new(width/2.0, height/2.0))
|
54
|
+
end
|
55
|
+
|
56
|
+
def corners
|
57
|
+
[lower_left, lower_right, upper_right, upper_left]
|
58
|
+
end
|
59
|
+
|
60
|
+
def left_edge
|
61
|
+
@left_edge ||= lower_left.x
|
62
|
+
end
|
63
|
+
|
64
|
+
def right_edge
|
65
|
+
@right_edge ||= upper_right.x
|
66
|
+
end
|
67
|
+
|
68
|
+
def top_edge
|
69
|
+
@top_edge ||= upper_right.y
|
70
|
+
end
|
71
|
+
|
72
|
+
def bottom_edge
|
73
|
+
@bottom_edge ||= lower_left.y
|
74
|
+
end
|
75
|
+
|
76
|
+
def move_to(point, opts = {})
|
77
|
+
reference_position_method = opts.fetch(:position, :lower_left)
|
78
|
+
if invalid_position_method?(reference_position_method)
|
79
|
+
raise ArgumentError, ":position option must be a valid position (one of #{POSITION_METHODS.map(&:inspect).join(', ')}), rather than #{opts[:position].inspect}"
|
80
|
+
end
|
81
|
+
|
82
|
+
reference_point = send(reference_position_method)
|
83
|
+
translation = Draught::Vector.translation_between(reference_point, point)
|
84
|
+
return self if translation == Draught::Vector::NULL
|
85
|
+
translate(translation)
|
86
|
+
end
|
87
|
+
|
88
|
+
def translate(point)
|
89
|
+
raise NotImplementedError
|
90
|
+
end
|
91
|
+
|
92
|
+
def transform(transformer)
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
|
96
|
+
def paths
|
97
|
+
raise NotImplementedError
|
98
|
+
end
|
99
|
+
|
100
|
+
def containers
|
101
|
+
raise NotImplementedError
|
102
|
+
end
|
103
|
+
|
104
|
+
def overlaps?(other_box)
|
105
|
+
!disjoint?(other_box)
|
106
|
+
end
|
107
|
+
|
108
|
+
def disjoint?(other_box)
|
109
|
+
horizontal_disjoint?(other_box) || vertical_disjoint?(other_box)
|
110
|
+
end
|
111
|
+
|
112
|
+
def include_point?(point)
|
113
|
+
horizontal_extent.include?(point.x) && vertical_extent.include?(point.y)
|
114
|
+
end
|
115
|
+
|
116
|
+
def min_gap
|
117
|
+
0
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def horizontal_disjoint?(other_box)
|
123
|
+
other_box.left_edge == right_edge || other_box.right_edge == left_edge ||
|
124
|
+
other_box.left_edge > right_edge || other_box.right_edge < left_edge
|
125
|
+
end
|
126
|
+
|
127
|
+
def vertical_disjoint?(other_box)
|
128
|
+
other_box.bottom_edge == top_edge || other_box.top_edge == bottom_edge ||
|
129
|
+
other_box.top_edge < bottom_edge || other_box.bottom_edge > top_edge
|
130
|
+
end
|
131
|
+
|
132
|
+
def horizontal_extent
|
133
|
+
@horizontal_extent ||= lower_left.x..upper_right.x
|
134
|
+
end
|
135
|
+
|
136
|
+
def vertical_extent
|
137
|
+
@vertical_extent ||= lower_left.y..upper_right.y
|
138
|
+
end
|
139
|
+
|
140
|
+
def invalid_position_method?(method_name)
|
141
|
+
!valid_position_method?(method_name)
|
142
|
+
end
|
143
|
+
|
144
|
+
def valid_position_method?(method_name)
|
145
|
+
POSITION_METHODS.include?(method_name)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'boxlike'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
class Container
|
6
|
+
extend Forwardable
|
7
|
+
include Boxlike
|
8
|
+
|
9
|
+
attr_reader :box, :min_gap
|
10
|
+
|
11
|
+
def_delegators :box, :lower_left, :width, :height, :containers
|
12
|
+
|
13
|
+
def initialize(box, opts = {})
|
14
|
+
@box = box
|
15
|
+
@min_gap = opts.fetch(:min_gap, 0)
|
16
|
+
end
|
17
|
+
|
18
|
+
def translate(point)
|
19
|
+
self.class.new(box.translate(point), {min_gap: min_gap})
|
20
|
+
end
|
21
|
+
|
22
|
+
def transform(transformer)
|
23
|
+
transformed_min_gap = Point.new(min_gap,0).transform(transformer).x
|
24
|
+
self.class.new(box.transform(transformer), {min_gap: transformed_min_gap})
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
min_gap == other.min_gap && box == other.box
|
29
|
+
end
|
30
|
+
|
31
|
+
def box_type
|
32
|
+
[:container]
|
33
|
+
end
|
34
|
+
|
35
|
+
def paths
|
36
|
+
[box]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require_relative './path_builder'
|
2
|
+
require_relative './vector'
|
3
|
+
require_relative './line'
|
4
|
+
require_relative './arc_builder'
|
5
|
+
|
6
|
+
module Draught
|
7
|
+
class Corner
|
8
|
+
def self.join_rounded(args)
|
9
|
+
new(args).join
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :radius, :paths
|
13
|
+
|
14
|
+
def initialize(args)
|
15
|
+
@radius = args.fetch(:radius)
|
16
|
+
@paths = args.fetch(:paths)
|
17
|
+
end
|
18
|
+
|
19
|
+
def join
|
20
|
+
paths.inject { |incoming, outgoing|
|
21
|
+
Rounded.join(radius: radius, incoming: incoming, outgoing: outgoing)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
class Rounded
|
26
|
+
def self.join(args)
|
27
|
+
new(args).joined
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :radius, :incoming, :outgoing
|
31
|
+
|
32
|
+
def initialize(args)
|
33
|
+
@radius = args.fetch(:radius)
|
34
|
+
@incoming = args.fetch(:incoming)
|
35
|
+
@outgoing = args.fetch(:outgoing)
|
36
|
+
end
|
37
|
+
|
38
|
+
def joined
|
39
|
+
PathBuilder.connect(
|
40
|
+
incoming_before_final_segment,
|
41
|
+
incoming_final_segment,
|
42
|
+
corner_arc_path,
|
43
|
+
outgoing_first_segment,
|
44
|
+
outgoing_after_first_segment
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def incoming_before_final_segment
|
49
|
+
incoming[0..-2]
|
50
|
+
end
|
51
|
+
|
52
|
+
def incoming_final_segment
|
53
|
+
incoming_line.extend(by: -distance_to_tangent)
|
54
|
+
end
|
55
|
+
|
56
|
+
def incoming_line
|
57
|
+
Line.from_path(incoming[-2,2])
|
58
|
+
end
|
59
|
+
|
60
|
+
def outgoing_first_segment
|
61
|
+
outgoing_line.extend(by: -distance_to_tangent, at: :start)
|
62
|
+
end
|
63
|
+
|
64
|
+
def outgoing_line
|
65
|
+
Line.from_path(outgoing[0..1])
|
66
|
+
end
|
67
|
+
|
68
|
+
def outgoing_after_first_segment
|
69
|
+
outgoing[1..-1]
|
70
|
+
end
|
71
|
+
|
72
|
+
def distance_to_tangent
|
73
|
+
@distance_to_tangent ||= join_angles.tangent_distance(radius)
|
74
|
+
end
|
75
|
+
|
76
|
+
def join_angles
|
77
|
+
@join_angles ||= JoinAngles.new(incoming_line, outgoing_line)
|
78
|
+
end
|
79
|
+
|
80
|
+
def corner_arc_path
|
81
|
+
ArcBuilder.radians(angle: join_angles.arc_sweep, radius: radius, starting_angle: starting_angle).path
|
82
|
+
end
|
83
|
+
|
84
|
+
def starting_angle
|
85
|
+
incoming_line.radians - (Math::PI / 2)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class JoinAngles
|
90
|
+
attr_reader :incoming_line, :outgoing_line
|
91
|
+
|
92
|
+
def initialize(incoming_line, outgoing_line)
|
93
|
+
@incoming_line = incoming_line
|
94
|
+
@outgoing_line = outgoing_line
|
95
|
+
end
|
96
|
+
|
97
|
+
def arc_sweep
|
98
|
+
anticlockwise? ? abs_arc_sweep : abs_arc_sweep * -1
|
99
|
+
end
|
100
|
+
|
101
|
+
def tangent_distance(radius)
|
102
|
+
radius / Math.tan(abs_corner_angle / 2.0)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def abs_corner_angle
|
108
|
+
@abs_corner_angle ||= begin
|
109
|
+
half_corner_angle = Math.asin(corner_top_line.length/incoming_corner_line.length)
|
110
|
+
half_corner_angle * 2.0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def corner_top_line
|
115
|
+
@corner_top_line ||= begin
|
116
|
+
corner_top_line = Line.build({
|
117
|
+
start_point: incoming_corner_line.start_point, end_point: outgoing_corner_line.end_point
|
118
|
+
})
|
119
|
+
corner_top_line.extend(to: corner_top_line.length / 2.0)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def incoming_corner_line
|
124
|
+
@incoming_corner_line ||= zeroed_unit_line_segment(incoming_line, :last)
|
125
|
+
end
|
126
|
+
|
127
|
+
def outgoing_corner_line
|
128
|
+
@outgoing_corner_line ||= zeroed_unit_line_segment(outgoing_line, :first)
|
129
|
+
end
|
130
|
+
|
131
|
+
def zeroed_unit_line_segment(segment, zero_to_point)
|
132
|
+
unit_line_segment = segment.extend(to: 1)
|
133
|
+
unit_line_segment.translate(
|
134
|
+
Vector.translation_to_zero(unit_line_segment.public_send(zero_to_point))
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
def abs_arc_sweep
|
139
|
+
@abs_arc_sweep ||= Math::PI - abs_corner_angle
|
140
|
+
end
|
141
|
+
|
142
|
+
def anticlockwise?
|
143
|
+
!clockwise?
|
144
|
+
end
|
145
|
+
|
146
|
+
def clockwise?
|
147
|
+
if aligned_zeroed_lines[0].x > aligned_zeroed_lines[1].x
|
148
|
+
aligned_zeroed_lines[0].y < aligned_zeroed_lines[2].y
|
149
|
+
else
|
150
|
+
aligned_zeroed_lines[0].y > aligned_zeroed_lines[2].y
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def aligned_zeroed_lines
|
155
|
+
@aligned_zeroed_lines ||= begin
|
156
|
+
joined_lines = PathBuilder.connect(incoming_line, outgoing_line)
|
157
|
+
zeroed_joined_lines = joined_lines.translate(Vector.translation_to_zero(joined_lines.first))
|
158
|
+
zeroed_joined_lines.transform(Transformations.rotate(incoming_line.radians * -1))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative './pointlike'
|
2
|
+
|
3
|
+
module Draught
|
4
|
+
class CubicBezier
|
5
|
+
include Pointlike
|
6
|
+
|
7
|
+
attr_reader :end_point, :control_point_1, :control_point_2
|
8
|
+
|
9
|
+
def initialize(args = {})
|
10
|
+
@end_point = args.fetch(:end_point)
|
11
|
+
@control_point_1 = args.fetch(:control_point_1)
|
12
|
+
@control_point_2 = args.fetch(:control_point_2)
|
13
|
+
end
|
14
|
+
|
15
|
+
def x
|
16
|
+
end_point.x
|
17
|
+
end
|
18
|
+
|
19
|
+
def y
|
20
|
+
end_point.y
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
other.point_type == point_type &&
|
25
|
+
comparison_array(other).all? { |a, b| a == b }
|
26
|
+
end
|
27
|
+
|
28
|
+
def point_type
|
29
|
+
:cubic_bezier
|
30
|
+
end
|
31
|
+
|
32
|
+
def approximates?(other, delta)
|
33
|
+
other.point_type == point_type &&
|
34
|
+
comparison_array(other).all? { |a, b|
|
35
|
+
a.approximates?(b, delta)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def translate(vector)
|
40
|
+
transform(vector.to_transform)
|
41
|
+
end
|
42
|
+
|
43
|
+
def transform(transformer)
|
44
|
+
new_args = Hash[args_hash.map { |k, point|
|
45
|
+
[k, point.transform(transformer)]
|
46
|
+
}]
|
47
|
+
self.class.new(new_args)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def args_hash
|
53
|
+
{end_point: end_point, control_point_1: control_point_1, control_point_2: control_point_2}
|
54
|
+
end
|
55
|
+
|
56
|
+
def comparison_array(other)
|
57
|
+
args_hash.map { |arg, point|
|
58
|
+
[other.send(arg), point]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|