Problem changing materials inside frame_change_observer

New to ruby scripting and posting here. I have many years of amateur experience programming autolisp for CAD. I’m having some success but still befuddled with modules, classes, etc.

I would like to automatically change all materials in the model when the scene changes. The code below works when pasted into the Ruby Console, including the undo. However when I insert into the frameChange callback in scenes_v2.rb (ref below), the materials are removed instead of being replaced, and the undo breaks.

Am I dealing with modules/classes incorrectly or are you not allowed to make changes to the model during an observer call-back?

Attached is a sketchup model and my modified scenes_v2.rb code -> _draftMatChanger.rb
(my added code has ### remark block ### starting line 133)
Using SU 2014 on Windows 10.

Expand for my code - works pasting into console
# credit for code comes from:
# Thomas Thomassen - tt_material_replacer\core.rb
# https://sketchucation.com/forums/viewtopic.php?f=323&t=26013
model = Sketchup.active_model
materials = model.materials
pages = model.pages
materialList = materials.map(&:name)
scenes_withMaterialOptions = []
@newmatname = "Stucco[3]"
@newmat = nil
model.materials.each{|e|@newmat=e if e.display_name==@newmatname}
@oldmatname = "Stucco[0]"
@oldmat = nil
model.materials.each{|e|@oldmat=e if e.display_name==@oldmatname}

if defined?(@newmat) and defined?(@oldmat)
    model.start_operation('Replace Materials')
    Sketchup.set_status_text('Replacing materials. Please wait...')

    model.entities.each { |e|
        if e.respond_to?(:material)
            e.material = @newmat if e.material == @oldmat
        end
        if e.respond_to?(:back_material)
            e.back_material = @newmat if e.back_material == @oldmat
        end
    }
    model.definitions.each { |d|
        next if d.image?
        d.entities.each { |e|
            if e.respond_to?(:material)
                e.material = @newmat if e.material == @oldmat
            end
            if e.respond_to?(:back_material)
                e.back_material = @newmat if e.back_material == @oldmat
            end
        }
    }
    Sketchup.set_status_text

    model.commit_operation
end # if

Original scenes_v2.rb from this post:
https://forums.sketchup.com/t/update-to-scenes-rb-example-from-the-old-api-blog/11514

Also special thanks to DanRathbun, no way I could have gotten this far without searching & finding his many posts on coding…

_materialChanger.skp (43.6 KB)

Do not skip the step of leaning to understand modules and classes. They are required for Ruby, and for deploying code in a shared environment such as SketchUp’s Ruby ObjectSpace.

Without modules, you code can clash with the code of other author’s. So ALL of your code must evaluate within YOUR unique module namespace. It is also suggested that within that module namespace, that you separate each of your plugins / extensions within a uniquely named submodule.

This will not help much as Lisp is a 100% functional language. Ruby is a dynamic, multi-paradigm, procedural, 100% object oriented language. And in addition, within SketchUp it becomes event-driven by the user. (Even a sequential block of code must be fired by the user in some way such as chosing a menu item or clicking a toolbar button.)

In order to really code well, you must first learn basic Ruby constructs.

SketchUp’s API adds extensions to the Ruby core modules and classes.

First off, that code example is written to ONLY work upon Dynamic Components. Not upon normal components, not upon normal groups, not upon primitive geometry.

The example model you have posted does not use any Dynamic Components, so the code example you used would not apply anyway.

Please be specific about what kind of objects you want this to work upon. IF it is ALL objects and ALL materials, then perhaps you need a different approach. It may be better to manipulate the materials collection rather than all the object’s material assignments.

In some observer callbacks the answer is NO (and the docs mention this.) Basically any “onChange” type of callback could go into a vicious endless loop if your code makes changes within it (resulting usually in a memory stack overflow.)

Yes your code is incorrect:

  • You have not changed the toplevel Author module namespace to your OWN uniquely named module.
  • You have not indicated in the file preamble what you changed.

Can you confirm that you are still working within SketchUp 2014 and what edition (Make or Pro) ?
(Looking at the file, it is saved as a version 14 model. The oldest version I have installed is 2016 so I could not exactly test in your situation.)

Also, you appear to have not yet installed the 14.1 maintenance update. Why?

See:

1 Like

@dan20047 In your redition posted above, I need you to fix something important.

You have a code block marked thus …

          #############################################
          # New code by Dan                           #
          #############################################

I need you to change that with YOUR fullname (or forum member name,) otherwise readers are likely to think that I (who am also named Dan,) inserted that code. I would never write such code with hardcoded material names.

Dan Rathbun,

Thank you for the previous response. I will respond in detail when I have a moment. For the code block I have deleted the attachment to avoid that problem. I’m sorry about that mistake and in the future will be sure to use my full name.

Best regards,

Dan Allen

Now from looking over the modified code:

  1. You still have the comment:

    # find the DCs that need redrawing, and redraw them.
    

    … but you are not dealing with Dynamic Components anymore, as it seems you’ve stripped out code that was meant for them.

    If you are going to modify code to this extent, don’t you think you should edit the code preamble comments that no longer applies ?

  2. You don’t use #each to find a member of a collection, you use #find

    model.materials.each{|e|@newmat=e if e.display_name==@newmatname}
    

    … should be …

    @newmat = model.materials.find { |e| e.display_name == @newmatname }
    

    However, you can simply use the API Materials collection’s [] method to get a material object reference by name. So the above statement reduces to …

    @newmat = model.materials[@newmatname]
    
  3. Ruby uses 2 space indents. Using more can cause excessive horizontal scrolling when posting code in the forums.

    It also looks bad in our code editors as we get extra incorrect ident guidelines with .rb files set to use 2 space indents. Ie …

  4. The following statement creates an array reference that is unused in later code …

            materialList = materials.map(&:name)
    
  5. Your code create this reference …

            materials = model.materials
    

    … but then afterward does not use it.


All this aside, I see that the materials have no associated texture. If his will generally be true, then perhaps it will be easier, simpler and quicker to just adjust the color of the material.

More to come as I examine your code …

Dan Rathburn,

I greatly appreciate the detailed response. In hindsight and with your comments I’ve learned how better to present my code and questions. I have much more code drafted towards my goal, but didn’t share it with the intent of simplifying my question - but I see that didn’t work. If you don’t mind, I think it would be better if I respond to your comments in detail and post the full code, later this week after I get some paying work done…

Thanks again,

Dan Allen

The major issue I see is that there is only 1 conditional block handling going from the “Red” scene to the “Yellow” scene. There needs to be at least 2 conditions based upon the value of the toPage argument.


We also have a secondary issue on that SketchUp entities use the nil value for material assignments to signify that no material is assigned or to clear a previous material assignment.
So checking an entity’s material property against a material reference that might be nil if it is undefined in the model might cause unexpected behavior.


I ran it and DID see the red faces become unassigned. I’m not sure why. It seems the scene transitions interfere with the assignments.

I had to wrap the operation in a delay timer block to get it to work.

_draftMatChanger_rev.rb (8.0 KB)

So basically the material assignments happen after the observer callback returns.

No that is okay … simplifying is better. I was able to find the solution. (See above.)