draught 0.1.0

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