Erase All Scenes (Pages)

Shouldn’t this erase all the scenes in the model?

Sketchup.active_model.pages.each{|this_page|
    Sketchup.active_model.pages.erase(this_page)
}

It only seems to erase a random amount of scenes, but not all of them.

You should never erase the contents of an enumerator while you are doing an each over it. That breaks the enumerator and will cause erratic results. Instead, make an array from it and do your each over that. Arrays are a linear structure, so altering a member doesn’t affect access to the others. But many enumerators use various kinds of tree structures that break if you modify them while traversing.

2 Likes

Because it violates the number 1 rule of iterating collections.

  1. Never delete collection members whilst iterating the collection itself.

    (Reason: The iterator size changes and the index variable ends up skipping members. As the collection is contracted, the next member after the one deleted, gets it’s index decreased by one so it gets skipped on the next iteration.)


The trick is to make an array copy of the collection, and iterate the copy, so that the indices are always valid in the copy as the members are actually erased from the original collection.

So we use the Enumerable#to_a method, which comes from mixing in the Enumerable library module. (The API docs have a box at the top of each class that tells you what Ruby core libraries are included into the API class.)

pages = Sketchup.active_model.pages
pages.to_a.each { |this_page|
  pages.erase(this_page)
}

ADD: Even though you were not using the Ruby iterator (#each_with_index) that exposes the index Integer, Ruby itself is implemented in C, whose iterators do use an index variable.

3 Likes

Aha! That makes perfect sense @slbaumgartner and @DanRathbun. Thanks.

Academically, you’d hope iterating and Enumerable would NOT change the index under your feet, wouldn’t you?

I really don’t know Ruby, but in other languages if you duplicate an array of references, and try to delete the referenced objects via the duplicate array, aren’t the object still kept because they are needed for the original array?

The answer in other languages is to count backwards through the array. In Ruby, don’t you have array.reverse.each? Wouldn’t that solve the problem too?

Academically, … an Enumerator. It is an object that allows iterating a collection type instance object.

(Enumerable is a library mixin module that adds functionality to collection classes. It is also mixed into the core Enumerator class.)

Iterating normally does not normally change indices.

But normally it’s poor practice to delete the members as you iterate. Ie, you use the member values to do other things, including perhaps building another collection type object.

Some Ruby classes have special iterator methods that do delete members safely. The means by which they do vary from method to method. (Some delete instantly at the end of the block eval, others store up the items to be deleted, and do a batch delete after the last block iteration.)

Ex: Instant deleting …

Ex: Store and batch deleting …

With SketchUp Ruby coders, it is also a common practice to use the “store and batch delete” pattern.

pages = Sketchup.active_model.pages
# Doom all pages not for animation:
doomed = pages.find_all { |page| not page.include_in_animation? }
# doomed is already an array with it's own references, but
#  in the block, we are erasing from the C++ pages collection:
doomed.each { |page| pages.erase(page) }

… after this snippet, the doomed array member references are all invalid, and inspection will show them to be of class DeletedEntity.


Well “other languages” is just too broad a scope. Some are not 100% OOP languages.

With Ruby if we were talking pure core Ruby external to SketchUp, … and you had array a and b both referencing the same set of mutable instance objects (say Strings for example,) … then yes, the text object would not “go away” if deleted from array b. Array a would still be referencing those string objects. But the references would no longer be in array b.

The same would hold true for Ruby arrays within SketchUp.

However, we are talking about API Collection objects that are “thinly wrapped” C++ collections belonging to the C++ model object, that are only exposed to Ruby in a limited way.

When you tell the API to delete one of the members of the model’s collections, it will be done immediately on the C++ side. The Ruby iterator index variable can (and does) loose track.

An interator API method could handle deletion safely, if it knew before hand what was “on the menu”. But the #each iterator is a general purpose iterator method that must be defined so that the Enumerable library can be mixed in, and those library iterator methods work.

So it is not “cut and dry”, “one size fits all”. SketchUp embedded Ruby is a interesting situation. Core C++ objects are exposed to the Ruby API via definitions in C “extern” sections.

The answer in other languages is to count backwards through the array.

Yes this can work, in other languages and for non-API Ruby collection classes, but you must code the iteration yourself.

In Ruby, don’t you have array.reverse.each ?

Yes.

Wouldn’t that solve the problem too?

No. Because Array#reverse just returns another array with all the members positions reversed and the basic #each iterator would start at the 0 index, just as it would with the original array.

But … Ruby has added the #reverse_each iterator for class Array that actually will iterate in reverse order, and after yielding to the block, it internally checks the length of the array, and if it changed, it adjusts it’s indexing.
This can work if deleting members as you go. I’m not sure if it would work so well if the block is inserting members though.

You can have a look at the C code .... (click to expand)
               static VALUE
rb_ary_reverse_each(VALUE ary)
{
    long len;

    RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length);
    len = RARRAY_LEN(ary);
    while (len--) {
        long nlen;
        rb_yield(RARRAY_AREF(ary, len));
        nlen = RARRAY_LEN(ary);
        if (nlen < len) {
            len = nlen;
        }
    }
    return ary;
}
1 Like

Super interesting @DanRathbun as always (and a bit over my head too!) Thanks.