sevgi 0.0.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/CHANGELOG.md +9 -0
- data/LICENSE +674 -0
- data/README.md +15 -0
- data/bin/sevgi +136 -0
- data/lib/sevgi/errors.rb +11 -0
- data/lib/sevgi/external.rb +8 -0
- data/lib/sevgi/function.rb +35 -0
- data/lib/sevgi/geometry/elements/rect.rb +133 -0
- data/lib/sevgi/geometry/elements/segment.rb +102 -0
- data/lib/sevgi/geometry/elements.rb +41 -0
- data/lib/sevgi/geometry/equation/line/diagonal.rb +90 -0
- data/lib/sevgi/geometry/equation/line/horizontal.rb +15 -0
- data/lib/sevgi/geometry/equation/line/vertical.rb +52 -0
- data/lib/sevgi/geometry/equation/line.rb +38 -0
- data/lib/sevgi/geometry/equation.rb +3 -0
- data/lib/sevgi/geometry/errors.rb +5 -0
- data/lib/sevgi/geometry/external.rb +15 -0
- data/lib/sevgi/geometry/operation/align.rb +40 -0
- data/lib/sevgi/geometry/operation/sweep.rb +52 -0
- data/lib/sevgi/geometry/operation.rb +28 -0
- data/lib/sevgi/geometry/point.rb +62 -0
- data/lib/sevgi/geometry.rb +10 -0
- data/lib/sevgi/graphics/attribute.rb +125 -0
- data/lib/sevgi/graphics/canvas.rb +51 -0
- data/lib/sevgi/graphics/content.rb +72 -0
- data/lib/sevgi/graphics/document/base.rb +51 -0
- data/lib/sevgi/graphics/document/default.rb +19 -0
- data/lib/sevgi/graphics/document/html.rb +11 -0
- data/lib/sevgi/graphics/document/inkscape.rb +17 -0
- data/lib/sevgi/graphics/document/minimal.rb +11 -0
- data/lib/sevgi/graphics/document.rb +53 -0
- data/lib/sevgi/graphics/element.rb +85 -0
- data/lib/sevgi/graphics/external.rb +15 -0
- data/lib/sevgi/graphics/mixtures/core.rb +81 -0
- data/lib/sevgi/graphics/mixtures/duplicate.rb +36 -0
- data/lib/sevgi/graphics/mixtures/hatch.rb +21 -0
- data/lib/sevgi/graphics/mixtures/identify.rb +93 -0
- data/lib/sevgi/graphics/mixtures/inkscape.rb +15 -0
- data/lib/sevgi/graphics/mixtures/lint.rb +33 -0
- data/lib/sevgi/graphics/mixtures/render.rb +149 -0
- data/lib/sevgi/graphics/mixtures/replicate.rb +125 -0
- data/lib/sevgi/graphics/mixtures/save.rb +23 -0
- data/lib/sevgi/graphics/mixtures/transform.rb +78 -0
- data/lib/sevgi/graphics/mixtures/underscore.rb +21 -0
- data/lib/sevgi/graphics/mixtures/validate.rb +27 -0
- data/lib/sevgi/graphics/mixtures/wrappers.rb +63 -0
- data/lib/sevgi/graphics/mixtures.rb +16 -0
- data/lib/sevgi/graphics.rb +10 -0
- data/lib/sevgi/internal/constant.rb +5 -0
- data/lib/sevgi/internal/dim.rb +13 -0
- data/lib/sevgi/internal/function/file.rb +30 -0
- data/lib/sevgi/internal/function/float.rb +35 -0
- data/lib/sevgi/internal/function/formula.rb +71 -0
- data/lib/sevgi/internal/function/math.rb +43 -0
- data/lib/sevgi/internal/function/string.rb +13 -0
- data/lib/sevgi/internal/function.rb +7 -0
- data/lib/sevgi/internal/list.rb +27 -0
- data/lib/sevgi/internal/locate.rb +47 -0
- data/lib/sevgi/internal/margin.rb +23 -0
- data/lib/sevgi/internal/minitest/script.rb +50 -0
- data/lib/sevgi/internal/minitest/shell.rb +71 -0
- data/lib/sevgi/internal/minitest/suite.rb +35 -0
- data/lib/sevgi/internal/minitest.rb +5 -0
- data/lib/sevgi/internal/paper.rb +26 -0
- data/lib/sevgi/internal.rb +10 -0
- data/lib/sevgi/standard/conform.rb +55 -0
- data/lib/sevgi/standard/data/attribute.rb +496 -0
- data/lib/sevgi/standard/data/element.rb +269 -0
- data/lib/sevgi/standard/data/specification.rb +1841 -0
- data/lib/sevgi/standard/data.rb +79 -0
- data/lib/sevgi/standard/errors.rb +28 -0
- data/lib/sevgi/standard/model.rb +82 -0
- data/lib/sevgi/standard.rb +26 -0
- data/lib/sevgi/utensils/external.rb +22 -0
- data/lib/sevgi/utensils/grid.rb +49 -0
- data/lib/sevgi/utensils/ruler.rb +47 -0
- data/lib/sevgi/utensils/tsquare.rb +28 -0
- data/lib/sevgi/utensils.rb +7 -0
- data/lib/sevgi/version.rb +5 -0
- data/lib/sevgi.rb +10 -0
- metadata +127 -0
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
[](https://github.com/roktas/sevgi/actions?query=workflow%3ATest)
|
2
|
+
[](https://codebeat.co/projects/github-com-roktas-sevgi-dev)
|
3
|
+
|
4
|
+
# Sevgi
|
5
|
+
|
6
|
+
<p align="center"><img src="srv/static/assets/light/logo.svg#gh-light-mode-only"/></p>
|
7
|
+
<p align="center"><img src="srv/static/assets/dark/logo.svg#gh-dark-mode-only"/></p>
|
8
|
+
|
9
|
+
<p align="center"><img src="srv/static/assets/light/pacman-animation.png"/></p>
|
10
|
+
|
11
|
+
Inspired by [Victor](https://github.com/DannyBen/victor), which might be a better choice for those seeking something
|
12
|
+
simpler. Please note that a fair amount of the examples used for demonstration purposes come from this project (thanks
|
13
|
+
to the author).
|
14
|
+
|
15
|
+
### Roadmap
|
data/bin/sevgi
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sevgi/external"
|
5
|
+
|
6
|
+
module Sevgi
|
7
|
+
module CLI
|
8
|
+
extend self
|
9
|
+
|
10
|
+
CLIError = Class.new(Sevgi::Error)
|
11
|
+
|
12
|
+
PROGNAME = "sevgi"
|
13
|
+
ENVVOMIT = "SEVGI_VOMIT"
|
14
|
+
|
15
|
+
Options = Struct.new(:library, :vomit, :help, :version) do
|
16
|
+
class << self
|
17
|
+
def parse(argv)
|
18
|
+
new.tap do |options|
|
19
|
+
argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
|
20
|
+
|
21
|
+
next unless options.library
|
22
|
+
|
23
|
+
CLIError.("No library found: #{options.library}") unless ::File.exist?(options.library)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def option(argv, options)
|
30
|
+
case (arg = argv.shift)
|
31
|
+
when "-l", "--library" then options.library = argv.shift
|
32
|
+
when "-x", "--exception" then options.vomit = true
|
33
|
+
when "-h", "--help" then options.help = true
|
34
|
+
when "-v", "--version" then options.version = true
|
35
|
+
else CLIError.("Not a valid option: #{arg}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private_constant :Options
|
42
|
+
|
43
|
+
def call(argv)
|
44
|
+
return puts(help) if (options = Options.parse(argv)).help
|
45
|
+
return puts(Sevgi::VERSION) if options.version
|
46
|
+
|
47
|
+
Signal.trap("INT") { Kernel.abort("") }
|
48
|
+
Kernel.load(file = options.library) if options.library
|
49
|
+
Kernel.load(file = script(argv))
|
50
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
51
|
+
die(e, file, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def description(e, file)
|
57
|
+
case e
|
58
|
+
when ArgumentError then "Programming error"
|
59
|
+
when GeometryError then "Geometry error"
|
60
|
+
when ValidationError then "Validation error"
|
61
|
+
else "Error"
|
62
|
+
end => error
|
63
|
+
|
64
|
+
<<~DESCRIPTION
|
65
|
+
#{error} #{scene(e.backtrace, file)}
|
66
|
+
#{e.message}
|
67
|
+
DESCRIPTION
|
68
|
+
end
|
69
|
+
|
70
|
+
def die(e, file, options)
|
71
|
+
abort(e.message) if e.is_a?(CLIError)
|
72
|
+
|
73
|
+
raise e if options.vomit || ENV[ENVVOMIT]
|
74
|
+
|
75
|
+
warn(description(e, file))
|
76
|
+
warn("")
|
77
|
+
abort(postmortem(file))
|
78
|
+
end
|
79
|
+
|
80
|
+
def help
|
81
|
+
<<~HELP
|
82
|
+
Usage: #{PROGNAME} [options...] <SCRIPT> [ARGS...]
|
83
|
+
|
84
|
+
See documentation for detailed help.
|
85
|
+
|
86
|
+
Options:
|
87
|
+
|
88
|
+
-l, --library FILE Preload library FILE
|
89
|
+
-x, --exception Raise exceptions instead of abort
|
90
|
+
-h, --help Show this help
|
91
|
+
-v, --version Display version
|
92
|
+
HELP
|
93
|
+
end
|
94
|
+
|
95
|
+
def postmortem(file)
|
96
|
+
<<~POSTMORTEM
|
97
|
+
For more details, run the script again:
|
98
|
+
|
99
|
+
- By using the -x switch
|
100
|
+
|
101
|
+
#{PROGNAME} -x #{file}
|
102
|
+
|
103
|
+
- By setting the #{ENVVOMIT} environment variable
|
104
|
+
|
105
|
+
#{ENVVOMIT}=t #{file}
|
106
|
+
|
107
|
+
If you think this is a bug, you can report it by creating an issue.
|
108
|
+
POSTMORTEM
|
109
|
+
end
|
110
|
+
|
111
|
+
def scene(backtrace, file)
|
112
|
+
default = "in #{file}"
|
113
|
+
return default unless backtrace
|
114
|
+
|
115
|
+
path = ::File.expand_path(file)
|
116
|
+
_, line = backtrace.map { _1.split(":")[..1] }.find do |spot|
|
117
|
+
::File.expand_path(spot.first) == path
|
118
|
+
end
|
119
|
+
|
120
|
+
line ? "in '#{file}', around line #{line}" : default
|
121
|
+
end
|
122
|
+
|
123
|
+
def script(argv)
|
124
|
+
CLIError.("No script file given.") unless (file = argv.shift)
|
125
|
+
CLIError.("No such script file: #{file}") unless ::File.exist?(file)
|
126
|
+
|
127
|
+
file
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def main
|
133
|
+
Sevgi::CLI.(ARGV)
|
134
|
+
end
|
135
|
+
|
136
|
+
main
|
data/lib/sevgi/errors.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Function
|
5
|
+
module External
|
6
|
+
EXTENSIONS = ["sevgi", "rb"].freeze
|
7
|
+
DIRECTORIES = ["lib", "library"].freeze
|
8
|
+
|
9
|
+
def Lib(name)
|
10
|
+
start = ::File.dirname(caller_locations(1..1).first.path)
|
11
|
+
|
12
|
+
paths = F.variations(name, DIRECTORIES, EXTENSIONS)
|
13
|
+
|
14
|
+
raise Error, "No library found matching: #{name}" unless (location = Locate.(paths, start))
|
15
|
+
|
16
|
+
Kernel.load(location.file)
|
17
|
+
end
|
18
|
+
|
19
|
+
module Function
|
20
|
+
extend Sevgi::Function::Float
|
21
|
+
extend Sevgi::Function::Math
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def included(base)
|
26
|
+
super
|
27
|
+
|
28
|
+
base.const_set(:F, External::Function)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
F = Function
|
35
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# codebeat:disable[TOO_MANY_IVARS]
|
4
|
+
|
5
|
+
require "forwardable"
|
6
|
+
|
7
|
+
module Sevgi
|
8
|
+
module Geometry
|
9
|
+
class Rect < Element
|
10
|
+
# Argument order is in clockwise rotation from position point
|
11
|
+
Corner = Data.define(:top_left, :top_right, :bottom_right, :bottom_left) do
|
12
|
+
class << self
|
13
|
+
def of(rect)
|
14
|
+
position = rect.position
|
15
|
+
|
16
|
+
new(
|
17
|
+
top_left: position,
|
18
|
+
top_right: position.translate(dx: rect.width),
|
19
|
+
bottom_right: position.translate(dx: rect.width, dy: rect.height),
|
20
|
+
bottom_left: position.translate(dy: rect.height),
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Argument order is in clockwise rotation from top segment (as in CSS)
|
27
|
+
Side = Data.define(:top, :right, :bottom, :left) do
|
28
|
+
class << self
|
29
|
+
def of(rect)
|
30
|
+
corner = rect.corner
|
31
|
+
|
32
|
+
new(
|
33
|
+
top: Segment[corner.top_left, corner.top_right],
|
34
|
+
right: Segment[corner.top_right, corner.bottom_right],
|
35
|
+
bottom: Segment[corner.bottom_left, corner.bottom_right],
|
36
|
+
left: Segment[corner.top_left, corner.bottom_left],
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
extend Forwardable
|
43
|
+
|
44
|
+
def_delegators :@side, *Side.members
|
45
|
+
def_delegators :@corner, *Corner.members
|
46
|
+
|
47
|
+
attr_reader :position, :width, :height
|
48
|
+
attr_reader :corner, :corners, :side, :sides
|
49
|
+
|
50
|
+
def initialize(position: nil, width:, height:)
|
51
|
+
super()
|
52
|
+
|
53
|
+
@position = position || Point.origin
|
54
|
+
@width = width.to_f
|
55
|
+
@height = height.to_f
|
56
|
+
|
57
|
+
compute
|
58
|
+
end
|
59
|
+
|
60
|
+
def bbox
|
61
|
+
BBox[*@corner.deconstruct]
|
62
|
+
end
|
63
|
+
|
64
|
+
def diagonal
|
65
|
+
Segment.forward(top_left, bottom_right)
|
66
|
+
end
|
67
|
+
|
68
|
+
def draw(container, **)
|
69
|
+
container.rect(x: position.x, y: position.y, width: width, height: height, **)
|
70
|
+
end
|
71
|
+
|
72
|
+
def inside?(point)
|
73
|
+
onto?(point) || (left.right?(point) && right.left?(point) && top.left?(point) && bottom.right?(point))
|
74
|
+
end
|
75
|
+
|
76
|
+
# codebeat:disable[ABC]
|
77
|
+
def intersection(line, precision: nil)
|
78
|
+
points = sides.map { _1.intersection(line).approx(precision) }.uniq.select { onto?(_1) }
|
79
|
+
return if points.empty?
|
80
|
+
|
81
|
+
GeometryError.("Unexpected number of intersection points: #{points.size}") if points.size > 2
|
82
|
+
|
83
|
+
Segment.forward(*points)
|
84
|
+
end
|
85
|
+
# codebeat:enable[ABC]
|
86
|
+
|
87
|
+
def onto?(point)
|
88
|
+
left.onto?(point) || right.onto?(point) || top.onto?(point) || bottom.onto?(point)
|
89
|
+
end
|
90
|
+
|
91
|
+
def orient(new_orientation)
|
92
|
+
return self if orientation == new_orientation
|
93
|
+
|
94
|
+
with(width: height, height: width)
|
95
|
+
end
|
96
|
+
|
97
|
+
def orientation
|
98
|
+
F.gt?(width, height) ? :landscape : :portrait
|
99
|
+
end
|
100
|
+
|
101
|
+
def outside?(point)
|
102
|
+
!inside?(point)
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_s
|
106
|
+
strings = []
|
107
|
+
|
108
|
+
strings << "R[#{F.approx(width)}x#{F.approx(height)}]"
|
109
|
+
strings << position.to_s if Point.eq?(position, Point.origin)
|
110
|
+
|
111
|
+
strings.join("@")
|
112
|
+
end
|
113
|
+
|
114
|
+
def translate(dx: nil, dy: nil)
|
115
|
+
self.class.new(position: position.translate(dx:, dy:), width: width, height: height)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def compute
|
121
|
+
@corner = Corner.of(self)
|
122
|
+
@corners = Corner.members.map { public_send(_1) }
|
123
|
+
|
124
|
+
@side = Side.of(self)
|
125
|
+
@sides = Side.members.map { public_send(_1) }
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
def [](width, height) = new(width:, height:)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Sevgi
|
6
|
+
module Geometry
|
7
|
+
class Segment < Element
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :line, :left?, :right?, :intersection
|
11
|
+
|
12
|
+
attr_reader :position, :ending
|
13
|
+
|
14
|
+
def initialize(position: Point.origin, ending:, &block)
|
15
|
+
super()
|
16
|
+
|
17
|
+
@position = position
|
18
|
+
@ending = ending
|
19
|
+
|
20
|
+
instance_exec(&block) if block # optimization backdoor for constructors
|
21
|
+
end
|
22
|
+
|
23
|
+
def bbox
|
24
|
+
BBox[position, ending]
|
25
|
+
end
|
26
|
+
|
27
|
+
def direction
|
28
|
+
@direction ||= F.angler(dx, dy)
|
29
|
+
end
|
30
|
+
|
31
|
+
def dx
|
32
|
+
@dx ||= F.dxp(position, ending)
|
33
|
+
end
|
34
|
+
|
35
|
+
def dy
|
36
|
+
@dy ||= F.dyp(position, ending)
|
37
|
+
end
|
38
|
+
|
39
|
+
def draw(container, **)
|
40
|
+
container.segment(x1: position.x, y1: position.y, x2: ending.x, y2: ending.y, **)
|
41
|
+
end
|
42
|
+
|
43
|
+
def horizontal?(precision = nil)
|
44
|
+
F.horizontal?(direction, precision:)
|
45
|
+
end
|
46
|
+
|
47
|
+
def infinite?
|
48
|
+
[position, ending].any(&:infinite?)
|
49
|
+
end
|
50
|
+
|
51
|
+
def length
|
52
|
+
@length ||= F.distance(position, ending)
|
53
|
+
end
|
54
|
+
|
55
|
+
def line
|
56
|
+
@line ||= Equation::Line.from_segment(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def onto?(point)
|
60
|
+
point.unordered_between?(ending, position) && line.onto?(point)
|
61
|
+
end
|
62
|
+
|
63
|
+
def rect
|
64
|
+
@rect ||= Rect[dx.abs, dy.abs]
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
"S[#{position} -> #{ending}]"
|
69
|
+
end
|
70
|
+
|
71
|
+
def translate(dx: nil, dy: nil)
|
72
|
+
self.class.new(position: position.translate(dx:, dy:), ending: ending.translate(dx:, dy:))
|
73
|
+
end
|
74
|
+
|
75
|
+
def vertical?(precision = nil)
|
76
|
+
F.vertical?(direction, precision:)
|
77
|
+
end
|
78
|
+
|
79
|
+
class << self
|
80
|
+
def [](position, ending)
|
81
|
+
new(position:, ending:)
|
82
|
+
end
|
83
|
+
|
84
|
+
def forward(p, q = nil)
|
85
|
+
new(position: (points = [p, q || p]).sort!.shift, ending: points.shift)
|
86
|
+
end
|
87
|
+
|
88
|
+
def directed(position: Point.origin, length:, direction:)
|
89
|
+
dx = F.dxa(length, direction)
|
90
|
+
dy = F.dya(length, direction)
|
91
|
+
|
92
|
+
new(position: position, ending: position.translate(dx:, dy:)) do # avoid recalculations
|
93
|
+
@direction = direction.to_f
|
94
|
+
@length = length.to_f
|
95
|
+
@dx = dx
|
96
|
+
@dy = dy
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
class Element
|
6
|
+
def bbox = raise(NotImplementedError, "Instance method required: bbox")
|
7
|
+
|
8
|
+
def ignorable?(precision = nil) = bbox.zero?(precision)
|
9
|
+
end
|
10
|
+
|
11
|
+
class BBox
|
12
|
+
BBoxError = Class.new(GeometryError)
|
13
|
+
|
14
|
+
attr_reader :ne, :sw
|
15
|
+
|
16
|
+
def initialize(ne:, sw:) = (@ne, @sw = ne, sw)
|
17
|
+
|
18
|
+
def height = @height ||= F.height(ne, sw)
|
19
|
+
|
20
|
+
def to_s = "[#{width}x#{height}]@[#{ne} ↘ #{sw}]"
|
21
|
+
|
22
|
+
def width = @width ||= F.width(ne, sw)
|
23
|
+
|
24
|
+
def zero?(precision = nil) = Point.eq?(ne, sw, precision:)
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate!
|
29
|
+
BBoxError.("North-East is not to the left of South-West: #{self}") if ne > sw
|
30
|
+
BBoxError.("North-East is not below South-West: #{self}") unless ne.below?(sw)
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def [](*points) = new(ne: points.min, sw: points.max)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
require_relative "elements/segment"
|
41
|
+
require_relative "elements/rect"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
module Equation
|
6
|
+
module Line
|
7
|
+
class Diagonal
|
8
|
+
attr_reader :slope, :intercept, :direction
|
9
|
+
|
10
|
+
def initialize(slope:, intercept:)
|
11
|
+
@slope = slope.to_f
|
12
|
+
@intercept = intercept.to_f
|
13
|
+
@direction = F.angles(@slope)
|
14
|
+
end
|
15
|
+
|
16
|
+
def approx(precision = nil)
|
17
|
+
self.class.new(slope: F.approx(slope, precision), intercept: F.approx(intercept, precision))
|
18
|
+
end
|
19
|
+
|
20
|
+
def eql?(other)
|
21
|
+
self.class == other.class && [slope, intercept] == [other.slope, other.intercept]
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :==, :eql?
|
25
|
+
|
26
|
+
def hash
|
27
|
+
[self.class, slope, intercept].hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def intersection(other)
|
31
|
+
case other
|
32
|
+
when Diagonal
|
33
|
+
x = (other.intercept - intercept) / (slope - other.slope)
|
34
|
+
y = y(x)
|
35
|
+
when Horizontal
|
36
|
+
y = other.y
|
37
|
+
x = x(y)
|
38
|
+
when Vertical
|
39
|
+
x = other.x
|
40
|
+
y = y(x)
|
41
|
+
end
|
42
|
+
|
43
|
+
Point[x, y]
|
44
|
+
end
|
45
|
+
|
46
|
+
def left?(point)
|
47
|
+
F.gt?(point.y, y(point.x))
|
48
|
+
end
|
49
|
+
|
50
|
+
def onto?(point)
|
51
|
+
F.eq?(point.y, y(point.x))
|
52
|
+
end
|
53
|
+
|
54
|
+
def right?(point)
|
55
|
+
F.lt?(point.y, y(point.x))
|
56
|
+
end
|
57
|
+
|
58
|
+
def shift(distance = nil, dx: nil, dy: nil)
|
59
|
+
dx ||= 0.0
|
60
|
+
dy ||= 0.0
|
61
|
+
|
62
|
+
if distance
|
63
|
+
dx += F.rx(distance, direction)
|
64
|
+
dy -= F.ry(distance, direction)
|
65
|
+
end
|
66
|
+
|
67
|
+
Diagonal.new(slope:, intercept: intercept - slope * dx + dy)
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
strings = []
|
72
|
+
|
73
|
+
strings << "#{F.approx(slope)} * x" if F.nonzero?(slope)
|
74
|
+
strings << F.approx(intercept).abs.to_s if F.nonzero?(intercept)
|
75
|
+
|
76
|
+
"L<y = #{strings.join(intercept.positive? ? " + " : " - ")}>"
|
77
|
+
end
|
78
|
+
|
79
|
+
def x(y)
|
80
|
+
(y - intercept) / slope
|
81
|
+
end
|
82
|
+
|
83
|
+
def y(x)
|
84
|
+
(slope * x) + intercept
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sevgi
|
4
|
+
module Geometry
|
5
|
+
module Equation
|
6
|
+
module Line
|
7
|
+
class Vertical
|
8
|
+
def initialize(c)
|
9
|
+
@x = c
|
10
|
+
end
|
11
|
+
|
12
|
+
def intersection(other)
|
13
|
+
case other
|
14
|
+
when Diagonal, Horizontal then x, y = self.x, other.y(self.x)
|
15
|
+
when Vertical then x, y = ::Float::INFINITY, ::Float::INFINITY
|
16
|
+
end
|
17
|
+
|
18
|
+
Point[x, y]
|
19
|
+
end
|
20
|
+
|
21
|
+
def left?(point)
|
22
|
+
F.lt?(point.x, x(point.y))
|
23
|
+
end
|
24
|
+
|
25
|
+
def onto?(point)
|
26
|
+
F.eq?(point.x, x(point.y))
|
27
|
+
end
|
28
|
+
|
29
|
+
def right?(point)
|
30
|
+
F.gt?(point.x, x(point.y))
|
31
|
+
end
|
32
|
+
|
33
|
+
def shift(distance = nil, dx: nil, dy: nil)
|
34
|
+
self.class.new(x + (distance || 0.0) + (dx || 0.0))
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"L<x = #{F.approx(x)}>"
|
39
|
+
end
|
40
|
+
|
41
|
+
def x(_ = nil)
|
42
|
+
@x
|
43
|
+
end
|
44
|
+
|
45
|
+
def y(_ = nil)
|
46
|
+
::Float::INFINITY
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "line/diagonal"
|
4
|
+
require_relative "line/horizontal"
|
5
|
+
require_relative "line/vertical"
|
6
|
+
|
7
|
+
module Sevgi
|
8
|
+
module Geometry
|
9
|
+
module Equation
|
10
|
+
module Line
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def diagonal(slope:, intercept:)
|
14
|
+
Diagonal.new(slope:, intercept:)
|
15
|
+
end
|
16
|
+
|
17
|
+
def from_direction(point:, direction:)
|
18
|
+
return horizontal(point.y) if F.horizontal?(direction)
|
19
|
+
return vertical(point.x) if F.vertical?(direction)
|
20
|
+
|
21
|
+
diagonal(slope: (slope = F.slopea(direction)), intercept: F.intercept(point, direction, slope))
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_segment(segment)
|
25
|
+
from_direction(point: segment.position, direction: segment.direction)
|
26
|
+
end
|
27
|
+
|
28
|
+
def horizontal(const)
|
29
|
+
Horizontal.new(const)
|
30
|
+
end
|
31
|
+
|
32
|
+
def vertical(const)
|
33
|
+
Vertical.new(const)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|