fontisan 0.1.0 → 0.2.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.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Optimizers
5
+ # Main orchestrator for CFF subroutine generation pipeline.
6
+ # Coordinates PatternAnalyzer, SubroutineOptimizer, SubroutineBuilder,
7
+ # and CharstringRewriter to generate optimized subroutines for fonts.
8
+ #
9
+ # The generator processes CharStrings from a CFF font table by:
10
+ # 1. Analyzing patterns across all glyphs
11
+ # 2. Selecting optimal patterns (avoiding conflicts, within limits)
12
+ # 3. Ordering patterns by frequency for efficient encoding
13
+ # 4. Building actual subroutine CharStrings
14
+ # 5. Rewriting original CharStrings with subroutine calls
15
+ #
16
+ # @example Basic usage
17
+ # generator = SubroutineGenerator.new(min_pattern_length: 10)
18
+ # result = generator.generate(font)
19
+ # puts "Generated #{result[:selected_count]} subroutines"
20
+ # puts "Total savings: #{result[:savings]} bytes"
21
+ #
22
+ # @see docs/SUBROUTINE_ARCHITECTURE.md
23
+ class SubroutineGenerator
24
+ # Default minimum pattern length in bytes
25
+ DEFAULT_MIN_PATTERN_LENGTH = 10
26
+
27
+ # Default maximum number of subroutines (CFF limit)
28
+ DEFAULT_MAX_SUBROUTINES = 65_535
29
+
30
+ # Initialize generator with options
31
+ # @param options [Hash] configuration options
32
+ # @option options [Integer] :min_pattern_length (10) minimum pattern size
33
+ # @option options [Integer] :max_subroutines (65535) max subroutine count
34
+ # @option options [Boolean] :optimize_ordering (true) enable frequency
35
+ # ordering
36
+ def initialize(options = {})
37
+ @min_pattern_length = options[:min_pattern_length] ||
38
+ DEFAULT_MIN_PATTERN_LENGTH
39
+ @max_subroutines = options[:max_subroutines] || DEFAULT_MAX_SUBROUTINES
40
+ @optimize_ordering = options[:optimize_ordering] != false
41
+ end
42
+
43
+ # Generate subroutines for a font
44
+ #
45
+ # Main entry point for the subroutine generation pipeline. Processes
46
+ # a font's CFF table to create optimized subroutines and rewrite
47
+ # CharStrings.
48
+ #
49
+ # @param font [Fontisan::OpenTypeFont] font to optimize
50
+ # @return [Hash] result containing:
51
+ # - :local_subrs [Array<String>] subroutine CharStrings
52
+ # - :charstrings [Hash<Integer, String>] rewritten CharStrings
53
+ # - :bias [Integer] CFF bias value for subroutines
54
+ # - :savings [Integer] total bytes saved
55
+ # - :pattern_count [Integer] total patterns found
56
+ # - :selected_count [Integer] patterns selected as subroutines
57
+ # @raise [ArgumentError] if font has no CFF table
58
+ def generate(font)
59
+ # 1. Extract CharStrings from CFF table
60
+ charstrings = extract_charstrings(font)
61
+
62
+ # Handle empty font gracefully
63
+ if charstrings.empty?
64
+ return {
65
+ local_subrs: [],
66
+ charstrings: {},
67
+ bias: 0,
68
+ savings: 0,
69
+ pattern_count: 0,
70
+ selected_count: 0,
71
+ }
72
+ end
73
+
74
+ # 2. Analyze patterns
75
+ analyzer = PatternAnalyzer.new(
76
+ min_length: @min_pattern_length,
77
+ stack_aware: true
78
+ )
79
+ patterns = analyzer.analyze(charstrings)
80
+
81
+ # 3. Optimize selection
82
+ optimizer = SubroutineOptimizer.new(patterns,
83
+ max_subrs: @max_subroutines)
84
+ selected_patterns = optimizer.optimize_selection
85
+
86
+ # 4. Optimize ordering (if enabled)
87
+ if @optimize_ordering
88
+ selected_patterns = optimizer.optimize_ordering(selected_patterns)
89
+ end
90
+
91
+ # 5. Build subroutines
92
+ builder = SubroutineBuilder.new(selected_patterns, type: :local)
93
+ subroutines = builder.build
94
+
95
+ # 6. Build subroutine map
96
+ subroutine_map = build_subroutine_map(selected_patterns)
97
+
98
+ # 7. Rewrite CharStrings
99
+ rewriter = CharstringRewriter.new(subroutine_map, builder)
100
+ rewritten_charstrings = rewrite_charstrings(
101
+ charstrings,
102
+ selected_patterns,
103
+ rewriter,
104
+ )
105
+
106
+ # 8. Return complete result
107
+ {
108
+ local_subrs: subroutines,
109
+ charstrings: rewritten_charstrings,
110
+ bias: builder.bias,
111
+ savings: calculate_total_savings(selected_patterns),
112
+ pattern_count: patterns.length,
113
+ selected_count: selected_patterns.length,
114
+ }
115
+ end
116
+
117
+ private
118
+
119
+ # Extract CharStrings from CFF table
120
+ #
121
+ # Retrieves raw CharString byte sequences for each glyph from the
122
+ # font's CFF table. The CharStrings INDEX is accessed through the
123
+ # CFF table structure.
124
+ #
125
+ # @param font [Fontisan::OpenTypeFont] font to extract from
126
+ # @return [Hash<Integer, String>] glyph_id => charstring_bytes
127
+ # @raise [ArgumentError] if font has no CFF table
128
+ def extract_charstrings(font)
129
+ cff = font.table("CFF ")
130
+ raise ArgumentError, "Font must have CFF table" unless cff
131
+
132
+ charstrings = {}
133
+
134
+ # Get CharStrings INDEX for first font (index 0)
135
+ charstrings_index = cff.charstrings_index(0)
136
+ unless charstrings_index
137
+ raise ArgumentError, "Font CFF table has no CharStrings"
138
+ end
139
+
140
+ # Extract raw CharString bytes for each glyph
141
+ # Index.each yields raw bytes, we add index manually
142
+ index = 0
143
+ charstrings_index.each do |cs_data|
144
+ charstrings[index] = cs_data
145
+ index += 1
146
+ end
147
+
148
+ charstrings
149
+ end
150
+
151
+ # Build map from pattern bytes to subroutine ID
152
+ #
153
+ # Creates a lookup table for the rewriter to quickly find which
154
+ # subroutine ID corresponds to each pattern's byte sequence.
155
+ #
156
+ # @param patterns [Array<Pattern>] selected patterns
157
+ # @return [Hash<String, Integer>] pattern_bytes => subroutine_id
158
+ def build_subroutine_map(patterns)
159
+ map = {}
160
+ patterns.each_with_index do |pattern, index|
161
+ map[pattern.bytes] = index
162
+ end
163
+ map
164
+ end
165
+
166
+ # Rewrite all CharStrings with subroutine calls
167
+ #
168
+ # Processes each glyph's CharString, replacing pattern occurrences
169
+ # with calls to their corresponding subroutines. Glyphs without
170
+ # applicable patterns are kept unchanged.
171
+ #
172
+ # @param charstrings [Hash<Integer, String>] original CharStrings
173
+ # @param patterns [Array<Pattern>] patterns to use
174
+ # @param rewriter [CharstringRewriter] rewriter instance
175
+ # @return [Hash<Integer, String>] rewritten CharStrings
176
+ def rewrite_charstrings(charstrings, patterns, rewriter)
177
+ rewritten = {}
178
+
179
+ charstrings.each do |glyph_id, charstring|
180
+ # Find patterns for this glyph
181
+ glyph_patterns = patterns.select { |p| p.glyphs.include?(glyph_id) }
182
+
183
+ rewritten[glyph_id] = if glyph_patterns.empty?
184
+ # No patterns, keep original
185
+ charstring
186
+ else
187
+ # Rewrite with subroutine calls
188
+ rewriter.rewrite(charstring, glyph_patterns)
189
+ end
190
+ end
191
+
192
+ rewritten
193
+ end
194
+
195
+ # Calculate total byte savings
196
+ #
197
+ # Sums up the savings from all selected patterns to determine
198
+ # total file size reduction achieved by subroutinization.
199
+ #
200
+ # @param patterns [Array<Pattern>] selected patterns
201
+ # @return [Integer] total bytes saved
202
+ def calculate_total_savings(patterns)
203
+ patterns.sum(&:savings)
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Optimizers
5
+ # Optimizes subroutine selection and ordering for maximum file size reduction.
6
+ # Uses a greedy algorithm to select the most beneficial patterns while avoiding
7
+ # conflicts, then orders them by frequency for efficient encoding.
8
+ #
9
+ # @example Basic usage
10
+ # analyzer = PatternAnalyzer.new
11
+ # patterns = analyzer.analyze(charstrings)
12
+ # optimizer = SubroutineOptimizer.new(patterns, max_subrs: 65535)
13
+ # selected_patterns = optimizer.optimize_selection
14
+ # ordered_patterns = optimizer.optimize_ordering(selected_patterns)
15
+ #
16
+ # @see docs/SUBROUTINE_ARCHITECTURE.md
17
+ class SubroutineOptimizer
18
+ # Initialize optimizer with patterns
19
+ # @param patterns [Array<Pattern>] patterns from analyzer
20
+ # @param max_subrs [Integer] maximum number of subroutines (default: 65535)
21
+ def initialize(patterns, max_subrs: 65535)
22
+ @patterns = patterns
23
+ @max_subrs = max_subrs
24
+ end
25
+
26
+ # Select optimal subset of patterns to subroutinize
27
+ # Uses greedy algorithm: select by highest savings first, checking for
28
+ # conflicts with already selected patterns.
29
+ #
30
+ # @return [Array<Pattern>] selected patterns
31
+ def optimize_selection
32
+ selected = []
33
+ remaining = @patterns.sort_by { |p| -p.savings }
34
+
35
+ remaining.each do |pattern|
36
+ break if selected.length >= @max_subrs
37
+ next if conflicts_with_selected?(pattern, selected)
38
+
39
+ selected << pattern
40
+ end
41
+
42
+ selected
43
+ end
44
+
45
+ # Optimize subroutine ordering by frequency
46
+ # Higher frequency patterns get lower IDs for more efficient encoding
47
+ # in CFF format.
48
+ #
49
+ # @param subroutines [Array<Pattern>] subroutines to order
50
+ # @return [Array<Pattern>] ordered subroutines
51
+ def optimize_ordering(subroutines)
52
+ # Higher frequency = lower ID (shorter encoding)
53
+ subroutines.sort_by { |subr| -subr.frequency }
54
+ end
55
+
56
+ # Check if nesting would be beneficial
57
+ # TODO: Phase 2.1 - check if subroutines contain common patterns
58
+ #
59
+ # @param subroutines [Array<Pattern>] subroutines to analyze
60
+ # @return [Array<Pattern>] subroutines (unchanged for now)
61
+ def optimize_nesting(subroutines)
62
+ subroutines
63
+ end
64
+
65
+ private
66
+
67
+ # Check if pattern conflicts with any already selected patterns
68
+ # A conflict occurs when patterns overlap in the same glyph at
69
+ # overlapping positions.
70
+ #
71
+ # @param pattern [Pattern] pattern to check
72
+ # @param selected [Array<Pattern>] already selected patterns
73
+ # @return [Boolean] true if conflicts, false otherwise
74
+ def conflicts_with_selected?(pattern, selected)
75
+ selected.any? do |sel|
76
+ # Check if they share any glyphs
77
+ common_glyphs = pattern.glyphs & sel.glyphs
78
+ next false if common_glyphs.empty?
79
+
80
+ # Check if positions overlap in any common glyph
81
+ common_glyphs.any? { |gid| positions_overlap?(pattern, sel, gid) }
82
+ end
83
+ end
84
+
85
+ # Check if two patterns overlap at positions in a specific glyph
86
+ # Ranges overlap if they intersect at any point.
87
+ #
88
+ # @param p1 [Pattern] first pattern
89
+ # @param p2 [Pattern] second pattern
90
+ # @param glyph_id [Integer] glyph to check
91
+ # @return [Boolean] true if positions overlap, false otherwise
92
+ def positions_overlap?(p1, p2, glyph_id)
93
+ pos1 = p1.positions[glyph_id] || []
94
+ pos2 = p2.positions[glyph_id] || []
95
+
96
+ pos1.any? do |start1|
97
+ end1 = start1 + p1.length
98
+ pos2.any? do |start2|
99
+ end2 = start2 + p2.length
100
+ # Check if ranges overlap: start1 < end2 && start2 < end1
101
+ start1 < end2 && start2 < end1
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end