triangular 0.0.2 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/build.yml +30 -0
  3. data/.gitignore +3 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +31 -0
  6. data/Gemfile +3 -1
  7. data/Gemfile.lock +51 -12
  8. data/MIT-LICENSE +1 -1
  9. data/README.md +98 -0
  10. data/examples/slice_example.rb +6 -4
  11. data/lib/triangular/facet.rb +32 -42
  12. data/lib/triangular/line.rb +42 -16
  13. data/lib/triangular/point.rb +15 -15
  14. data/lib/triangular/polyline.rb +13 -12
  15. data/lib/triangular/ray.rb +41 -0
  16. data/lib/triangular/solid.rb +34 -36
  17. data/lib/triangular/units.rb +18 -14
  18. data/lib/triangular/vector.rb +8 -1
  19. data/lib/triangular/version.rb +3 -1
  20. data/lib/triangular/vertex.rb +17 -16
  21. data/lib/triangular.rb +4 -1
  22. data/spec/benchmark/benchmark_spec.rb +23 -0
  23. data/spec/profile/profile_spec.rb +20 -0
  24. data/spec/spec_helper.rb +17 -3
  25. data/spec/triangular/facet_spec.rb +235 -0
  26. data/spec/triangular/line_spec.rb +285 -0
  27. data/spec/triangular/point_spec.rb +108 -0
  28. data/spec/triangular/polyline_spec.rb +22 -0
  29. data/spec/triangular/ray_spec.rb +63 -0
  30. data/spec/{solid_spec.rb → triangular/solid_spec.rb} +71 -70
  31. data/spec/triangular/triangular_spec.rb +24 -0
  32. data/spec/triangular/units_spec.rb +77 -0
  33. data/spec/triangular/vector_spec.rb +23 -0
  34. data/spec/triangular/vertex_spec.rb +46 -0
  35. data/triangular.gemspec +22 -18
  36. metadata +114 -65
  37. data/README.rdoc +0 -64
  38. data/Rakefile +0 -11
  39. data/spec/facet_spec.rb +0 -233
  40. data/spec/line_spec.rb +0 -108
  41. data/spec/point_spec.rb +0 -88
  42. data/spec/polyline_spec.rb +0 -20
  43. data/spec/triangular_spec.rb +0 -22
  44. data/spec/units_spec.rb +0 -75
  45. data/spec/vertex_spec.rb +0 -44
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8ad12df593fb18caf82b87ed4f58c789750f1aa0d17b902a3720032a6e3fb344
4
+ data.tar.gz: cadec3a31d3af63f89af1e51f398d4af3a4ca0bb78a045ba2426a8d6d13846e4
5
+ SHA512:
6
+ metadata.gz: 9980d58a82095bc137e524c1778cb519d22578e28e785b07e6c1207e6d336aa5cf6eddcfba2fe57d3a788555614d5634e5ab29c699ee7a5851451b0d62eb7f19
7
+ data.tar.gz: fc3b770e3a01cc5fb4f4a4c055896a7c9fb6f4129741bc1c34bd7bc2d75fdca200bab16f091af2ed751d52a285c6590695f907c2fa277f8f113ba53257e8d4fe
@@ -0,0 +1,30 @@
1
+ name: build
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - uses: actions/checkout@v1
11
+
12
+ - name: Install Ruby (2.6)
13
+ uses: actions/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6.x
16
+
17
+ - name: Setup Code Climate test-reporter
18
+ run: |
19
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
20
+ chmod +x ./cc-test-reporter
21
+ ./cc-test-reporter before-build
22
+ - name: Build and test with RSpec
23
+ run: |
24
+ gem install bundler
25
+ bundle install --jobs 4 --retry 3
26
+ bundle exec rspec
27
+ - name: Publish code coverage
28
+ run: |
29
+ export GIT_BRANCH="${GITHUB_REF/refs\/heads\//}"
30
+ ./cc-test-reporter after-build --id ${{secrets.CC_TEST_REPORTER_ID}} --coverage-input-type simplecov
data/.gitignore CHANGED
@@ -1,3 +1,6 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
+ coverage
5
+ *.DS_Store
6
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format progress
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ Metrics/BlockLength:
2
+ IgnoredMethods: ['describe', 'context']
3
+
4
+ Style/SymbolProc:
5
+ IgnoredMethods: ['configure']
6
+
7
+ AllCops:
8
+ SuggestExtensions: false
9
+ NewCops: enable
10
+
11
+ Style/Documentation:
12
+ Enabled: false
13
+
14
+ Naming/MethodParameterName:
15
+ AllowedNames: ['x', 'y', 'z']
16
+
17
+ Lint/BinaryOperatorWithIdenticalOperands:
18
+ Enabled: false
19
+
20
+ Layout/EmptyLineAfterGuardClause:
21
+ Enabled: false
22
+
23
+ Metrics/AbcSize:
24
+ Max: 20
25
+ CountRepeatedAttributes: false
26
+
27
+ Metrics/MethodLength:
28
+ Max: 15
29
+
30
+ Metrics/CyclomaticComplexity:
31
+ Max: 10
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
- source :gemcutter
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in triangular.gemspec
4
6
  gemspec
