draught 0.1.0

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.
@@ -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