Duplicate Points in Array

I’m looking for a quick and efficient algorithm for checking if there are duplicate points in an array of points so I don’t try to create a face and get this error:

Error: #<ArgumentError: Duplicate points in array>

An example of an array with four points might be:

[0.0, 0.0, 97.125], [0.0, 0.0, 97.125], [0.0, 5.5, 97.125], [0.0, 5.5, 97.125]

Note that this array actually has two duplicate pairs, if even one duplicate pair exists I need to flag the array as such.

ary == ary.uniq

edited: no need for length…

john

2 Likes

uniq with no block doesn’t work for points since it uses the eql? comparison under the hood.

a = [
  Geom::Point3d.new(1,2,1),
  Geom::Point3d.new(1,2,1),
  Geom::Point3d.new(3,3,3),
  Geom::Point3d.new(6,6,1)
]
a.uniq
# => [Point3d(1, 2, 1), Point3d(1, 2, 1), Point3d(3, 3, 3), Point3d(6, 6, 1)]

If you have point objects you can temporarely convert them to array objects for the comparison:

a.uniq { |p| p.to_a }

Or shorter:

a.uniq(&:to_a)
2 Likes

Beware… Your title asks about points. Although the API allows 3 element arrays to be used as point coordinate data in many of it’s method arguments, they are not actually points objects.

a =[
  [0.0, 0.0, 97.125], [0.0, 0.0, 97.125], [0.0, 5.5, 97.125], [0.0, 5.5, 97.125]
]

Plain Ruby will see the first two members as the same because Array#uniq uses #eql? to compare members which for Array#eql? sees two arrays as equivalent if they contain the same exact core numeric data (ie, Float or Integer values.)

a == a.uniq
#=> false
a[0] == a[1]
#=> true

However, when the members are actual point objects the result is different …

a.map!{|e| Geom::Point3d.new(e) }
#=> [
#   Point3d(0, 0, 97.125), Point3d(0, 0, 97.125),
#   Point3d(0, 5.5, 97.125), Point3d(0, 5.5, 97.125)
# ]
a[0] == a[1]
#=> true, because the API overrode Geom::Point3d#==
a[0].eql?(a[1])
#=> false, because the API did not override Geom::Point3d#eql?
a == a.uniq
#=> true, because again Array#uniq is using Object#eql?
# ... which the API did not override for the Geom::Point3d class.

So, it might be best if you use a custom method to uniquify a list of points …

  def unique_point_array(*args)
    if args.size == 1 && args[0].is_a?(Array) &&
    args[0].all? {|arg| !arg.is_a?(Numeric) }
      args.flatten!
    end
    # Create a copy of the array mapped to points:
    ary = args.map do |obj|
      next obj if obj.is_a?(Geom::Point3d)
      next nil unless obj.respond_to?(:to_a)
      Geom::Point3d.new(obj.to_a[0..2]) # Array | Geom::Vector3d
    end
    ary.compact! # removes nil references
    ary.delete_if do |pt|
      # Compare pt against the remainder of the array, removing
      # the pt immediately each iteration if block returns true:
      ary[ary.index(pt)+1..-1].any? { |other|
        # Use API's Geom::Point3d#== to compare within tolerance:
        pt == other
      }
    end
  end

Now you still need to be sure that you pass a minimum of three points or an array of 3 points to the #add_face method.

ents = group.entities

Now, assume you have an array of mixed objects (numeric arrays, points or vectors):

ents.add_face(unique_point_array(array))

Or, if the objects were not in an array, you can pass as a list …

ents.add_face(
  unique_point_array(pt1,[0.0, 0.0, 97.125],vec2,pt3,[0.0, 5.5, 97.125])
)

AND … if you wish to pare it down more … and not allow a list of parameters, …
ie, require 1 array argument of arrays, points or vectors …

  # Takes only 1 Array argument of (arrays, points or vectors)
  def unique_point_array(args)
    # Create a copy of the array mapped to points:
    ary = args.select {|arg| arg.respond_to?(:to_a) }.map do |arg|
      Geom::Point3d.new(arg.to_a[0..2]) # Array | Point3d | Vector3d
    end
    ary.delete_if do |pt|
      ary[ary.index(pt)+1..-1].any? { |other| pt == other }
    end
  end

:bulb:

2 Likes