data/Gemfile.lock CHANGED
@@ -1,25 +1,64 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- triangular (0.0.1)
4
+ triangular (0.1.0)
5
5
 
6
6
  GEM
7
- remote: http://rubygems.org/
7
+ remote: https://rubygems.org/
8
8
  specs:
9
- diff-lcs (1.1.2)
10
- rspec (2.5.0)
11
- rspec-core (~> 2.5.0)
12
- rspec-expectations (~> 2.5.0)
13
- rspec-mocks (~> 2.5.0)
14
- rspec-core (2.5.1)
15
- rspec-expectations (2.5.0)
16
- diff-lcs (~> 1.1.2)
17
- rspec-mocks (2.5.0)
9
+ ast (2.4.2)
10
+ diff-lcs (1.3)
11
+ docile (1.3.5)
12
+ json (2.6.1)
13
+ parallel (1.20.1)
14
+ parser (3.1.0.0)
15
+ ast (~> 2.4.1)
16
+ rainbow (3.0.0)
17
+ regexp_parser (2.2.0)
18
+ rexml (3.2.5)
19
+ rspec (3.7.0)
20
+ rspec-core (~> 3.7.0)
21
+ rspec-expectations (~> 3.7.0)
22
+ rspec-mocks (~> 3.7.0)
23
+ rspec-core (3.7.0)
24
+ rspec-support (~> 3.7.0)
25
+ rspec-expectations (3.7.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.7.0)
28
+ rspec-mocks (3.7.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.7.0)
31
+ rspec-support (3.7.0)
32
+ rubocop (1.12.1)
33
+ parallel (~> 1.10)
34
+ parser (>= 3.0.0.0)
35
+ rainbow (>= 2.2.2, < 4.0)
36
+ regexp_parser (>= 1.8, < 3.0)
37
+ rexml
38
+ rubocop-ast (>= 1.2.0, < 2.0)
39
+ ruby-progressbar (~> 1.7)
40
+ unicode-display_width (>= 1.4.0, < 3.0)
41
+ rubocop-ast (1.4.1)
42
+ parser (>= 2.7.1.5)
43
+ ruby-prof (1.4.2)
44
+ ruby-progressbar (1.11.0)
45
+ simplecov (0.17.1)
46
+ docile (~> 1.1)
47
+ json (>= 1.8, < 3)
48
+ simplecov-html (~> 0.10.0)
49
+ simplecov-html (0.10.2)
50
+ unicode-display_width (2.1.0)
18
51
 
19
52
  PLATFORMS
20
53
  ruby
21
54
 
22
55
  DEPENDENCIES
23
56
  bundler (>= 1.0.0)
24
- rspec (~> 2)
57
+ rspec (~> 3)
58
+ rubocop
59
+ ruby-prof
60
+ simplecov (= 0.17.1)
25
61
  triangular!
