After exploding a group at the root context, undo causes a bugsplat

I have code that explodes selected groups and components down to the lowest “leaf” group or component, leaving these lowest groups and components un-exploded. Think of a bunch of rectangular solid groups/components that represent board and panels, which are built-up into assemblies of nested groups and components, like furniture or buildings. I want to explode these assemblies to their most basic parts in one step so that they may be laid flat and nested for CNC manufacturing,

The following code works, but I’ve been trying to figure out exactly why a subsequent undo operation will bugsplat if there are groups at the root context level. If editing is within a group or component (read, the active_entities is a group or component and not the root model) and break_apart is called and then undone, all is fine. It’s the presence of a top-level group in the selection that cannot be reconstructed by SU during the undo operation.

I have done quite a bit of troubleshooting…

  • It happens in both the last release of 2023 and the new 2024 release
  • It happens with different files and different models
  • It happens when run from the ruby console in 2023 or 2024, and when run from Ruby Console+ and Ruby Code Editor in 2023
  • It happens when only the default plugins are loaded (tested in 2024 because I haven’t migrated my 2023 setup to 2024 yet)
  • Loading the “Bomb” extension from Smustard and exploding all groups and components also causes the undo crash

But, it does not happen when I have only components at the root level. To work around the crash, I added the following test and conversion to turn any groups into components right before the explode::

grp.to_component if grp.instance_of? Sketchup::Group

I’m familiar with issues about grouping entities in the root context and in differing contexts, but this is an “ungroup” operation I’m doing. I have read past posts where Colin was chasing ungroup crashes, so maybe there is some fundamental issue that has never been fixed in SU that has to do with the unique behavior of groups?

Looking for any insight or confirmation on this really odd crash behavior. Thanks.

def break_apart(ents)

  @explodable = []

  def recurse_entity(ent)
    if (ent.instance_of? Sketchup::ComponentInstance) or (ent.instance_of? Sketchup::Group)
      if (ent.definition.entities.grep(Sketchup::ComponentInstance).length + ent.definition.entities.grep(Sketchup::Group).length) != 0
        @explodable.push(ent)
        # TO DO: Need to deal with case of group instances being non-unique?
        ent.definition.entities.each{ |e|
          recurse_entity(e)
        }
      end
    end
  end

  ents.to_a.each{ |ent|
    recurse_entity(ent)
  }

  while !@explodable.empty?
    grp = @explodable.pop
    # NOTE: Had to add the group.to_component below to avoid an undo crash if the selection contains a group at the root model context 
    grp = grp.to_component if grp.instance_of? Sketchup::Group
    grp.explode if grp.valid?
  end

end

model = Sketchup.active_model
sel = model.selection
model.start_operation('Break Apart', true)
break_apart(sel)
model.commit_operation

Here’s a test file that I’ve used. Three “crates” with two of them being grouped. Comment-out the grp.to_component line, select the group, break_apart and then undo. You should see the crash.

nested components.skp (2.5 MB)

Edited: Found a minor issue when editing the tool from which this code came… Needed to reassign “grp” variable when converting to component. Didn’t change the crash behavior though :confused:

Edit: Removed typo after valid? that Dan pointed out. But, I left the style inconsistencies so Dan’s notes still have context.

Quick note:

  1. This is a typo in the last statement of the while loop. The method valid? does not take arguments.

  2. You should not define instance variables in the top-level ObjectSpace.

  3. It is kind of weird to have methods inside other methods.
    It is usually not necessary and not the best readable code.


On Win 10: I also see the crash (#7347) on SU2024.0 as given below …

Module wrapped (... click to expand ...)
module BergDesign

  extend self

  def break_apart(ents)

    @explodable = []

    ents.to_a.each { |ent|
      recurse_entity(ent)
    }

    while !@explodable.empty?
      grp = @explodable.pop
      # NOTE: Had to add the group.to_component below to avoid an undo crash
      #  if the selection contains a group at the root model context 
      #grp.to_component if grp.instance_of?(Sketchup::Group)
      grp.explode if grp.valid?
    end

  end

  def recurse_entity(ent)
    if (
      ent.instance_of?(Sketchup::ComponentInstance) or
      ent.instance_of?(Sketchup::Group)
    )
      dents = ent.definition.entities
      if dents.any? { |e| e.respond_to?(:definition) }
        @explodable.push(ent)
        # TO DO: Need to deal with case of group instances being non-unique?
        dents.each { |e| recurse_entity(e) if e.respond_to?(:definition) }
      end
    end
  end

  def go
    model = Sketchup.active_model
    sel = model.selection
    model.start_operation('Break Apart', true)
    break_apart(sel)
    model.commit_operation
  end

end

Weirdly, there is no crash if exploding manually using the same test file.

But then the manual explode only explodes one level deep.

Another test crash #7370 trying to pass an array copy of the selection and clearing the selection before calling the method. So, it does not have to do with the selection trying to hold onto invalid references.

ADD: And just for kicks, switched to classic graphics engine, Crash #7376

Does it prevent the bugsplat for you when you add the following before the explode?

grp = grp.to_component if grp.instance_of? Sketchup::Group

SU seems to have no problem cleaning up the changed model definitions from this addition.

Yes, in the attached code I posted above, see that I commented out that line in order to experience the bug.

Be aware that this (or similar crash) was reported in 2021 …

At the time, I asked Majid to open a bug report in the GitHub SketchUp API tracker but he did not.


RELATED:

I opened an API feature request to look into a batch form explode method …


PING:

@tt_su @ene_su @ChrisFullmer

Dear Dan,
Please forgive me for it. I don’t know what to do.

1 Like

Well then perhaps someone else will file the report.

Here’s an evolution of the code that behaves more like I originally intended, and interestingly, it doesn’t cause a bugsplat. My original methodology had the unintended consequence of altering the structure of component definitions because the bottom-up exploding would alter a component’s internals before the parent instance was exploded. Not what I was really striving for :confused:

The methodology below explodes in a top-down direction so that none of the component definitions are altered. Since this doesn’t case a bugsplat when exploding a group at the root context, there must be something about the bottom-up exploding that’s corrupting a group’s definition?

The code below is a little tighter too since there’s one recursive method that can be passed a single entity, and array or a selection.

module BergDesign

  extend self

  @exploded_ents = []

  def explode_to_leaf_groups(ents)
    ents.to_a.each do |ent|
      if ent.valid? and ((ent.instance_of? Sketchup::ComponentInstance) or (ent.instance_of? Sketchup::Group))
        if (ent.definition.entities.grep(Sketchup::ComponentInstance).length + ent.definition.entities.grep(Sketchup::Group).length) > 0
          children = ent.explode.grep(Sketchup::Drawingelement)
          @exploded_ents.concat(children) # add all elements from exploding
          explode_to_leaf_groups(children)
        end
      end
    end
  end

  def go
    model = Sketchup.active_model
    sel = model.selection
    return if sel.empty?
    
    model.start_operation("Break Apart", true)
    @exploded_ents.clear
    explode_to_leaf_groups(sel)
    sel.clear
    @exploded_ents.keep_if { |ent| ent.valid? and ent.is_a? Sketchup::Drawingelement } # remove elements that have been exploded and no longer exist
    sel.add(@exploded_ents)
    model.commit_operation
  end

end

I did try using @explodable.reverse! with the previous edition but still got a crash.