SceneObserver.frameChange does not allow material changes to component instances

I have a plugin that makes changes to component transformations and materials when the scene is changed. If I run the update method from the Ruby console it works as expected - changing transformations and materials. However, I can’t figure out why it only changes transformations using the SceneObserver frameChange method.

Any help would be greatly appreciated. I’ve included the relevant code below.


class SceneObserver

  def initialize
    @curr_page = nil
  end 

  def frameChange(from_page, to_page, percentage)
    if percentage == 1.0 and to_page != @curr_page
      @curr_page = to_page
      MyModule.restore_data to_page
    end
  end

end

Module MyModule

  def self.initialize
    Sketchup::Pages.add_frame_change_observer(SceneObserver.new)
  end 

  def self.restore_data(page)
    materials = Sketchup.active_model.materials
    page.get_attributes('MyModule', 'Data').each {  |guid, trans, color|  
      ent = find_component guid
      next if ent.nil?
      ent.transformation = Geom::Transformation.new trans
      ent.material = materials.add guid if not color.nil? and ent.material.nil?
      ent.material = color.nil? ? nil : materials.find { |m| m.name == color }
    }
  end 

  def self.find_component(guid)
    Sketchup.active_model.entities.find { |e| 
      is_component?(e) and e.guid == guid 
    }
  end 

end

It’s hard to say, since the fragment you posted is incomplete.

One thing that I assume is a typo in transcribing the code to your post: there is no #get_attributes method on Page, only #get_attribute.

When is the ‘MyModule’ dictionary added to the Page, and what does its ‘Data’ attribute look like? Clearly it must be something that provides #each, such as an Array. And each element must be a triple of values, such as a 3-element Array. But beyond that I have no idea how the elements were loaded and whether they are always valid.

The line

ent.material = materials.add guid if not color.nil? and ent.material.nil?

seems odd. Could it be that the if statement causes this to be skipped when you run from the Console, but to fail when you run otherwise?

The statement in the topic title is untrue. I have done it, and so has SketchUp Team in an old example. (“Components that react to scene changes”.)


(1) if not color.nil? and ent.material.nil?

The above will only create a new material if color is not nil and material is nil.

(2) Do you really want to make materials with a huge name based upon a guid string ?


FYI, there was a fast C-side method added for v15+:
Sketchup::Model#find_entity_by_id
http://www.sketchup.com/intl/en/developer/docs/ourdoc/model#find_entity_by_id

Your find by guid method is doing more work that it need to. Ie, it is checking all entities.
It could be more efficient like:

 def self.find_component(guid)
   Sketchup.active_model.find_entity_by_id(guid)
 rescue NoMethodError # for older than v 15
   Sketchup.active_model.entities.grep(Sketchup::ComponentInstance).find {|e| 
     e.guid == guid 
   }
 end

Thanks for your replies. I’m quite frustrated with this and apologize for not making the problem clearer.

I’ve disabled my extension and then created a bare minimum plugin to only test color changes in the frameChange method. I’ve included the full test plugin code at end of this reply.

If I select a component in the model and run the following from the Ruby Console:

Test.change_color

Then the console outputs:

change_color start
#<Sketchup::Material:0x0000000048c128>
change_color end

With the component still selected and then change to another scene, then the console outputs:

frameChange start
change_color start
change_color end
frameChange end

Note that the material is NOT output between the change_color start/end. Indicating that the material is nil. So, for some reason, material changes are not workng inside the frameChange method.

Again. Thanks for your input.

Test.rb

require 'sketchup.rb'

module Test

  def self.initialize
    Sketchup::Pages.add_frame_change_observer(SceneObserver.new)
  end

  def self.change_color
    puts 'change_color start'
    c = Sketchup.active_model.selection.first
    c.material = '[0020_Red]'
    puts c.material unless c.material.nil?
    puts 'change_color end'
  end

  class SceneObserver

    def initialize
      @curr_page = nil
    end

    def frameChange(from_page, to_page, percentage)
      if percentage == 1.0 and to_page != @curr_page
        @curr_page = to_page
        puts 'frameChange start'
        Test.change_color
        puts 'frameChange end'
      end
    end

  end

end

unless file_loaded?(__FILE__)
  Test.initialize
  file_loaded(__FILE__)
end

I made a few minor changes, mainly to increase diagnostic output, but then it works. One important change is that you shouldn’t name your module Test. An (undocumented) module of that name already exists in SketchUp, and your modifications may collide with it. I don’t think the name collision had any effect in this case, but changed it anyway as a precaution.

One needs to set up a model before running this. You need at least two pages (so there can be a scene change) , a material named [0020_Red], and something to select.

In change_color, I added a test for whether the selection is nil. Other than that, I’m not sure why your version isn’t working for you.

module ColorChangeTest

  def self.initialize
    Sketchup::Pages.add_frame_change_observer(SceneObserver.new)
  end

  def self.change_color
    puts 'change_color start'
    c = Sketchup.active_model.selection.first
    puts "selection = #{c.inspect}"
    unless c.nil?
      c.material = '[0020_Red]'
      puts c.material.inspect
    else
      puts "no selection"
    end
    puts 'change_color end'
  end

  class SceneObserver

    def initialize
      @curr_page = nil
    end

    def frameChange(from_page, to_page, percentage)
      if percentage == 1.0 and to_page != @curr_page
        puts "frameChange: page changed"
        puts "curr = #{@curr_page.inspect}, from=#{from_page.inspect}, to=#{to_page.inspect}, pct=#{percentage}"
        @curr_page = to_page
        ColorChangeTest.change_color
        puts "frameChange end"
      end
  end

end

end
  unless file_loaded?(__FILE__)
  ColorChangeTest.initialize
  file_loaded(__FILE__)
end

Disabled all of my extensions and created a new model with two scenes and the color ‘[0020_Red]’. Replaced my test.rb with your ColorChangeTest code and still the same problem. ColorChangeTest.change_color works from the console but not when called from frameChange.

I’m using Sketchup 2015 Pro (15.3.331) 64 bit. What version of Sketchup are you using?

This is the output in the Ruby Console when I make a scene change:

frameChange: page changed
curr = #<Sketchup::Page:0x00000008061050>, from=nil, to=#<Sketchup::Page:0x00000008060560>, pct=1.0
change_color start
selection = #<Sketchup::ComponentInstance:0x00000008060dd0>
nil
change_color end
frameChange end

Can you show me the output from your console when your scene changes?

I’m using SU 2016 Pro. I just stumbled on what may be a key factor: the technique works only if there is a non-zero scene transition delay! If I either uncheck the box or decrease the delay to 0, it ceases to work. But even with an 0.1 second delay it works. This undocumented gotcha seems to me like a bug.

1 Like

Yes. Now it works. Never thought to check that setting.

Thanks.

By the way, you can test for the problem via the Page#transition_time method and can change the time via Page#transition_time=

Also, the model’s defaults (used when the above returns -1 transition time) are found in the active model’s OptionsProvider named ‘PageOptions’

Sketchup.active_model.options[‘PageOptions’].each_pair {|k,v| puts “#{k} => #{v}”}
ShowTransition => true
TransitionTime => 0.1

That will help too. Thanks.