62
+
63
+ BUNDLED WITH
64
+ 1.17.3
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Aaron Gough (http://thingsaaronmade.com/)
1
+ Copyright (c) 2022 Aaron Gough (http://thingsaaronmade.com/)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
2
+ [![Actions Status](https://github.com/aarongough/triangular/actions/workflows/build.yml/badge.svg)](https://github.com/aarongough/triangular/actions/workflows/build.yml)
3
+ [![Maintainability](https://api.codeclimate.com/v1/badges/e64ecb8e1c703a010077/maintainability)](https://codeclimate.com/github/aarongough/triangular/maintainability)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/e64ecb8e1c703a010077/test_coverage)](https://codeclimate.com/github/aarongough/triangular/test_coverage)
5
+ [![Gem Version](https://badge.fury.io/rb/triangular.svg)](https://badge.fury.io/rb/triangular)
6
+
7
+ # Triangular:
8
+
9
+ Triangular is an easy-to-use Ruby library for reading, writing and manipulating Stereolithography (STL) files.
10
+
11
+ The main purpose of Triangular is to enable its users to quickly create new software for Rapid Prototyping and Personal Manufacturing applications. Triangular has many of the core functions needed in order to generate toolpaths for 3D printers and CNC Mills/Routers.
12
+
13
+ ### Usage:
14
+
15
+
16
+ ```ruby
17
+ require "rubygems"
18
+ require "triangular"
19
+
20
+ # Open and parse an STL file
21
+ solid = Triangular.parse_file("test.stl")
22
+
23
+ # Set the units of measurement for the resulting solid to inches
24
+ solid.units = :inches
25
+
26
+ # Move the solid so that all of it's coordinates are in positive space (ie: greater than 0)
27
+ solid.align_to_origin!
28
+
29
+ # Get the bounding box of the solid
30
+ bounds = solid.bounds
31
+
32
+ # Create a section plane ('slice') through the solid on the XY plane at a Z height of 0.7
33
+ slice = solid.slice_at_z(0.7)
34
+
35
+ # Open a file for SVG output
36
+ File.open("slice.svg", "w+") do |file|
37
+
38
+ # Output the slice as an SVG document (correctly scaled according to the solid's units)
39
+ file.puts slice.to_svg(bounds[1].x, bounds[1].y, solid.units)
40
+ end
41
+ ```
42
+
43
+ ### Installation:
44
+
45
+ Add Triangular to your Gemfile:
46
+
47
+ ```ruby
48
+ gem 'triangular', '~>0.1.0'
49
+ ```
50
+ And then execute:
51
+ ```
52
+ bundle install
53
+ ```
54
+ Or install it manually by entering the following on your command line:
55
+ ```
56
+ gem install triangular
57
+ ```
58
+
59
+ ### Performance:
60
+
61
+ At the moment Triangular has not been optimized at all. The parser is a relatively naive one that was designed to be easy to read rather than performant. Once the feature-set of Triangular has stabilized I will be doing a pass over it in order to make it fast enough for production use. Right now it could definitely be improved.
62
+
63
+ For example here is some information about run-times when processing a 51Mb STL file:
64
+
65
+ ```ruby
66
+ solid = Triangular.parse("big_file.stl")
67
+ # 65 seconds
68
+
69
+ solid.align_to_origin!
70
+ # 8 seconds
71
+
72
+ solid.slice_at_z(1.0)
73
+ # 2 seconds
74
+ ```
75
+
76
+ ### Development:
77
+
78
+ To get setup for local development of Triangular please run the following steps:
79
+
80
+ ```
81
+ git clone git@github.com:aarongough/triangular.git
82
+ cd triangular
83
+ bundle install
84
+ ```
85
+
86
+ Then run the specs to make sure everything is working!
87
+ ```
88
+ bundle exec rspec
89
+ ```
90
+
91
+ ### Author & Credits:
92
+
93
+ Author: [Aaron Gough](mailto:aaron@aarongough.com)
94
+
95
+ Special thanks go out to [Alkas Baybas](https://github.com/abaybas) for lending me his massive brain!
96
+
97
+ Copyright © 2022 [Aaron Gough](http://aarongough.com/) ([http://aarongough.com](http://aarongough.com/)), released under the MIT license
98
+
@@ -1,5 +1,7 @@
1
- require "bundler/setup"
2
- require "triangular"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'triangular'
3
5
 
4
6
  solid = Triangular.parse_file("#{File.dirname(__FILE__)}/example_files/y-axis-spacer.stl")
5
7
  solid.units = :inches
@@ -9,6 +11,6 @@ bounds = solid.get_bounds
9
11
 
10
12
  polyline = solid.slice_at_z(0.7)
11
13
 
12
- File.open(File.expand_path("~/Desktop/slice.svg"), "w+") do |file|
14
+ File.open(File.expand_path('~/Desktop/slice.svg'), 'w+') do |file|
13
15
  file.puts polyline.to_svg(bounds[1].x, bounds[1].y, solid.units)
14
- end
16
+ end
@@ -1,25 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Triangular
2
4
  class Facet
3
-
4
5
  attr_accessor :normal, :vertices
5
-
6
+
6
7
  def initialize(normal = nil, *args)
7
8
  @normal = normal
8
9
  @vertices = args
9
10
  end
10
-
11
+
11
12
  def to_s
12
- output = "facet normal #{@normal.to_s}\n"
13
+ output = "facet normal #{@normal}\n"
13
14
  output += "outer loop\n"
14
15
  @vertices.each do |vertex|
15
- output += vertex.to_s + "\n"
16
+ output += "#{vertex}\n"
16
17
  end
17
18
  output += "endloop\n"
18
19
  output += "endfacet\n"
19
-
20
+
20
21
  output
21
22
  end
22
-
23
+
23
24
  def lines
24
25
  [
25
26
  Line.new(@vertices[0], @vertices[1]),
@@ -27,51 +28,40 @@ module Triangular
27
28
  Line.new(@vertices[2], @vertices[0])
28
29
  ]
29
30
  end
30
-
31
+
31
32
  def intersection_at_z(z_plane)
32
- return nil if @vertices.count{|vertex| vertex.z == z_plane} > 2
33
-
34
- intersection_points = []
35
- lines.each do |line|
36
- intersection_points << line.intersection_at_z(z_plane) unless line.start.z == z_plane && line.end.z == z_plane
37
- end
38
-
39
- intersection_points.compact!
40
- if intersection_points.empty?
41
- nil
42
- elsif intersection_points.count == 2
43
- Line.new(intersection_points[0], intersection_points[1])
44
- end
33
+ return nil if @vertices.count { |vertex| vertex.z == z_plane } > 2
34
+
35
+ intersection_points = lines.map do |line|
36
+ line.intersection_at_z(z_plane)
37
+ end.compact
38
+
39
+ return Line.new(intersection_points[0], intersection_points[1]) if intersection_points.count == 2
40
+
41
+ nil
45
42
  end
46
-
43
+
47
44
  def translate!(x, y, z)
48
45
  @vertices.each do |vertex|
49
46
  vertex.translate!(x, y, z)
50
47
  end
51
- end
52
-
48
+ end
49
+
53
50
  def self.parse(string)
54
51
  facets = []
55
-
56
- string.scan(self.pattern) do |match_data|
57
- facet = self.new
58
-
59
- facet.vertices << Vertex.parse(match_data[4])
60
- facet.vertices << Vertex.parse(match_data[9])
61
- facet.vertices << Vertex.parse(match_data[14])
62
-
63
- facet.normal = Vector.parse(match_data[0])
64
-
65
- facets << facet
66
- end
67
-
68
- if facets.length == 1
69
- facets.first
70
- else
71
- facets
52
+
53
+ string.scan(pattern) do |match_data|
54
+ facets << Facet.new(
55
+ Vector.parse(match_data[0]), # Normal
56
+ Vertex.parse(match_data[4]), # Vertex 1
57
+ Vertex.parse(match_data[9]), # Vertex 2
58
+ Vertex.parse(match_data[14]) # Vertex 3
59
+ )
72
60
  end
61
+
62
+ facets.length == 1 ? facets.first : facets
73
63
  end
74
-
64
+
75
65
  def self.pattern
76
66
  /
77
67
  \s* facet\snormal\s (?<normal> #{Point.pattern})\s
@@ -1,36 +1,62 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Triangular
2
4
  class Line
3
-
4
5
  attr_accessor :start, :end
5
-
6
+
6
7
  def initialize(line_start, line_end)
7
8
  @start = line_start
8
9
  @end = line_end
9
10
  end
10
-
11
+
11
12
  def ==(other)
12
13
  return false unless other.is_a?(Line)
13
- self.start == other.start && self.end == other.end
14
+
15
+ start == other.start && self.end == other.end
14
16
  end
15
-
17
+
18
+ def intersects_x?(x_plane)
19
+ (@start.x >= x_plane && @end.x <= x_plane) || (@start.x <= x_plane && @end.x >= x_plane)
20
+ end
21
+
22
+ def intersection_at_x(x_plane)
23
+ return nil unless intersects_x?(x_plane)
24
+ return nil if @start.x == x_plane && @end.x == x_plane
25
+
26
+ y_intersect = (@end.y - @start.y) / (@end.x - @start.x) * (x_plane - @start.x) + @start.y
27
+ z_intersect = (@end.z - @start.z) / (@end.x - @start.x) * (x_plane - @start.x) + @start.z
28
+
29
+ Point.new(x_plane, y_intersect, z_intersect)
30
+ end
31
+
32
+ def intersects_y?(y_plane)
33
+ (@start.y >= y_plane && @end.y <= y_plane) || (@start.y <= y_plane && @end.y >= y_plane)
34
+ end
35
+
36
+ def intersection_at_y(y_plane)
37
+ return nil unless intersects_y?(y_plane)
38
+ return nil if @start.y == y_plane && @end.y == y_plane
39
+
40
+ x_intersect = (@end.x - @start.x) / (@end.y - @start.y) * (y_plane - @start.y) + @start.x
41
+ z_intersect = (@end.z - @start.z) / (@end.y - @start.y) * (y_plane - @start.y) + @start.z
42
+
43
+ Point.new(x_intersect, y_plane, z_intersect)
44
+ end
45
+
16
46
  def intersects_z?(z_plane)
17
- if (@start.z >= z_plane && @end.z <= z_plane) || (@start.z <= z_plane && @end.z >= z_plane)
18
- true
19
- else
20
- false
21
- end
47
+ (@start.z >= z_plane && @end.z <= z_plane) || (@start.z <= z_plane && @end.z >= z_plane)
22
48
  end
23
-
49
+
24
50
  def intersection_at_z(z_plane)
25
- return nil if !self.intersects_z?(z_plane)
26
- raise "Cannot calculate intersection for line that lies on the target Z plane" if @start.z == z_plane && @end.z == z_plane
27
-
51
+ return nil unless intersects_z?(z_plane)
52
+ return nil if @start.z == z_plane && @end.z == z_plane
53
+
28
54
  x_intersect = (@end.x - @start.x) / (@end.z - @start.z) * (z_plane - @start.z) + @start.x
29
55
  y_intersect = (@end.y - @start.y) / (@end.z - @start.z) * (z_plane - @start.z) + @start.y
30
-
56
+
31
57
  Point.new(x_intersect, y_intersect, z_plane)
32
58
  end
33
-
59
+
34
60
  def to_svg_path(units)
35
61
  "<path d=\"M #{@start.x} #{@start.y} L #{@end.x} #{@end.y}\" fill=\"none\" stroke=\"black\" stroke-width=\"#{Units.stroke_width(units)}\" />"
36
62
  end
@@ -1,39 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Triangular
2
4
  class Point
3
-
4
5
  attr_accessor :x, :y, :z
5
-
6
+
6
7
  def initialize(x, y, z)
7
8
  @x = x
8
9
  @y = y
9
- @z = z
10
+ @z = z
10
11
  end
11
-
12
+
12
13
  def to_s
13
14
  "#{@x.to_f} #{@y.to_f} #{@z.to_f}"
14
15
  end
15
-
16
+
16
17
  def translate!(x, y, z)
17
18
  @x += x
18
19
  @y += y
19
20
  @z += z
20
21
  end
21
-
22
+
22
23
  def ==(other)
23
24
  return false unless other.is_a?(Point)
24
- self.x == other.x && self.y == other.y && self.z == other.z
25
+
26
+ x == other.x && y == other.y && z == other.z
25
27
  end
26
-
28
+
27
29
  def self.parse(string)
28
- string.strip!
29
- match_data = string.match(self.pattern)
30
-
31
- self.new(match_data[:x].to_f, match_data[:y].to_f, match_data[:z].to_f)
30
+ match_data = string.strip.match(pattern)
31
+
32
+ new(match_data[:x].to_f, match_data[:y].to_f, match_data[:z].to_f)
32
33
  end
33
-
34
+
34
35
  def self.pattern
35
- /(?<x>-?\d+.\d+(e\-?\d+)?)\s(?<y>-?\d+.\d+(e\-?\d+)?)\s(?<z>-?\d+.\d+(e\-?\d+)?)/
36
+ /(?<x>-?\d+(.\d+(e-?\d+)?)?)\s(?<y>-?\d+(.\d+(e-?\d+)?)?)\s(?<z>-?\d+(.\d+(e-?\d+)?)?)/
36
37
  end
37
-
38
38
  end
39
39
  end
@@ -1,24 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Triangular
2
4
  class Polyline
3
-
4
5
  attr_accessor :lines
5
-
6
+
6
7
  def initialize(lines)
7
8
  @lines = lines
8
9
  end
9
-
10
+
10
11
  def to_svg(width, height, units, x_offset = 0, y_offset = 0)
11
- output = '<?xml version="1.0" standalone="no"?>' + "\n"
12
- output << '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + "\n"
13
- output << "<svg x=\"0\" y=\"0\" width=\"#{width}#{Units.svg_name(units)}\" height=\"#{height}#{Units.svg_name(units)}\" viewBox=\"0 0 #{width} #{height}\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n"
14
- output << " <g transform=\"translate(#{x_offset}#{Units.svg_name(units)},#{y_offset}#{Units.svg_name(units)})\">\n"
15
-
12
+ output = "<?xml version=\"1.0\" standalone=\"no\"?>\n"
13
+ output += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
14
+ output += "<svg x=\"0\" y=\"0\" width=\"#{width}#{Units.svg_name(units)}\" height=\"#{height}#{Units.svg_name(units)}\" viewBox=\"0 0 #{width} #{height}\" xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n"
15
+ output += " <g transform=\"translate(#{x_offset}#{Units.svg_name(units)},#{y_offset}#{Units.svg_name(units)})\">\n"
16
+
16
17
  @lines.each do |line|
17
- output << " " + line.to_svg_path(units) + "\n"
18
+ output += " #{line.to_svg_path(units)}\n"
18
19
  end
19
-
20
- output << ' </g>' + "\n"
21
- output << '</svg>'
20
+
21
+ output += " </g>\n"
22
+ output += '</svg>'
22
23
  end
23
24
  end
24
25
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Triangular
4
+ class Ray
5
+ attr_accessor :x, :y
6
+
7
+ def initialize(x, y)
8
+ @x = x
9
+ @y = y
10
+ end
11
+
12
+ def intersection(facet)
13
+ lines = facet.lines
14
+
15
+ x_intersection = false
16
+ y_intersection = false
17
+
18
+ lines.each do |line|
19
+ x_intersection = true if line.start.x <= @x && line.end.x >= @x
20
+ x_intersection = true if line.end.x <= @x && line.start.x >= @x
21
+
22
+ y_intersection = true if line.start.y <= @y && line.end.y >= @y
23
+ y_intersection = true if line.end.y <= @y && line.start.y >= @y
24
+ end
25
+
26
+ return nil unless x_intersection && y_intersection
27
+
28
+ x_intersections = lines.map do |line|
29
+ line.intersection_at_x(@x)
30
+ end
31
+
32
+ x_intersections.compact!
33
+
34
+ if x_intersections[0] == x_intersections[1]
35
+ x_intersections[0]
36
+ else
37
+ Line.new(x_intersections[0], x_intersections[1]).intersection_at_y(@y)
38
+ end
39
+ end
40
+ end
41
+ end