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,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
|