sevgi 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![test status](https://github.com/roktas/sevgi/workflows/Test/badge.svg)](https://github.com/roktas/sevgi/actions?query=workflow%3ATest)
|
2
|
+
[![code quality](https://codebeat.co/badges/f2de3389-dd66-4d7a-b7fb-452330d62176)](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
|