rayzer 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d2d485235aff6865bd30482c32ffb45f1d4fb51ca8ecc49f4514edd774453aa
4
+ data.tar.gz: 6d01f1423c71f9ebe81e259fefc8db44bbc1795cd139abc2729992d52cf22c14
5
+ SHA512:
6
+ metadata.gz: 37213df3a9af6cdc3e3c75850644a89abba0991f1bcf95ab3c899193811137162d2f240de63723b70ec9aeb3dd50ed0062728cbea1385de7545c60d1b01da6af
7
+ data.tar.gz: cc974c31a04788be0206123bddeacfef55e11d92836264609e564ce430d2832383787b1ca47ced170cfa4faf8457f1e8f1a22c261b8fe9501387c6176c7be5ad
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.1] - 2025-11-17
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 NullFluxKevin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Rayzer
2
+
3
+ A razor sharp layout engine for rectangular areas.
4
+
5
+ Use constraints such as fixed length, minimum length, percentage, ratio, and maximum length to divide a rectangular area into a tree of nested rows and columns.
6
+
7
+
8
+ ## Usage
9
+
10
+ See `usage_example.rb`.
11
+
12
+ For full specs, read the tests.
13
+
14
+ TODO: more on usage
15
+
16
+
17
+ ## Installation
18
+
19
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
20
+
21
+ Install the gem and add to the application's Gemfile by executing:
22
+
23
+ ```bash
24
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
25
+ ```
26
+
27
+ If bundler is not being used to manage dependencies, install the gem by executing:
28
+
29
+ ```bash
30
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
31
+ ```
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,111 @@
1
+ module Rayzer
2
+ class Constraint
3
+ # zero or one <=, >=, %, or :
4
+ # (?<=) is a look ahead assertion, if matches < or >, then check for =
5
+ # followed by digits
6
+ # then optionally followed by a decimal point "." and digits
7
+ @parse_arg_format_regex = /^([<>]?(?<=[<>])=|[%:])?\d+(.\d+)?$/
8
+
9
+ FIXED = :fixed
10
+ MINIMUM = :minimum
11
+ MAXIMUM = :maximum
12
+ PERCENTAGE = :percentage
13
+ RATIO = :ratio
14
+
15
+ # define factory class methods such as Constraint.fixed(value)
16
+ constants.each do |constraint_type|
17
+ define_singleton_method(constraint_type.downcase) do |value|
18
+ new(const_get(constraint_type), value)
19
+ end
20
+ end
21
+
22
+
23
+ attr_reader :type, :value
24
+
25
+
26
+ def initialize(type, value)
27
+ raise ArgumentError, "Negative constraint value: #{value}" if value.negative?
28
+ @type = type
29
+ @value = value
30
+ end
31
+
32
+
33
+ # This intends to provide a concise api for things that accepts constraints
34
+ #
35
+ # Usage:
36
+ # To get a fixed constraint, pass: 30, "30", or :"30"
37
+ # To get a minimum constraint, pass: ">=30", or :">=30"
38
+ # To get a percentage constraint, pass: "30%", :"30%", "%30", :"%30"
39
+ # To get a ratio constraint, pass: ":30", or :":30"
40
+ # To get a maximum constraint, pass: "<=30", or :"<=30"
41
+ #
42
+ #
43
+ # API Usage Example:
44
+ # constraints = %i[ 3 20% <=10 >=5 :3 ] # or use %w
45
+ # constraints << Constraint.fixed(1)
46
+ #
47
+ # some_method(args)
48
+ #
49
+ # def some_method(args)
50
+ # constraints = args.map { |arg| Constraint.parse arg )
51
+ # ...
52
+ # end
53
+ #
54
+ def self.parse(arg)
55
+
56
+ is_of_valid_type = [Integer, Float, String, Symbol, self].any?{ |cls| arg.instance_of? cls }
57
+
58
+ unless is_of_valid_type
59
+ raise ArgumentError,
60
+ "Cannot parse: #{arg}. Constraint.parse only accepts: Constraint, Integer, Float, Symbol and String of valid format"
61
+ end
62
+
63
+ return arg.dup if arg.instance_of? self
64
+
65
+ if [Integer, Float].include? arg.class
66
+ return fixed arg
67
+ end
68
+
69
+ # I'm not making the regexp any more complex with look ahead assertion for checking % at the end only if the arg starts with a digit.
70
+ if arg[-1] == "%"
71
+ arg = "%" + arg[...-1]
72
+ end
73
+
74
+ raise ArgumentError, "Invalid argument format in: #{arg}" unless arg =~ @parse_arg_format_regex
75
+
76
+ if arg[0] =~ /\d/
77
+ value = parse_number arg
78
+ return fixed value
79
+ end
80
+
81
+ case arg[0]
82
+ when ">"
83
+ value = parse_number arg[2..] # 2 == '>='.size
84
+ minimum value
85
+ when "%"
86
+ value = parse_number arg[1..]
87
+ percentage value
88
+ when "<"
89
+ value = parse_number arg[2..] # 2 == '<='.size
90
+ maximum value
91
+ when ":"
92
+ value = parse_number arg[1..]
93
+ ratio value
94
+ end
95
+ end
96
+
97
+
98
+ def ==(other)
99
+ (@type == other.type) && (@value == other.value)
100
+ end
101
+
102
+
103
+ private
104
+ def self.parse_number(arg)
105
+ arg = arg.to_s
106
+ arg.include?(".") ? arg.to_f : arg.to_i
107
+ end
108
+
109
+ end # end of class
110
+
111
+ end # end of module
@@ -0,0 +1,126 @@
1
+ # Constraint Priority:
2
+ # fixed = minimum > percentage > ratio > maximum
3
+ #
4
+ # If there is still a remaining when every constraint is satisfied:
5
+ # If there is a minimum constraint, then the remaining is added to the result of the first minimum constraint.
6
+ # Otherwise:
7
+ # If used distribute, the remaining is added to the end of the returned array
8
+ # If used distribute!, raise an exception.
9
+
10
+ module Rayzer
11
+ module Distributor
12
+ refine Numeric do
13
+
14
+ def distribute(*constraints)
15
+ _distribute(constraints, false)
16
+ end # end of distribute method
17
+
18
+
19
+ def distribute!(*constraints)
20
+ # raises if the value is not completely distributed
21
+ _distribute(constraints, true)
22
+ end # end of distribute! method
23
+
24
+
25
+ private
26
+ def _distribute(args, raise_if_has_remaining)
27
+ raise ArgumentError, "Can not distribute non-real value #{self}" unless [Integer, Float].include? self.class
28
+
29
+ raise ArgumentError, "Can not distribute non-positive value #{self}" unless self.positive?
30
+
31
+ constraints = args.map { |arg| Constraint.parse arg }
32
+
33
+ remaining = self
34
+ parts = Array.new(constraints.size) { 0 }
35
+ first_min_index = nil
36
+
37
+ total_percentage = 0
38
+ percentages = []
39
+
40
+ total_ratio = 0
41
+ ratios = []
42
+
43
+ maximums = []
44
+
45
+
46
+ constraints.each.with_index do |cons, i|
47
+ case cons.type
48
+ when Constraint::FIXED, Constraint::MINIMUM
49
+ parts[i] = cons.value
50
+ remaining -= cons.value
51
+ raise ArgumentError, "Sum of required constraints exceeds #{self}" if remaining < 0
52
+
53
+ if cons.type == Constraint::MINIMUM
54
+ first_min_index = i if first_min_index.nil?
55
+ end
56
+
57
+ when Constraint::PERCENTAGE
58
+ total_percentage += cons.value
59
+ raise ArgumentError, "Sum of percentage constraints exceeds 100" if total_percentage > 100
60
+
61
+ percentages << [i, cons.value]
62
+
63
+ when Constraint::RATIO
64
+ total_ratio += cons.value
65
+ ratios << [i, cons.value]
66
+
67
+ when Constraint::MAXIMUM
68
+ maximums << [i, cons.value]
69
+ end
70
+ end
71
+
72
+
73
+ unless remaining.zero? or total_percentage.zero?
74
+ percentages.each do |i, percentage|
75
+ parts[i] = percentage * 0.01 * remaining
76
+ end
77
+
78
+ remaining = (1 - total_percentage * 0.01) * remaining
79
+ end
80
+
81
+
82
+ unless remaining.zero? or total_ratio.zero?
83
+ value_per_ratio = remaining.fdiv total_ratio
84
+
85
+ ratios.each do |i, ratio|
86
+ parts[i] = ratio * value_per_ratio
87
+ end
88
+
89
+ remaining = 0
90
+ end
91
+
92
+
93
+ maximums.each do |i, value|
94
+ if value < remaining
95
+ parts[i] = value
96
+ remaining -= value
97
+ else
98
+ parts[i] = remaining
99
+ remaining = 0
100
+ break
101
+ end
102
+ end
103
+
104
+
105
+ unless remaining.zero?
106
+ unless first_min_index.nil?
107
+ parts[first_min_index] += remaining
108
+ return parts
109
+ end
110
+
111
+ raise "Incomplete distribution of #{self}, remaining #{remaining}" if raise_if_has_remaining
112
+
113
+ parts << remaining
114
+ end
115
+
116
+
117
+ parts
118
+ end # end of _distribute method
119
+
120
+ # end of private
121
+
122
+ end # end of refinement
123
+
124
+ end # end of Distributor
125
+
126
+ end # end of Rayzer
@@ -0,0 +1,141 @@
1
+ require_relative 'distributor'
2
+
3
+
4
+ module Rayzer
5
+ class Layout
6
+ class RemainingSpaceError < ArgumentError; end
7
+
8
+ using Distributor
9
+
10
+ COLUMN_CONTAINER = :column_container
11
+ ROW_CONTAINER = :row_container
12
+ LEAF = :leaf
13
+
14
+ attr_reader :x, :y, :width, :height, :parent, :children, :type
15
+
16
+
17
+ def initialize(x, y, width, height, parent=nil)
18
+ @x = x
19
+ @y = y
20
+ @width = width
21
+ @height = height
22
+ @parent = parent
23
+ @children = []
24
+ @type = LEAF
25
+ end
26
+
27
+
28
+ def root?
29
+ @parent.nil?
30
+ end
31
+
32
+
33
+ def leaf?
34
+ @type == LEAF
35
+ end
36
+
37
+ def rect
38
+ [@x, @y, @width, @height]
39
+ end
40
+
41
+
42
+ def split_to_rows(constraints, names=nil, &block)
43
+ heights = @height.distribute(*constraints)
44
+ curr_y = @y
45
+ @children = heights.map do |h|
46
+ row = self.class.new(@x, curr_y, @width, h, self)
47
+ curr_y += h
48
+ row
49
+ end
50
+
51
+ constraints_size = constraints.size
52
+ add_named_layout_instance_vars names, constraints_size
53
+ add_remaining_instance_var constraints_size
54
+
55
+ @type = ROW_CONTAINER
56
+ yield @children if block_given?
57
+ @children
58
+ end
59
+
60
+
61
+ def split_to_cols(constraints, names=nil, &block)
62
+ widths = @width.distribute(*constraints)
63
+
64
+ curr_x = @x
65
+
66
+ @children = widths.map do |w|
67
+ col = self.class.new(curr_x, @y, w, @height, self)
68
+ curr_x += w
69
+ col
70
+ end
71
+
72
+ constraints_size = constraints.size
73
+
74
+ add_named_layout_instance_vars names, constraints_size
75
+ add_remaining_instance_var constraints_size
76
+
77
+ @type = COLUMN_CONTAINER
78
+ yield @children if block_given?
79
+ @children
80
+ end
81
+
82
+
83
+ def split_to_cols!(constraints, names=nil, &block)
84
+ split_to_cols(constraints, names, &block)
85
+ raise RemainingSpaceError if instance_variable_defined? :@remaining
86
+ @children
87
+ end
88
+
89
+
90
+ def split_to_rows!(constraints, names=nil, &block)
91
+ split_to_rows(constraints, names, &block)
92
+ raise RemainingSpaceError if instance_variable_defined? :@remaining
93
+ @children
94
+ end
95
+
96
+
97
+ def ==(other)
98
+ # should require parent and children equality?
99
+ return false unless @x == other.x
100
+ return false unless @y == other.y
101
+ return false unless @width == other.width
102
+ return false unless @height == other.height
103
+ true
104
+ end
105
+
106
+
107
+ private
108
+ def add_named_layout_instance_vars(names, constraints_size)
109
+ unless names.nil?
110
+ if names.is_a? Array
111
+ raise ArgumentError, "Size of names and size of constraints mismatch" unless names.size == constraints_size
112
+
113
+ names.each.with_index do |name, i|
114
+ next if name.nil?
115
+ instance_variable_set "@#{name}", @children[i]
116
+ define_singleton_method(name) { instance_variable_get "@#{name}" }
117
+ end
118
+
119
+ elsif names.is_a? Hash
120
+ names.each do |i, name|
121
+ instance_variable_set "@#{name}", @children.fetch(i)
122
+ define_singleton_method(name) { instance_variable_get "@#{name}" }
123
+ end
124
+
125
+ else
126
+ raise ArgumentError, "Invalid type for arg: names. Expecting Array or Hash"
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+ def add_remaining_instance_var(constraints_size)
133
+ if @children.size - constraints_size == 1
134
+ instance_variable_set :@remaining, @children[-1]
135
+ define_singleton_method(:remaining) { instance_variable_get :@remaining }
136
+ end
137
+ end
138
+
139
+
140
+ end # end of layout
141
+ end # end of Rayzer
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rayzer
4
+ VERSION = "0.0.1"
5
+ end
data/lib/rayzer.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rayzer/version"
4
+ require_relative "rayzer/constraint"
5
+ require_relative "rayzer/distributor"
6
+ require_relative "rayzer/layout"
@@ -0,0 +1,6 @@
1
+ ## Development
2
+
3
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
4
+
5
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
6
+
data/sig/rayzer.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rayzer
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/usage_example.rb ADDED
@@ -0,0 +1,48 @@
1
+ require "rayzer"
2
+
3
+ x = 0
4
+ y = 0
5
+ width = 100
6
+ height = 100
7
+
8
+ # Create a new top-level layout
9
+ root = Rayzer::Layout.new(x, y, width, height)
10
+
11
+ # Create 3 sections with fixed length 10, minimum length 0 and another fixed length 10.
12
+ sections = %w[ 10 >=0 10 ]
13
+
14
+ # You can optionally name the sections. If given a name, the section can be accessed with the reader of the same name.
15
+ section_names = %w[ header main footer ]
16
+
17
+ # Split_to_* returns the children as well also yields them to the block for further partition if a block is given.
18
+ root.split_to_rows!(sections, section_names) do |header, main, footer|
19
+
20
+ # Percentage constraint
21
+ constraints = %w[ 30% ]
22
+
23
+ # If there is remaining space after all constraints are satisfied, split_to_* appends the remaining to the children, whereas the bang version raises Rayzer::Layout::RemainingSpaceError.
24
+ #
25
+ # The remaining is also accessable using the remaining method.
26
+ #
27
+ # When no names are given, you can only access them using the children attr_reader later
28
+ main.split_to_cols(constraints) do |sidebar, remaining|
29
+
30
+ # Ratio constraints
31
+ rows = %i[:1 :3 :1]
32
+
33
+ # If you only want to access some inner layout by name, use a hash with index as the key and a symbol or name as value.
34
+ row_names = { 1 => :content }
35
+
36
+ remaining.split_to_rows!(rows, row_names)
37
+ end
38
+ end
39
+
40
+
41
+ p root.header.rect # [0, 0, 100, 10]
42
+ p root.main.rect # [0, 10, 100, 80]
43
+ p root.footer.rect # [0, 90, 100, 10]
44
+ p root.main.children[0].rect # [0, 10, 30.0, 80]
45
+ p root.main.remaining.rect # [30.0, 10, 70.0, 80]
46
+ p root.main.remaining.children[0].rect # [30.0, 10, 70.0, 16.0]
47
+ p root.main.remaining.content.rect # [30.0, 26.0, 70.0, 48.0]
48
+ p root.main.remaining.children[2].rect # [30.0, 74.0, 70.0, 16.0]
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rayzer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - NullFluxKevin
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: This Gem allows you to use constraints such as fixed length, minimum
13
+ length, percentage, ratio, and maximum length to divide a rectangular space into
14
+ a tree of nested rows and columns.
15
+ email:
16
+ - null_flux_kevin@outlook.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - lib/rayzer.rb
26
+ - lib/rayzer/constraint.rb
27
+ - lib/rayzer/distributor.rb
28
+ - lib/rayzer/layout.rb
29
+ - lib/rayzer/version.rb
30
+ - releasing_new_version.md
31
+ - sig/rayzer.rbs
32
+ - usage_example.rb
33
+ homepage: https://github.com/NullFluxKevin/rayzer
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/NullFluxKevin/rayzer
38
+ source_code_uri: https://github.com/NullFluxKevin/rayzer
39
+ changelog_uri: https://github.com/NullFluxKevin/rayzer/blob/main/CHANGELOG.md
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.1.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.6.6
55
+ specification_version: 4
56
+ summary: A constraint-based layout engine for rectangular areas.
57
+ test_files: []