draught 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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