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.
- 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,77 @@
|
|
1
|
+
require_relative 'path'
|
2
|
+
|
3
|
+
module Draught
|
4
|
+
class PathCleaner
|
5
|
+
def self.dedupe(path)
|
6
|
+
Path.new(new(path.points).dedupe)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.simplify(path)
|
10
|
+
Path.new(new(path.points).simplify)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :input_points
|
14
|
+
|
15
|
+
def initialize(input_points)
|
16
|
+
@input_points = input_points
|
17
|
+
end
|
18
|
+
|
19
|
+
def dedupe
|
20
|
+
output_points = [input_points.first]
|
21
|
+
input_points.inject do |previous_point, point|
|
22
|
+
output_points << point if point != previous_point
|
23
|
+
point
|
24
|
+
end
|
25
|
+
output_points
|
26
|
+
end
|
27
|
+
|
28
|
+
def simplify
|
29
|
+
points = dedupe
|
30
|
+
pos = 0
|
31
|
+
while pos < (points.length - 2)
|
32
|
+
triple = points[pos, 3]
|
33
|
+
if intercepts?(*triple)
|
34
|
+
points.delete_at(pos + 1)
|
35
|
+
else
|
36
|
+
pos += 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
points
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def intercepts?(previous_point, point, next_point)
|
45
|
+
intercepts_horizontal?(previous_point, point, next_point) ||
|
46
|
+
intercepts_vertical?(previous_point, point, next_point)
|
47
|
+
end
|
48
|
+
|
49
|
+
def intercepts_horizontal?(previous_point, point, next_point)
|
50
|
+
points = [previous_point, point, next_point]
|
51
|
+
intercepts_line?(points, :x)
|
52
|
+
end
|
53
|
+
|
54
|
+
def intercepts_vertical?(previous_point, point, next_point)
|
55
|
+
points = [previous_point, point, next_point]
|
56
|
+
intercepts_line?(points, :y)
|
57
|
+
end
|
58
|
+
|
59
|
+
def intercepts_line?(points, axis)
|
60
|
+
axis_aligned?(points, perpendicular_axis(axis)) && obviously_intermediate?(points, axis)
|
61
|
+
end
|
62
|
+
|
63
|
+
def perpendicular_axis(axis)
|
64
|
+
{:x => :y, :y => :x}.fetch(axis)
|
65
|
+
end
|
66
|
+
|
67
|
+
def axis_aligned?(points, axis)
|
68
|
+
points.map(&axis).uniq.length == 1
|
69
|
+
end
|
70
|
+
|
71
|
+
def obviously_intermediate?(points, axis)
|
72
|
+
p1, p2, p3 = points.map(&axis)
|
73
|
+
operator = p1 < p3 ? :< : :>
|
74
|
+
p1.send(operator, p2) && p2.send(operator, p3)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Draught
|
2
|
+
module Pathlike
|
3
|
+
def points
|
4
|
+
raise NotImplementedError, "Pathlike objects must return an array of their points"
|
5
|
+
end
|
6
|
+
|
7
|
+
def translate(vector)
|
8
|
+
raise NotImplementedError, "Pathlike objects must implement translation by Vector"
|
9
|
+
end
|
10
|
+
|
11
|
+
def transform(transformation)
|
12
|
+
raise NotImplementedError, "Pathlike objects must implement transformation by Affine transform or point-taking lambda"
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](index_start_or_range, length = nil)
|
16
|
+
raise NotImplementedError, "Pathlike objects must implement [] access on their points, returning a new instance"
|
17
|
+
end
|
18
|
+
|
19
|
+
def number_of_points
|
20
|
+
points.length
|
21
|
+
end
|
22
|
+
|
23
|
+
def first
|
24
|
+
points.first
|
25
|
+
end
|
26
|
+
|
27
|
+
def last
|
28
|
+
points.last
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty?
|
32
|
+
points.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
return false if number_of_points != other.number_of_points
|
37
|
+
points.zip(other.points).all? { |a, b| a == b }
|
38
|
+
end
|
39
|
+
|
40
|
+
def approximates?(other, delta)
|
41
|
+
return false if number_of_points != other.number_of_points
|
42
|
+
points.zip(other.points).all? { |a, b| a.approximates?(b, delta) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def paths
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
|
49
|
+
def containers
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
|
53
|
+
def box_type
|
54
|
+
[:path]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative './vector'
|
2
|
+
require_relative './pointlike'
|
3
|
+
require 'matrix'
|
4
|
+
|
5
|
+
module Draught
|
6
|
+
class Point
|
7
|
+
def self.from_matrix(matrix)
|
8
|
+
x, y = matrix.to_a.flatten
|
9
|
+
Point.new(x, y)
|
10
|
+
end
|
11
|
+
|
12
|
+
include Pointlike
|
13
|
+
|
14
|
+
attr_reader :x, :y
|
15
|
+
|
16
|
+
def initialize(x, y)
|
17
|
+
@x, @y = x, y
|
18
|
+
end
|
19
|
+
|
20
|
+
def point_type
|
21
|
+
:point
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
other.point_type == point_type &&
|
26
|
+
other.x == x && other.y == y
|
27
|
+
end
|
28
|
+
|
29
|
+
def approximates?(other, delta)
|
30
|
+
other.point_type == point_type &&
|
31
|
+
((other.x - x).abs <= delta) &&
|
32
|
+
((other.y - y).abs <= delta)
|
33
|
+
end
|
34
|
+
|
35
|
+
def translate(vector)
|
36
|
+
transform(vector.to_transform)
|
37
|
+
end
|
38
|
+
|
39
|
+
def translation_to(point)
|
40
|
+
Vector.translation_between(self, point)
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_matrix
|
44
|
+
@matrix ||= Matrix[[x],[y],[1]].freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
def transform(transformation)
|
48
|
+
transformation.call(self)
|
49
|
+
end
|
50
|
+
|
51
|
+
ZERO = new(0, 0)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Draught
|
2
|
+
module Pointlike
|
3
|
+
def x
|
4
|
+
raise NotImplementedError, "including classes must return an x value"
|
5
|
+
end
|
6
|
+
|
7
|
+
def y
|
8
|
+
raise NotImplementedError, "including classes must return an y value"
|
9
|
+
end
|
10
|
+
|
11
|
+
def point_type
|
12
|
+
raise NotImplementedError, "including classes must return a Symbol with their point type"
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
raise NotImplementedError, "including classes must implement equality checking. It's assumed other point_types are always unequal"
|
17
|
+
end
|
18
|
+
|
19
|
+
def approximates?(other, delta)
|
20
|
+
raise NotImplementedError, "including classes must implement approximate equality checking. It's assumed other point_types are always unequal"
|
21
|
+
end
|
22
|
+
|
23
|
+
def translate(vector)
|
24
|
+
raise NotImplementedError, "including classes must return a new instance translated by the vector arg"
|
25
|
+
end
|
26
|
+
|
27
|
+
def transform(transformer)
|
28
|
+
raise NotImplementedError, "including classes must return a new instance transformed by the Affine transform or lambda Point-based transform supplied"
|
29
|
+
end
|
30
|
+
|
31
|
+
def points
|
32
|
+
[self]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
require_relative './curve'
|
3
|
+
require_relative './cubic_bezier'
|
4
|
+
|
5
|
+
module Draught
|
6
|
+
class Renderer
|
7
|
+
class PdfContext
|
8
|
+
include Prawn::View
|
9
|
+
|
10
|
+
attr_reader :width, :height
|
11
|
+
|
12
|
+
def initialize(width, height)
|
13
|
+
@width, @height = width, height
|
14
|
+
end
|
15
|
+
|
16
|
+
def document
|
17
|
+
@document ||= Prawn::Document.new(page_size: [width, height], margin: 0)
|
18
|
+
end
|
19
|
+
|
20
|
+
def draw_closed_path(path)
|
21
|
+
points = path.points.dup
|
22
|
+
close_and_stroke do
|
23
|
+
self.line_width = 0.003
|
24
|
+
move_to(*point_tuple(points.shift))
|
25
|
+
points.each do |pointlike|
|
26
|
+
draw_pointlike(pointlike)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def draw_pointlike(pointlike)
|
32
|
+
case pointlike
|
33
|
+
when Draught::Curve
|
34
|
+
pointlike.as_cubic_beziers.each do |cubic_bezier|
|
35
|
+
draw_pointlike(cubic_bezier)
|
36
|
+
end
|
37
|
+
when Draught::CubicBezier
|
38
|
+
curve_to(point_tuple(pointlike.end_point), {
|
39
|
+
bounds: [
|
40
|
+
point_tuple(pointlike.control_point_1),
|
41
|
+
point_tuple(pointlike.control_point_2)
|
42
|
+
]
|
43
|
+
})
|
44
|
+
else
|
45
|
+
line_to(*point_tuple(pointlike))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def point_tuple(point)
|
50
|
+
[point.x, point.y]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.render_to_file(sheet, path)
|
55
|
+
new(sheet).render_to_file(path)
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :root_box
|
59
|
+
|
60
|
+
def initialize(root_box)
|
61
|
+
@root_box = root_box
|
62
|
+
end
|
63
|
+
|
64
|
+
def context
|
65
|
+
@context ||= PdfContext.new(root_box.width, root_box.height)
|
66
|
+
end
|
67
|
+
|
68
|
+
def render_to_file(path)
|
69
|
+
render && context.save_as(path)
|
70
|
+
end
|
71
|
+
|
72
|
+
def render
|
73
|
+
walk(root_box)
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_container(container, context)
|
77
|
+
end
|
78
|
+
|
79
|
+
def render_path(path, context)
|
80
|
+
context.draw_closed_path(path)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def walk(box)
|
86
|
+
render_container(box, context) if box.box_type.include?(:container)
|
87
|
+
box.paths.each do |child|
|
88
|
+
render_path(child, context) if child.box_type.include?(:path)
|
89
|
+
walk(child)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative 'boxlike'
|
2
|
+
require_relative 'point'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
class Sheet
|
6
|
+
include Boxlike
|
7
|
+
|
8
|
+
attr_reader :containers, :lower_left, :width, :height
|
9
|
+
|
10
|
+
def initialize(opts = {})
|
11
|
+
@containers = opts.fetch(:containers)
|
12
|
+
@lower_left = opts.fetch(:lower_left, Point::ZERO)
|
13
|
+
@width = opts.fetch(:width)
|
14
|
+
@height = opts.fetch(:height)
|
15
|
+
end
|
16
|
+
|
17
|
+
def translate(point)
|
18
|
+
tr_lower_left = lower_left.translate(point)
|
19
|
+
tr_containers = containers.map { |container| container.translate(point) }
|
20
|
+
self.class.new(containers: tr_containers, lower_left: tr_lower_left, width: width, height: height)
|
21
|
+
end
|
22
|
+
|
23
|
+
def transform(transformer)
|
24
|
+
tr_lower_left = lower_left.transform(transformer)
|
25
|
+
tr_containers = containers.map { |container| container.transform(transformer) }
|
26
|
+
extent = Point.new(width, height).transform(transformer)
|
27
|
+
tr_width, tr_height = extent.x, extent.y
|
28
|
+
self.class.new({
|
29
|
+
containers: tr_containers, lower_left: tr_lower_left, width: tr_width, height: tr_height
|
30
|
+
})
|
31
|
+
end
|
32
|
+
|
33
|
+
def paths
|
34
|
+
containers
|
35
|
+
end
|
36
|
+
|
37
|
+
def box_type
|
38
|
+
[:container]
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
lower_left == other.lower_left && width == other.width && height == other.height && containers == other.containers
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require_relative 'sheet'
|
2
|
+
require_relative 'point'
|
3
|
+
|
4
|
+
module Draught
|
5
|
+
class SheetBuilder
|
6
|
+
attr_reader :max_height, :max_width, :outer_gap, :boxes
|
7
|
+
|
8
|
+
def self.sheet(args)
|
9
|
+
new(args).sheet
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(opts = {})
|
13
|
+
@max_width = opts.fetch(:max_width)
|
14
|
+
@max_height = opts.fetch(:max_height)
|
15
|
+
@outer_gap = opts.fetch(:outer_gap, 0)
|
16
|
+
@boxes = opts.fetch(:boxes)
|
17
|
+
end
|
18
|
+
|
19
|
+
def sheet
|
20
|
+
containers = nested
|
21
|
+
Sheet.new({
|
22
|
+
lower_left: Point::ZERO,
|
23
|
+
containers: containers,
|
24
|
+
width: width(containers),
|
25
|
+
height: height(containers)
|
26
|
+
})
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
comparison_args.inject(true) { |ok, meth_name|
|
31
|
+
send(meth_name) == other.send(meth_name) && ok
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def comparison_args
|
38
|
+
[:max_width, :max_height, :outer_gap, :boxes]
|
39
|
+
end
|
40
|
+
|
41
|
+
def containers
|
42
|
+
@containers ||= nested
|
43
|
+
end
|
44
|
+
|
45
|
+
def nested
|
46
|
+
full = false
|
47
|
+
nested_boxes = []
|
48
|
+
boxes.cycle do |box|
|
49
|
+
break if full
|
50
|
+
placement_point = find_placement_point(box, nested_boxes)
|
51
|
+
if placement_point
|
52
|
+
nested_boxes << box.move_to(placement_point)
|
53
|
+
else
|
54
|
+
full = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
nested_boxes.map { |box| box.translate(origin_offset) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def width(boxes)
|
61
|
+
edge_length(boxes, :left_edge, :right_edge)
|
62
|
+
end
|
63
|
+
|
64
|
+
def height(boxes)
|
65
|
+
edge_length(boxes, :bottom_edge, :top_edge)
|
66
|
+
end
|
67
|
+
|
68
|
+
def edge_length(boxes, min_method, max_method)
|
69
|
+
min = boxes.map(&min_method).min
|
70
|
+
max = boxes.map(&max_method).max
|
71
|
+
(max - min) + (2 * outer_gap)
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_placement_point(box, placed_boxes)
|
75
|
+
return Point::ZERO if placeable_at_location?(box, Point::ZERO, placed_boxes)
|
76
|
+
placement_after_a_box(box, placed_boxes) || placement_above_a_box(box, placed_boxes)
|
77
|
+
end
|
78
|
+
|
79
|
+
def placement_after_a_box(box, placed_boxes)
|
80
|
+
placement_around_a_box(box, placed_boxes, :lower_right)
|
81
|
+
end
|
82
|
+
|
83
|
+
def placement_above_a_box(box, placed_boxes)
|
84
|
+
placement_around_a_box(box, placed_boxes, :upper_left)
|
85
|
+
end
|
86
|
+
|
87
|
+
def placement_around_a_box(box, placed_boxes, reference_point_method)
|
88
|
+
reference_box = placed_boxes.find { |placed_box|
|
89
|
+
point = offset(box, placed_box, reference_point_method)
|
90
|
+
placeable_at_location?(box, point, placed_boxes)
|
91
|
+
}
|
92
|
+
if reference_box
|
93
|
+
return offset(box, reference_box, reference_point_method)
|
94
|
+
end
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def offset(box, reference_box, reference_point_method)
|
99
|
+
gap = [box.min_gap, reference_box.min_gap].max
|
100
|
+
offset_point(gap, reference_box, reference_point_method)
|
101
|
+
end
|
102
|
+
|
103
|
+
def offset_point(gap, box, reference_point_method)
|
104
|
+
offset = offset_translation(gap, reference_point_method)
|
105
|
+
box.send(reference_point_method).translate(offset)
|
106
|
+
end
|
107
|
+
|
108
|
+
def offset_translation(gap, reference_point_method)
|
109
|
+
case reference_point_method
|
110
|
+
when :lower_right
|
111
|
+
Vector.new(gap, 0)
|
112
|
+
when :upper_left
|
113
|
+
Vector.new(0, gap)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def placeable_at_location?(box, placement_point, placed_boxes)
|
118
|
+
box_to_place = box.move_to(placement_point)
|
119
|
+
no_overlaps?(box_to_place, placed_boxes) && fits?(box_to_place)
|
120
|
+
end
|
121
|
+
|
122
|
+
def no_overlaps?(box, placed_boxes)
|
123
|
+
placed_boxes.none? { |placed_box| box.overlaps?(placed_box) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def fits?(box)
|
127
|
+
x = 0..usable_width
|
128
|
+
y = 0..usable_height
|
129
|
+
box.corners.all? { |point| x.include?(point.x) && y.include?(point.y) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def usable_width
|
133
|
+
@usable_width ||= max_width - (2 * outer_gap)
|
134
|
+
end
|
135
|
+
|
136
|
+
def usable_height
|
137
|
+
@usable_height ||= max_height - (2 * outer_gap)
|
138
|
+
end
|
139
|
+
|
140
|
+
def origin_offset
|
141
|
+
Vector.new(outer_gap, outer_gap)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|