ea 0.6.147 → 0.6.148

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f9087e59e10aff45bf81103a88593079d215fc97aa689f41cc8c4ea9a41d37f
4
- data.tar.gz: 0652d5a945a6ff2ccd3c45cbf1302be809f6846eaad1832eced4c617bfd20059
3
+ metadata.gz: d2395d5ef03b627d02de55c09f65eff28984a512919ed75b39d8b1cbe110dc20
4
+ data.tar.gz: ff25b771f4134f453882054a6921eba3e0ffbbb2fe07347292ffdb1ade43e9b2
5
5
  SHA512:
6
- metadata.gz: 460b7225fbd3fd826628f1e9a1c2400a6452579aa3000f33186fd9ed0907ea41582573c6c41e2aa4961993062725e3aa9fe4533096dad4c19b696c2afe8f6cfc
7
- data.tar.gz: b2bef638ce7b06732af0318e7ba159511cd8684b45c990bb2900ee0b1b825bb485d92adc84175c597b53853278ec8285072202e4bd3a7760ed4df83d2a6e5ef7
6
+ metadata.gz: 3c0e61969712ad88fc8d9ccc9da3a034ce5877f880738271935a192eff7219d15927f609134b1f6d956f2abb15d9191492bddf14085372068b1b3bb9544d37fe
7
+ data.tar.gz: fa6306f9f66d3e3192c921127710d365f3a53a64fac9a3498011a9f7c0c632ccb04c8777a5da45bbc78386fc0ba8f8cad18f7c3e5d6fa9597bd4b147cb2391e9
@@ -96,7 +96,7 @@ module Ea
96
96
  .map { |(x, y)| [x.to_f, y.to_f] }
97
97
  {
98
98
  kind: :path, points: points,
99
- centroid: centroid(points), closed: node["d"].to_s.include?("Z"),
99
+ centroid: centroid(points), closed: node["d"].to_s.match?(/[zZ]/),
100
100
  stroke: g_style[/stroke:([^;]+)/, 1]
101
101
  }
102
102
  end
@@ -223,6 +223,171 @@ module Ea
223
223
  private_constant :TextComparison
224
224
  include TextComparison
225
225
 
