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 +4 -4
- data/lib/ea/svg/parity/differ.rb +169 -3
- data/lib/ea/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2395d5ef03b627d02de55c09f65eff28984a512919ed75b39d8b1cbe110dc20
|
|
4
|
+
data.tar.gz: ff25b771f4134f453882054a6921eba3e0ffbbb2fe07347292ffdb1ade43e9b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c0e61969712ad88fc8d9ccc9da3a034ce5877f880738271935a192eff7219d15927f609134b1f6d956f2abb15d9191492bddf14085372068b1b3bb9544d37fe
|
|
7
|
+
data.tar.gz: fa6306f9f66d3e3192c921127710d365f3a53a64fac9a3498011a9f7c0c632ccb04c8777a5da45bbc78386fc0ba8f8cad18f7c3e5d6fa9597bd4b147cb2391e9
|
data/lib/ea/svg/parity/differ.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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