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