226
+ # Endpoint distances are Euclidean distances after canvas alignment.
227
+ # Arrowhead tips correspond by geometry. An unpaired arrowhead
228
+ # has infinite drift; correctly placed endpoints and tips have zero.
229
+ module ConnectorComparison
230
+ def connector_deltas
231
+ heads = shape_pairs(arrowheads(@reference), arrowheads(@ours))
232
+ connector_pairs.map do |ours, ref|
233
+ connector_delta(ours, ref).merge(arrowhead_deltas: arrowhead_deltas(ours, ref, heads))
234
+ end
235
+ end
236
+
237
+ def connector_delta(ours, ref)
238
+ {
239
+ kind: ours[:kind], ours: ours, reference: ref,
240
+ source_delta: endpoint_distance(ours[:points].first, ref[:points].first),
241
+ target_delta: endpoint_distance(ours[:points].last, ref[:points].last)
242
+ }
243
+ end
244
+ private :connector_delta
245
+
246
+ def connectors(primitives)
247
+ primitives.select do |primitive|
248
+ %i[path line].include?(primitive[:kind]) && !primitive[:closed] &&
249
+ primitive[:points].size >= 2 && !open_arrowhead?(primitive)
250
+ end
251
+ end
252
+ private :connectors
253
+
254
+ # Preserve local matches even when both docks move: the allowance
255
+ # for total endpoint drift scales with connector length. Choose the
256
+ # smallest total endpoint drift first, so a shared target cannot
257
+ # steal another connector's exact match.
258
+ def connector_pairs
259
+ candidates = connectors(@ours).product(connectors(@reference))
260
+ candidates.sort_by! { |ours, ref| connector_distance(ours, ref) }
261
+ used = {}.compare_by_identity
262
+ candidates.select do |ours, ref|
263
+ next false if used[ours] || used[ref] || !connector_distance(ours, ref).finite?
264
+
265
+ used[ours] = used[ref] = true
266
+ end
267
+ end
268
+ private :connector_pairs
269
+
270
+ def connector_distance(ours, ref)
271
+ return Float::INFINITY unless ours[:kind] == ref[:kind]
272
+
273
+ drifts = connector_delta(ours, ref).values_at(:source_delta, :target_delta)
274
+ return drifts.sum if drifts.min <= MATCH_DISTANCE
275
+ return drifts.sum if shape_distance(shift_primitive(ours), ref) <= MATCH_DISTANCE
276
+ return drifts.sum if drifts.sum <= [connector_length(ours), connector_length(ref)].max
277
+
278
+ Float::INFINITY
279
+ end
280
+ private :connector_distance
281
+
282
+ def connector_length(primitive)
283
+ primitive[:points].each_cons(2).sum { |first, last| point_distance(first, last) }
284
+ end
285
+ private :connector_length
286
+
287
+ def endpoint_distance(ours, ref)
288
+ return Float::INFINITY unless ours && ref
289
+
290
+ Math.hypot(ours[0] + @shift[0] - ref[0], ours[1] + @shift[1] - ref[1])
291
+ end
292
+ private :endpoint_distance
293
+ end
294
+ private_constant :ConnectorComparison
295
+ include ConnectorComparison
296
+
297
+ # EA emits triangle/diamond polygons and symmetric open V paths. The
298
+ # latter have their tip at the middle vertex, rather than at an end.
299
+ module ArrowheadComparison
300
+ def arrowheads(primitives)
301
+ primitives.select { |primitive| primitive[:kind] == :polygon || open_arrowhead?(primitive) }
302
+ end
303
+ private :arrowheads
304
+
305
+ def open_arrowhead?(primitive)
306
+ return false unless primitive[:kind] == :path && !primitive[:closed] && primitive[:points].size == 3
307
+
308
+ symmetric_wings?(*primitive[:points])
309
+ end
310
+ private :open_arrowhead?
311
+
312
+ def symmetric_wings?(first, tip, last)
313
+ left = point_distance(first, tip)
314
+ right = point_distance(last, tip)
315
+ base = point_distance(first, last)
316
+ # EA rounds each vertex to whole pixels, allowing up to sqrt(2)
317
+ # error per wing. Ordinary right-angle bends are not arrowheads.
318
+ (left - right).abs <= 2 * Math.sqrt(2) && base.positive? && base < [left, right].min
319
+ end
320
+ private :symmetric_wings?
321
+
322
+ def point_distance(first, last)
323
+ Math.hypot(first[0] - last[0], first[1] - last[1])
324
+ end
325
+ private :point_distance
326
+
327
+ def arrowhead_deltas(ours, ref, heads)
328
+ { source: 0, target: -1 }.filter_map do |name, index|
329
+ pair = arrowhead_pair(ours[:points][index], ref[:points][index], heads)
330
+ next unless pair
331
+
332
+ our_head, ref_head, anchor, tip = pair
333
+ { end: name, ours: our_head, reference: ref_head,
334
+ tip_delta: endpoint_distance(corresponding_tip(our_head, anchor, tip),
335
+ corresponding_tip(ref_head, anchor, tip)) }
336
+ end
337
+ end
338
+ private :arrowhead_deltas
339
+
340
+ def arrowhead_pair(our_point, ref_point, heads)
341
+ if (attachment = touching_arrowhead(@reference, ref_point))
342
+ ref, tip = attachment
343
+ ours = heads.find { |_, candidate| candidate.equal?(ref) }&.first
344
+ elsif (attachment = touching_arrowhead(@ours, our_point))
345
+ ours, tip = attachment
346
+ ref = heads.find { |candidate, _| candidate.equal?(ours) }&.last
347
+ else
348
+ return
349
+ end
350
+
351
+ [ours, ref, attachment.first, tip]
352
+ end
353
+ private :arrowhead_pair
354
+
355
+ # Removing each head's translation lets the reference tip identify
356
+ # its counterpart even when polygon vertices start at another corner
357
+ # or run in the opposite direction.
358
+ def corresponding_tip(head, anchor, index)
359
+ return unless head
360
+ return head[:points][1] if head[:kind] == :path
361
+
362
+ offset = centered_point(anchor[:points][index], anchor)
363
+ head[:points].min_by { |point| point_distance(centered_point(point, head), offset) }
364
+ end
365
+ private :corresponding_tip
366
+
367
+ def centered_point(point, primitive)
368
+ [point[0] - primitive[:centroid][0], point[1] - primitive[:centroid][1]]
369
+ end
370
+ private :centered_point
371
+
372
+ def touching_arrowhead(primitives, endpoint)
373
+ vertices = arrowheads(primitives).flat_map do |head|
374
+ indices = head[:kind] == :path ? [1] : head[:points].each_index
375
+ indices.map { |index| [head, index] }
376
+ end
377
+ nearest = vertices.min_by { |polygon, index| vertex_distance(polygon, index, endpoint) }
378
+ nearest if nearest && vertex_distance(*nearest, endpoint) <= EPSILON
379
+ end
380
+ private :touching_arrowhead
381
+
382
+ def vertex_distance(polygon, index, endpoint)
383
+ point = polygon[:points][index]
384
+ Math.hypot(point[0] - endpoint[0], point[1] - endpoint[1])
385
+ end
386
+ private :vertex_distance
387
+ end
388
+ private_constant :ArrowheadComparison
389
+ include ArrowheadComparison
390
+
226
391
  def parse(svg)
227
392
  PrimitiveParser.new.parse(svg)
228
393
  end
@@ -271,8 +436,9 @@ module Ea
271
436
  end
272
437
  private :dimension_delta
273
438
 
274
- def shape_pairs(ref_shapes = @reference.reject { |p| p[:kind] == :text })
275
- @ours.reject { |p| p[:kind] == :text }.filter_map do |ours|
439
+ def shape_pairs(ref_shapes = @reference.reject { |p| p[:kind] == :text },
440
+ our_shapes = @ours.reject { |p| p[:kind] == :text })
441
+ our_shapes.filter_map do |ours|
276
442
  shifted = shift_primitive(ours)
277
443
  ref = ref_shapes.min_by { |c| shape_distance(shifted, c) }
278
444
  next unless ref && shape_distance(shifted, ref) <= MATCH_DISTANCE
data/lib/ea/version.rb CHANGED
@@ -4,5 +4,5 @@ module Ea
4
4
  # Gem release version. Extracted to its own file so the
5
5
  # `gem-release` tool's `gem bump` can find and update it via the
6
6
  # standard `lib/<gem>/version.rb` convention.
7
- VERSION = "0.6.147"
7
+ VERSION = "0.6.148"
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.147
4
+ version: 0.6.148
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.