Problem with tables (nested arrays)

 Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection
pointsArray = [[8.171561352942838, -1.0813777661160615, 0.0], [8.197647551206401, -0.861607476847094, 0.0], [8.217824211396051, -0.6412160700980944, 0.0]]
pts = Array.new(pointsArray)
pts2 = []
pointsArray.each_index{|i| pts2[i] = pointsArray[i]}
pts.each_index{|i| pts[i].z = 3.0}
puts "pointsArray = #{pointsArray.inspect}"
puts "pts = #{pts.inspect}"
puts "pts2 = #{pts2.inspect}"

Hello. Can someone tell me where the error is? Changing a copy of a painting changes all the original tables. What for? Is that normal?

LE_GALL, Google search for Ruby Shallow, Deep, and Marshal. Which will send you down the correct path.

For example:

You can get around the shallow copy problem in a couple of ways. Here’s one:

pointsArray = [[8.171561352942838, -1.0813777661160615, 0.0], [8.197647551206401, -0.861607476847094, 0.0], [8.217824211396051, -0.6412160700980944, 0.0]]

pts2 = []
pointsArray.each{|pt|
  pts2 << [pt.x, pt.y, 30.0]
}

puts "pointsArray = #{pointsArray.inspect}"
puts "pts2 = #{pts2.inspect}"
nil





OK, thanks Williams

Good morning, William.
I’ve tried all the other possibilities of independent copies of one array in another without using the assignment = :
push, concat.
Only the method << works properly. I do not know if that is normal. On the other hand I do not understand why the creation of a new initialized table like:
Array.new (OldArray)
does not change pointer?
Yours

You get a new outer-most Array, but the elements of the new Array still “point” to the original objects. This is called a “shallow copy” and why you need to manually copy each element as well.

a1.equals?(a2) # ==> false
a1[0].equals?(a2[0]) # ==> true

Documentation for Ruby 2.2.4 used in SketchUp 2017.

1 Like

Okay Jim, that’s clear. Thank you

Because this does not make a copy, even of the outer array. Referring to the doc for Array::new

The second form creates a copy of the array passed as a parameter (the array is generated by calling #to_ary on the parameter).

… So, referring to Array#to_ary

Returns self.

EDIT: The Ruby docs are outdated and the Array constructor no longer uses #to_ary instead calls rb_ary_replace directly. So Array::new(other_array) will create a shallow copy.

To really produce a shallow copy you can do …

pts = Array.new(pointsArray.dup)

However the Array constructor call to Array::new is totally unnecessary as Object#dup has already produced a shallow copy.


Deep copy of an Array

The Array#map enumerator creates a new array (mapping the members of the new array to the result of executing an iterator block for each member of the old array.)

Using it you can iterate and duplicate the inner (nested) arrays …

pts = [
  [8.171561352942838, -1.0813777661160615, 0.0],
  [8.197647551206401, -0.861607476847094, 0.0],
  [8.217824211396051, -0.6412160700980944, 0.0]
]
pts2 = pts.map {|a| a.dup }

There is also a shortcut for the iteration block in Ruby 2.x …

pts2 = pts.map(&:dup)

Okay DanRathbun. Thank you for this elegant solution.

1 Like

Hi Dan. I’m confused then. So what does it do if not make a copy of the outer Array? a1 and a2 are not the same object.

I was just pointing out what the docs say it does. (Verified that Ruby docs are incorrect.)

Actually clicking on the toggle C source does not work for the :new method.
(I’d have to go look up the source at GitHub to see if the doc is incorrect.)

I also see that their #object_id is unique.

I would not be surprised if the ::new documentation was outdated, and reflects the situation before some later change to #to_ary.


However, using #dup is so easier.

Verified. It #to_ary was likely changed.

::new does not call #to_ary in order to make the copy … it now calls rb_ary_replace directly.
(This is from rb_ary_initialize which begins at L726.

The #to_ary method just returns itself if the receiver is of class Array.
If the receiver is a subclass of Array it then returns a copy by creating a new array and then calling rb_ary_replace to stuff the new array with contents of the old subclass object …

Here is how Ruby Facets implemented a “deep copy” method using Marshal:

module Kernel

  # Anything that can be marshaled can be copied in totality.
  #
  #   "ABC".deep_copy  #=> "ABC"
  #
  # NOTE: Not sure why we wouldn't just call this #copy, but
  # the term +deep_copy+ seems to be the common practive.
  def deep_copy
    Marshal::load(Marshal::dump(self))
  end

end
pts = []
pts= pointsArray.deep_copy

It also works very well.
Thanks Jim

Please beware that extensions modifying code Ruby API will be rejected from the Extension Warehouse.

(I would recommend not posting examples that demonstrate modifying the Ruby API as we often see this eventually make their way into extensions - which in turn we reject causing more work for developers.)

Yeah I didn’t think about it - no need to modify the Kernel module. The code is a one-liner:

new_object = Marshal::load(Marshal::dump(original_object))

Technically I posted a link and the forum did the rest. Is there a way to disable it?

1 Like

I don’t know… It appear to try to provide the extra box whenever possible. But I’m not sure exactly how that works.

@LE_GALL

Using Marshal to copy objects can be overkill if you have a well defined structure.

For instance, your example was just an array of 3 element arrays.

pts1 = [ [0,0,0], [1,1,0], [2,2,0] ]
pts2 = pts1.map { |a| a.dup.tap { |b| b[2] = 3 } }

puts pts1.inspect  #=> [[0, 0, 0], [1, 1, 0], [2, 2, 0]]
puts pts2.inspect  #=> [[0, 0, 3], [1, 1, 3], [2, 2, 3]]

Marshal does not know what to do with API class objects, so if an array contains things other than simple core class objects, this would not be a good way to go.

I recall there also was a deep_copy gem ?