Help with Ruby Script / Show-Hide Tag-Folders / Tags

Hey everybody,

I am developing (with ALOT of help from all kinds of AI-helpers…) two small scripts that already are making my day. However, I am stuck at one part and the more I talk to Gemini about it, the more messed up it gets. I want to do something simple in the script: Hide and show certain tag folders and tags. This is what I got so far:

 # Define which folders and tags to show or hide
    folders_to_hide = ["3. EBENEN", "2. STANDORT"]
    tags_to_hide = ["!Einzelteile-Matrix", "!Hauptmodell", "#hidden", "#2d_graphik"]
    
    folders_to_show = ["@ SECCUTS"]
    tags_to_show = ["!Positionierungshilfe", "#kanten_gestrichelt", "#kanten_mitte"]

    # Hide specified folders
    folders_to_hide.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = false if folder
    end

    # Hide specified tags
    tags_to_hide.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = false if tag
    end
    
    # Show specified folders
    folders_to_show.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = true if folder
    end

    # Show specified tags
    tags_to_show.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = true if tag
    end

This gives me the following error: NoMethodError: undefined method layer_folders' for #<Sketchup::Model:0x00000002434e7588> (eval):57:in create_scenes’
(eval):157:in <module:SceneFromSection>' (eval):20:in

My assumption is that AI just hallucinated a function that doesn’t exist in the API but I am just getting started with this stuff, so it is rather overwhelming. Still, what I was able to get running in just a few days is super-promising.

Any ideas, of how to fix the problem?

Is layers_folders correct? Or is it: layer_folders = layers.folders

We have no idea how the

method looks like in your - also unknow

module.
So, so we cannot be sure if the

defined in your code as method or variable or at all or is there a syntax error. However, the error message is clearly stated there should be a problem related to this.

Using only “NI” :stuck_out_tongue_winking_eye: I would do something like this:

# method to collect all tag folders recursively
def all_folders(folders = [], root_folder = Sketchup.active_model.layers)
  root_folder.each_folder do |folder|
    folders<<folder
    all_folders(folders, folder)
  end
  folders
end

folders_to_hide = ["3. EBENEN", "2. STANDORT"]
tags_to_hide = ["!Einzelteile-Matrix", "!Hauptmodell", "#hidden", "#2d_graphik"]

folders_to_show = ["@ SECCUTS"]
tags_to_show = ["!Positionierungshilfe", "#kanten_gestrichelt", "#kanten_mitte"]

# Tag visibilities, itarate tags (layers)
Sketchup.active_model.layers.each do |layer|
  layer.visible = false if tags_to_hide.include?(layer.name)
  layer.visible = true if tags_to_show.include?(layer.name)
end

# For folder visibilities call the method above and iterate the result
all_folders.each do |folder|
  folder.visible = false if folders_to_hide.include?(folder.name)
  folder.visible = true if folders_to_show.include?(folder.name)
end

References:
The Layers #each method is used to iterate through all of the layers in the model.
The Layers #each_folder method is used to iterate through the folders that are direct children to the layer manager.
The LayerFolder #each_folder method is used to iterate through the folders that are direct children to the folder.

class Array - RDoc Documentation

I Thanks @dezmo → I didn’t realize the full code was needed to understand this, but as said - I am just getting started. I used to do a lot of PHP/MySQL stuff for about 10 years but Ruby is very new to me and I am happy to have some of the stuff already running. :slight_smile: It will help us a lot. Anyway - here is the full code:

# frozen_string_literal: true

# -----------------------------------------------------------------------------
#
# Create Scenes from Selected Section Planes
#
# Description:
# This script sets specific tag visibilities, then generates a new scene for
# each selected section plane, aligns the view, and finally sorts all scenes
# in the model alphabetically by name.
#
# This script is intended to be run directly from the SketchUp Ruby Console.
# -----------------------------------------------------------------------------

# Main module for the script
module SceneFromSection
  # Main method to define the functionality
  def self.create_scenes
    model = Sketchup.active_model
    selection = model.selection
    pages = model.pages
    layers = model.layers # Tag collection
    layer_folders = model.layer_folders # Tag Folder collection

    # --- Step 1: Get Selection and Validate ---
    section_planes = selection.grep(Sketchup::SectionPlane)

    if section_planes.empty?
      UI.messagebox('No section planes are selected. Please select at least one section plane and run the script again.')
      return
    end

    # --- Step 2: Preparation ---
    style_name = 'CallOut White Cut'
    styles = model.styles
    created_scenes = []

    callout_style = styles[style_name]

    if !callout_style
      UI.messagebox("Style '#{style_name}' not found.\n\nPlease import the required style into your model and run the script again.")
      return
    end
    
    styles.selected_style = callout_style
    model.active_view.camera.perspective = false

    model.start_operation('Create and Sort Scenes', true)
    
    # --- NEU: Step 3: Set Tag and Folder Visibility ---
    
    # Define which folders and tags to show or hide
    folders_to_hide = ["3. EBENEN", "2. STANDORT"]
    tags_to_hide = ["!Einzelteile-Matrix", "!Hauptmodell", "#hidden", "#2d_graphik", "@Sec CallOut Backdrop", "@Sec Raster-Schnitte"]
    
    folders_to_show = ["@ SECCUTS", "@ CALLOUTS", "@Sec CallOut Container"]
    tags_to_show = ["!Positionierungshilfe", "#kanten_gestrichelt", "#kanten_mitte"]

    # Hide specified folders
    folders_to_hide.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = false if folder
    end

    # Hide specified tags
    tags_to_hide.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = false if tag
    end
    
    # Show specified folders
    folders_to_show.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = true if folder
    end

    # Show specified tags
    tags_to_show.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = true if tag
    end


    # --- Step 4: Scene Creation ---
    section_planes.each do |sp|
      parent_object = sp.parent
      parent_name = parent_object.is_a?(Sketchup::Model) ? 'Model' : parent_object.name
      section_name = sp.name.empty? ? 'Section' : sp.name
      scene_name = "#{parent_name}_#{section_name}"
      
      scene_name.sub!(/^_Wrapper /, '@CallOut ')

      if pages.find { |p| p.name == scene_name }
        puts "Scene '#{scene_name}' already exists. Skipping."
        next
      end

      sp.activate

      view = model.active_view
      camera = view.camera
      plane_eq = sp.get_plane
      normal = Geom::Vector3d.new(plane_eq[0], plane_eq[1], plane_eq[2])
      target = sp.bounds.center
      eye = target.offset(normal.reverse)
      z_axis = Geom::Vector3d.new(0, 0, 1)
      up_vector = normal.parallel?(z_axis) ? Geom::Vector3d.new(0, 1, 0) : z_axis

      camera.set(eye, target, up_vector)
      camera.perspective = false
      view.zoom_extents

      new_scene = pages.add(scene_name)
      
      new_scene.use_camera = true
      new_scene.use_rendering_options = true
      new_scene.use_section_planes = true
      new_scene.use_style = true
      new_scene.use_hidden_layers = true # Ensures the tag visibility is saved

      created_scenes << scene_name
    end
    
    # --- Step 5: Sort All Scenes Alphabetically ---
    all_pages_array = pages.to_a
    if all_pages_array.length > 1
      sorted_pages = all_pages_array.sort_by { |p| p.name }
      
      sorted_pages.each_with_index do |page, index|
        pages.reorder(page, index)
      end
    end

    model.commit_operation

    # --- Step 6: Wrap Up ---
    if created_scenes.empty?
      UI.messagebox('No new scenes were created. This may be because scenes with the generated names already exist.')
    else
      message = "Successfully created #{created_scenes.length} scene(s):\n\n- #{created_scenes.join("\n- ")}\n\nTag visibilities have been set, and all scenes have been sorted alphabetically."
      UI.messagebox(message)
    end
  end

  # --- Run the script ---
  create_scenes
end

Basically what I do is I create a scene for each selected section plane. It worked fine until I added the stuff about hiding and showing certain tags and layers first. Ok - I will now try to understand your answer and see if I can incorparate it in here somehow… :slight_smile:

Replace this:


layers = model.layers
layer_folders = model.layer_folders

With:



layers = model.layers
layer_folders = layers.respond_to?(:folders) ? layers.folders : []
1 Like

You are right:

This method does not exist in SU Ruby API!
You need to create a method to collect the folders, e.g. like mine above, and add to your script:

That works! So the call .layer_folders doesn’t actually exist. Ok - this will safe so much time over here. I am reporting back to share what we are actually working on. :slight_smile: Basically we are solving the following problem: We love designing and detailing in Sketchup and creating Layoutfiles of the model has become increasingly easier with the last few Sketchup / Layout updates. What is a pain for us is that we sometimes need to create hundreds of drawings of components. Like a drawing of each furniture with section cuts, and so on. There is no easy way in Sketchup to “CallOut” a specific component so it can be detailed in Layout. Hence I came up with a new method for our team to get this done:

Step 1) select all the components you want to call out, execute “callout script” - this will place the components onto sort of a “stage” and add a wrapper component around each component AND tug it away in a hidden tag, so it won’t interfear with the rest of the model
Step 2) Go through each component, add Section planes where necessary (the section planes are added in the wrapper component) → run script “create scenes from section planes”. We where considering automating the process, but it is actually nicer this way because each component may require a section plane at a different position, or more then one, or none at all.
Step 3) Send to layout, distribute on the sheets, anotate… :slight_smile:

We like it! Thanks for you help guys!!!

1 Like

I will try that, too - the fix from @3DxJFD already helped get the script running again, but I haven’t actually tested it too much yet if it does what it is supposed to do. :joy:

This does not return all the folders in the model, only those that are direct children of the layer manager or empty Array. This will prevent the error message (in @napperkt code) but does not necessarily give all LayerFolder in a model. (The nested folders won’t be there)

2 Likes

That’s interesting. So it works for me because the folders I am looking at are top-level but your code makes more sense, because it is more robust down the road, if I want to hide subfolders. Got it! :slight_smile: Again - thank you so much for your help, you two!

3 Likes

Good morning. May I ask another question? My script is progressing nicely and I am getting where I want to get, but I ran into another weird wall. It’s very simple actually, but I do not see why I am getting unexpected results. I want to place a container called _Callouts-Container at 0,0,0 .

I thought “add_instance” might be the right function.

Instead the container is being placed at 5479,22,-118 and it’s rotated the wrong way. My initial assumption was the when placing the component it would honor the components axis - calculating from the internal axis point and rotation to the global axis and rotation. Am I using the wrong function?

Attached the testing file.

# Name: (TEST) Place _Callouts-Container with Rotation
# Intent: A minimal script to test the placement of the _Callouts-Container.
# It deletes all existing instances and creates a single, new one at a specific
# location and with a specific rotation.

require 'sketchup.rb'

# --- Get a handle to the active model ---
model = Sketchup.active_model

# --- Start an undoable operation ---
model.start_operation('Place _Callouts-Container', true)

# --- Define Constants for the container ---
container_def_name = "_Callouts-Container"

# 1. Define the location (Translation)
# Set the target position using .mm to ensure correct units.
target_point = Geom::Point3d.new(0.mm, 0.mm, 0.mm)
target_transformation = Geom::Transformation.translation(target_point)

# --- Find or Create the Component Definition ---
definitions = model.definitions
callouts_container_def = definitions[container_def_name] || definitions.add(container_def_name)

# --- Find and Delete ALL Existing Instances ---
# Get all instances of the container in the model's main entities collection.
instances_to_delete = model.entities.grep(Sketchup::ComponentInstance).select do |inst|
  !inst.deleted? && inst.definition == callouts_container_def
end

# If any instances were found, delete them all to ensure a clean slate.
unless instances_to_delete.empty?
  puts "Found and removed #{instances_to_delete.length} existing instance(s) of '#{container_def_name}'."
  model.entities.erase_entities(instances_to_delete)
end

# --- Create One New, Clean Instance ---
# Place a single, new instance using the combined transformation.
new_instance = model.entities.add_instance(callouts_container_def, target_transformation)
puts "Placed one new instance of '#{container_def_name}' at #{target_point} with a 90-degree Z-axis rotation."


# --- Commit the operation to make the changes permanent ---
model.commit_operation

# --- Provide feedback to the user ---
UI.messagebox("Process complete. A single, rotated '#{container_def_name}' has been placed at (0, 0, 0).")

CallOut Test 2025-11-15.skp (13.7 MB)

1 Like

AH! I figured it out! It’s the model, stoopid! My script works, after all!

The visible axis of the model is NOT the actual origin of the model but the active Drawing Axes. I figured it out by testing the script in an empty file. :rofl:

2 Likes

All right - I DO need help. I’ve been banging my head against this wall for the past day and I cannot get it fixed. I really need some help.

I am still at the camera problem. I cannot find a stable way of telling the script either:

Go to standard-view top, zoom at component _Callout_Container, thank you!

or

place a camera at x,y, z 0,-5000-,10000, LOOK DOWN! thank you

What I want to achieve is this: I have a few components selected (that MIGHT be nested in a different component). I run the script. It “calls out” all these components by copying them into the CallOut_Container (a component I created for this purpose). It then creates a scene for each of the components, so I can go into Layout from there.

There is a bit of magic to the side, like checking if the CallOut_Container is actually in the model, if it is placed correctly, if the necessary style is there, etc. all of that works. But the camera I cannot get to do what I want it to do. Could somebody please help?

Here is what I have so far…

# Name: Callout Selected Components (With Wrapper)
# Intent: Creates sectioned callouts for multiple selected components. Each callout is placed in a unique wrapper component for manual data entry, and remains linked to the original.

require 'sketchup.rb'

# 1) Get the selection and filter for component instances only
model = Sketchup.active_model
selection = model.selection.grep(Sketchup::ComponentInstance)
view = model.active_view
camera = view.camera
layers = model.layers
layer_folders = layers.respond_to?(:folders) ? layers.folders : []

if selection.empty?
  UI.messagebox("The selection must contain at least one component.")
else
  # --- SETUP: ONE-TIME OPERATIONS ---
  definitions = model.definitions
  tags = model.layers
  pages = model.pages
  container_def_name = "_Callouts-Container"

  # 2) NEW, INTEGRATED LOGIC: Ensure the definition exists and the instance is correctly placed.
  # --- PRE-CHECK: ENSURE THE COMPONENT DEFINITION EXISTS ---
  callouts_container_def = definitions[container_def_name]

  if callouts_container_def.nil?
    # If the definition is not found in the model, show an error and stop.
    UI.messagebox("Cannot find component _Callouts-Container in the model. Please insert it from the library and try again.")
  else
    # --- The component definition exists, so proceed with the main logic. ---
    model.start_operation('Callout Selected Components with Wrappers', true)

    # --- Calculate the final target transformation for the container ---
    drawing_axes_transformation = model.axes.transformation
    local_target_point = Geom::Point3d.new(0.mm, -5000.mm, 0.mm)
    local_translation = Geom::Transformation.translation(local_target_point)
    final_world_transformation = drawing_axes_transformation * local_translation

    # --- Find all existing instances in the model's root ---
    all_instances = model.entities.grep(Sketchup::ComponentInstance).select do |inst|
      !inst.deleted? && inst.definition == callouts_container_def
    end
    
    # Declare a variable for the final instance, needed later for the camera
    callouts_container_instance = nil

    # --- Apply conditional logic based on the number of instances found ---
    case all_instances.length
    when 0
      # Create a new instance
      callouts_container_instance = model.entities.add_instance(callouts_container_def, final_world_transformation)
    when 1
      # Move the existing instance
      instance_to_move = all_instances.first
      instance_to_move.transformation = final_world_transformation
      callouts_container_instance = instance_to_move
    else
      # Clean up all instances and create a new one
      model.entities.erase_entities(all_instances)
      callouts_container_instance = model.entities.add_instance(callouts_container_def, final_world_transformation)
    end
    
    # 3) Check if the required generic tags (layers) exist
    tag_names = [
      "@Sec CallOut Bd", "@Sec CallOut Bu", "@Sec CallOut Container",
      "@Sec CallOut G<", "@Sec CallOut G>", "@Sec CallOut R<",
      "@Sec CallOut R>"
    ]
    tag_names.each do |name|
      unless tags[name]
        new_tag = tags.add(name)
        pages.each { |page| page.set_visibility(new_tag, false) }
      end
    end

    # Lists for the final report
    created_count = 0
    skipped_names = []

    # Set Style
    style_name = 'CallOut Color'
    styles = model.styles
    created_scenes = []

    callout_style = styles[style_name]

    if !callout_style
      UI.messagebox("Style '#{style_name}' not found.\n\nPlease import the required style into your model and run the script again.")
      return
    end
    
    styles.selected_style = callout_style
    model.active_view.camera.perspective = false

    model.start_operation('Create and Sort Scenes', true)

  # --- KAMERA ANPASSEN ---

    # --- Define the DEFINITION name of the component to focus on ---
    callouts_container_definition_name = "_Callouts-Container"
#    callouts_container_instance = nil
    
    # Find the first component instance by its DEFINITION name
    model.entities.each do |entity|
      if entity.is_a?(Sketchup::ComponentInstance) && entity.definition.name == callouts_container_definition_name
        callouts_container_instance = entity
        break # Exit the loop once the first matching instance is found
      end
    end
    
    # Check if a matching component instance was found
    if callouts_container_instance.nil?
      UI.messagebox("Component with definition name '#{callouts_container_definition_name}' not found.")
    else
      # Get the active drawing axes
      drawing_axes = model.axes
    
      # Get the bounding box of the specific component instance that was found
      bounds = callouts_container_instance.bounds
    
      # Calculate the center of the bounding box
      center = bounds.center
    
      # Determine a suitable distance for the camera eye based on the component's size
      distance = bounds.diagonal * 1.5
    
      # Get the Z-axis (top) and Y-axis (up) from the active drawing axes
      top_vector = drawing_axes.zaxis
      up_vector = drawing_axes.yaxis
    
      # Calculate the new camera position by moving from the center along the active Z-axis
      eye = center.offset(top_vector, distance)
    
      # Set the camera eye, target, and up vector
      camera.set(eye, center, up_vector)
    
      # Set the camera to orthographic projection for a true 2D top view
      view.camera.perspective = true
    
      # Zoom to the selected component instance
      view.zoom(callouts_container_instance)
    end

    # --- NEU: Step 3: Set Tag and Folder Visibility ---
    
    # Define which folders and tags to show or hide
    folders_to_hide = ["3. EBENEN", "2. STANDORT"]
    tags_to_hide = ["!Einzelteile-Matrix", "!Hauptmodell", "#hidden", "#2d_graphik", "!CallOut Backdrop", "@Sec Raster-Schnitte"]
    
    folders_to_show = ["@ SECCUTS", "@ CALLOUTS", "!CallOut Container"]
    tags_to_show = ["!Positionierungshilfe", "#kanten_gestrichelt", "#kanten_mitte"]

    # Hide specified folders
    folders_to_hide.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = false if folder
    end

    # Hide specified tags
    tags_to_hide.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = false if tag
    end
    
    # Show specified folders
    folders_to_show.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = true if folder
    end

    # Show specified tags
    tags_to_show.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = true if tag
    end

    # --- MAIN LOOP: PROCESS EACH SELECTED COMPONENT ---
    selection.each do |original_instance|
      original_definition = original_instance.definition
      component_name = original_definition.name

      # --- MODIFIED LOGIC ---
      # Check if a callout already exists for this component by searching the wrapper components.
      callout_exists = false
      # IMPORTANT: We search the *definition* of the container, as that's where wrappers are added.
      callouts_container_def.entities.grep(Sketchup::ComponentInstance).each do |wrapper_instance|
        wrapper_definition = wrapper_instance.definition
        if wrapper_definition.entities.grep(Sketchup::ComponentInstance).any? { |inner_inst| inner_inst.definition == original_definition }
          callout_exists = true
          break # Found, no need to search further
        end
      end

      if callout_exists
        skipped_names << component_name
      else
        # --- No callout exists for this component, so create one inside a wrapper ---

        # 4) Create a new, unique wrapper component definition
        wrapper_def_name = "_Wrapper " + component_name
        unique_wrapper_def_name = definitions.unique_name(wrapper_def_name)
        wrapper_def = definitions.add(unique_wrapper_def_name)

        # 5) Place an instance of the original component into the new wrapper definition
        wrapper_def.entities.add_instance(original_definition, Geom::Transformation.new)

        # 6) Place an instance of the wrapper component inside the "_Callouts-Container"
        wrapper_instance = callouts_container_def.entities.add_instance(wrapper_def, Geom::Transformation.new)

        # 7) Create the specific tag for the callout
        callout_tag_name = "@CallOut " + component_name
        callout_tag = tags[callout_tag_name] || tags.add(callout_tag_name)
        pages.each { |page| page.set_visibility(callout_tag, false) }

        # 8) Assign the new tag to the WRAPPER instance
        wrapper_instance.layer = callout_tag

        # 9) Set up tag visibility and create the scene
        tags.each do |tag|
          if tag.name.start_with?("@CallOut")
            tag.visible = false
          end
        end
        callout_tag.visible = true

        scene_name = "@CallOut " + component_name
        new_scene = pages.add(scene_name)
        new_scene.update(2) if new_scene

        created_count += 1
      end
    end # End of the main loop

    # --- FINALIZE ---
    
    # --- NEW: ORGANIZE CALLOUT TAGS INTO A FOLDER ---
    # This block will find all tags starting with "@CallOut" and place them
    # into a folder named "@ CALLOUTS". This requires SketchUp 2021 or newer.
    if layers.respond_to?(:folders)
      folder_name = "@ CALLOUTS"
      # Find the folder or create it if it doesn't exist
      callouts_folder = layers.folders.find { |f| f.name == folder_name }
      unless callouts_folder
        callouts_folder = layers.add_folder(folder_name)
      end

      # If the folder was successfully found or created, move the tags
      if callouts_folder
        layers.each do |tag|
          if tag.name.start_with?("@CallOut")
            tag.folder = callouts_folder
          end
        end
      end
    end
    
    # Exit any open component editing contexts
    Sketchup.active_model.active_path = nil
    model.commit_operation

    # Final report to the user
    message = "Operation complete.\n"
    message += "Successfully created #{created_count} callout(s), each in a unique wrapper.\n"
    unless skipped_names.empty?
      message += "Skipped because they already exist (#{skipped_names.length}): #{skipped_names.join(', ')}"
    end

    UI.messagebox(message)
  end
end
[CallOut Test 2025-11-16.skp|attachment](upload://b2FpJMz2BCe8i9OhQ07RTi37HOG.skp) (13.7 MB)

I was also dabbeling with this, but it feels even more wrong:

    # --- NEW: KAMERA ANPASSEN (CAMERA ADJUSTMENT) ---
    # This block explicitly sets the camera's position to create a precise top-down view.
    # The rotation has been adjusted by changing the "up_vector".

    puts "Setting camera to a fixed, rotated position."
    view = model.active_view
    camera = view.camera

    # 1. Ensure the view is in Parallel Projection (Orthographic).
    camera.perspective = false

    # 2. Define the camera vectors.
    target_point = Geom::Point3d.new(0.mm, -2500.mm, 0.mm)
    eye_point = Geom::Point3d.new(0.mm, -2500.mm, 5000.mm)
    
    # FIXED: The "up" direction for the camera has been changed.
    # By using the red axis (1,0,0) as the up vector instead of the green axis (0,1,0),
    # the camera view is rotated by 90 degrees.
    up_vector = Geom::Vector3d.new(1, 0, 0)

    # 3. Set the camera.
    camera.set(eye_point, target_point, up_vector)
    
    # 4. Adjust the zoom level for consistency.
 #   view.zoom_extents
#    view.camera.height = 2000.mm # A smaller number zooms in. Adjust as needed.

Here is my testing file and a screencast of what is happening at the moment…
CallOut Test 2025-11-16.skp (13.7 MB)

model = Sketchup.active_model
axes = model.axes
view = model.active_view
target = Geom::Point3d.new(0, -5, 10) # example
dir_from_bottom_to_top = axes.zaxis

# distance does not really matter now, we will zoom later
distance_e_t = 10 
eye = target.offset(dir_from_bottom_to_top, distance_e_t)
up = axes.yaxis

# craate camera object with Paralell projection
new_camera = Sketchup::Camera.new(eye, target, up, false)

# You can inluence the "zoom" for Paralell projection with
# the height of the camera, e.g.:
# new_camera.height = 100, 
# but more easy to zoom to entity(es)

# Set the camera of view to the new camara
view.camera = new_camera

# Alternatively You can also set the view camera directly
# without creating new camara object:
# view.camera.set(eye, target, up)
# view.camera.perspective = false

# Zoom to the container(s) 
view.zoom( component_Callout_Container )

Class: Sketchup::Camera — SketchUp Ruby API Documentation

View l#zoom instance method

1 Like

Good morning @dezmo. Thanks for your help. I have integrated this (haven’t cleaned out your comments yet, will do that in the end). It’s running but still giving me a similar weird behavior. What’s strange is, that the script is behaving differently depending on what level of object I have selected. If I select an object from within the group (nested) the camera will zoom be placed bottom up and not zoom on the container. If I select an object that is not nested the camera will be placed top-down and zoom to the container as expected. My guess right now is that the script does not understand how to pick the container correctly as it is still thinks it is in the nested group or something. Am I on the right track? I made a screencast to demonstrate it.

# Name: Callout Selected Components (With Wrapper)
# Intent: Creates sectioned callouts for multiple selected components. Each callout is placed in a unique wrapper component for manual data entry, and remains linked to the original.

require 'sketchup.rb'

# 1) Get the selection and filter for component instances only
model = Sketchup.active_model
selection = model.selection.grep(Sketchup::ComponentInstance)
view = model.active_view
camera = view.camera
layers = model.layers
layer_folders = layers.respond_to?(:folders) ? layers.folders : []

if selection.empty?
  UI.messagebox("The selection must contain at least one component.")
else
  # --- SETUP: ONE-TIME OPERATIONS ---
  definitions = model.definitions
  tags = model.layers
  pages = model.pages
  container_def_name = "_Callouts-Container"

  # 2) NEW, INTEGRATED LOGIC: Ensure the definition exists and the instance is correctly placed.
  # --- PRE-CHECK: ENSURE THE COMPONENT DEFINITION EXISTS ---
  callouts_container_def = definitions[container_def_name]

  if callouts_container_def.nil?
    # If the definition is not found in the model, show an error and stop.
    UI.messagebox("Cannot find component _Callouts-Container in the model. Please insert it from the library and try again.")
  else
    # --- The component definition exists, so proceed with the main logic. ---
    model.start_operation('Callout Selected Components with Wrappers', true)

    # --- Calculate the final target transformation for the container ---
    drawing_axes_transformation = model.axes.transformation
    local_target_point = Geom::Point3d.new(0.mm, -5000.mm, 0.mm)
    local_translation = Geom::Transformation.translation(local_target_point)
    final_world_transformation = drawing_axes_transformation * local_translation

    # --- Find all existing instances in the model's root ---
    all_instances = model.entities.grep(Sketchup::ComponentInstance).select do |inst|
      !inst.deleted? && inst.definition == callouts_container_def
    end
    
    # Declare a variable for the final instance, needed later for the camera
    callouts_container_instance = nil

    # --- Apply conditional logic based on the number of instances found ---
    case all_instances.length
    when 0
      # Create a new instance
      callouts_container_instance = model.entities.add_instance(callouts_container_def, final_world_transformation)
    when 1
      # Move the existing instance
      instance_to_move = all_instances.first
      instance_to_move.transformation = final_world_transformation
      callouts_container_instance = instance_to_move
    else
      # Clean up all instances and create a new one
      model.entities.erase_entities(all_instances)
      callouts_container_instance = model.entities.add_instance(callouts_container_def, final_world_transformation)
    end
    
    # 3) Check if the required generic tags (layers) exist
    tag_names = [
      "@Sec CallOut Bd", "@Sec CallOut Bu", "@Sec CallOut Container",
      "@Sec CallOut G<", "@Sec CallOut G>", "@Sec CallOut R<",
      "@Sec CallOut R>"
    ]
    tag_names.each do |name|
      unless tags[name]
        new_tag = tags.add(name)
        pages.each { |page| page.set_visibility(new_tag, false) }
      end
    end

    # Lists for the final report
    created_count = 0
    skipped_names = []

    # Set Style
    style_name = 'CallOut Color'
    styles = model.styles
    created_scenes = []

    callout_style = styles[style_name]

    if !callout_style
      UI.messagebox("Style '#{style_name}' not found.\n\nPlease import the required style into your model and run the script again.")
      return
    end
    
    styles.selected_style = callout_style
    model.active_view.camera.perspective = false

    model.start_operation('Create and Sort Scenes', true)

  # --- KAMERA ANPASSEN ---

model = Sketchup.active_model
axes = model.axes
view = model.active_view
target = Geom::Point3d.new(2000, -5000, 5000) # example
dir_from_top_to_bottom = axes.zaxis

# distance does not really matter now, we will zoom later
distance_e_t = 10 
eye = target.offset(dir_from_top_to_bottom, distance_e_t)
up = axes.yaxis

# craate camera object without Paralell projection
new_camera = Sketchup::Camera.new(eye, target, up, true)

# You can inluence the "zoom" for Paralell projection with
# the height of the camera, e.g.:
# new_camera.height = 100, 
# but more easy to zoom to entity(es)

# Set the camera of view to the new camara
view.camera = new_camera

# Alternatively You can also set the view camera directly
# without creating new camara object:
# view.camera.set(eye, target, up)
# view.camera.perspective = false

# Zoom to the container(s) 
view.zoom( callouts_container_instance )

    # --- NEU: Step 3: Set Tag and Folder Visibility ---
    
    # Define which folders and tags to show or hide
    folders_to_hide = ["3. EBENEN", "2. STANDORT"]
    tags_to_hide = ["!Einzelteile-Matrix", "!Hauptmodell", "#hidden", "#2d_graphik", "!CallOut Backdrop", "@Sec Raster-Schnitte"]
    
    folders_to_show = ["@ SECCUTS", "@ CALLOUTS", "!CallOut Container"]
    tags_to_show = ["!Positionierungshilfe", "#kanten_gestrichelt", "#kanten_mitte"]

    # Hide specified folders
    folders_to_hide.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = false if folder
    end

    # Hide specified tags
    tags_to_hide.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = false if tag
    end
    
    # Show specified folders
    folders_to_show.each do |folder_name|
      folder = layer_folders.find { |f| f.name == folder_name }
      folder.visible = true if folder
    end

    # Show specified tags
    tags_to_show.each do |tag_name|
      tag = layers.find { |l| l.name == tag_name }
      tag.visible = true if tag
    end

    # --- MAIN LOOP: PROCESS EACH SELECTED COMPONENT ---
    selection.each do |original_instance|
      original_definition = original_instance.definition
      component_name = original_definition.name

      # --- MODIFIED LOGIC ---
      # Check if a callout already exists for this component by searching the wrapper components.
      callout_exists = false
      # IMPORTANT: We search the *definition* of the container, as that's where wrappers are added.
      callouts_container_def.entities.grep(Sketchup::ComponentInstance).each do |wrapper_instance|
        wrapper_definition = wrapper_instance.definition
        if wrapper_definition.entities.grep(Sketchup::ComponentInstance).any? { |inner_inst| inner_inst.definition == original_definition }
          callout_exists = true
          break # Found, no need to search further
        end
      end

      if callout_exists
        skipped_names << component_name
      else
        # --- No callout exists for this component, so create one inside a wrapper ---

        # 4) Create a new, unique wrapper component definition
        wrapper_def_name = "_Wrapper " + component_name
        unique_wrapper_def_name = definitions.unique_name(wrapper_def_name)
        wrapper_def = definitions.add(unique_wrapper_def_name)

        # 5) Place an instance of the original component into the new wrapper definition
        wrapper_def.entities.add_instance(original_definition, Geom::Transformation.new)

        # 6) Place an instance of the wrapper component inside the "_Callouts-Container"
        wrapper_instance = callouts_container_def.entities.add_instance(wrapper_def, Geom::Transformation.new)

        # 7) Create the specific tag for the callout
        callout_tag_name = "@CallOut " + component_name
        callout_tag = tags[callout_tag_name] || tags.add(callout_tag_name)
        pages.each { |page| page.set_visibility(callout_tag, false) }

        # 8) Assign the new tag to the WRAPPER instance
        wrapper_instance.layer = callout_tag

        # 9) Set up tag visibility and create the scene
        tags.each do |tag|
          if tag.name.start_with?("@CallOut")
            tag.visible = false
          end
        end
        callout_tag.visible = true

        scene_name = "@CallOut " + component_name
        new_scene = pages.add(scene_name)
        new_scene.update(2) if new_scene

        created_count += 1
      end
    end # End of the main loop

    # --- FINALIZE ---
    
    # --- NEW: ORGANIZE CALLOUT TAGS INTO A FOLDER ---
    # This block will find all tags starting with "@CallOut" and place them
    # into a folder named "@ CALLOUTS". This requires SketchUp 2021 or newer.
    if layers.respond_to?(:folders)
      folder_name = "@ CALLOUTS"
      # Find the folder or create it if it doesn't exist
      callouts_folder = layers.folders.find { |f| f.name == folder_name }
      unless callouts_folder
        callouts_folder = layers.add_folder(folder_name)
      end

      # If the folder was successfully found or created, move the tags
      if callouts_folder
        layers.each do |tag|
          if tag.name.start_with?("@CallOut")
            tag.folder = callouts_folder
          end
        end
      end
    end
    
    # Exit any open component editing contexts
    Sketchup.active_model.active_path = nil
    model.commit_operation

    # Final report to the user
    message = "Operation complete.\n"
    message += "Successfully created #{created_count} callout(s), each in a unique wrapper.\n"
    unless skipped_names.empty?
      message += "Skipped because they already exist (#{skipped_names.length}): #{skipped_names.join(', ')}"
    end
  end
end

Possibly:

model.active_path = nil

Here:

if selection.empty?
  UI.messagebox("The selection must contain at least one component.")
else

  model.active_path = nil
  
  # --- SETUP: ONE-TIME OPERATIONS ---

Would you provide another, dumber, test version? I looked at your examples but it wasn’t clear to me that the ‘stage’ (callouts-container) was at the location you wanted it to be. And, the axes of the callouts container is the location you want the copy of the component-to-be-called-out to be placed at?

Well, yes, you copy-pasted it to your (sorry to say, messy) script to a random position.
The Model #axes method returns the drawing axes for the model. This means that it gives the axes that are in the current editing context.
In the current editing context, of course, there may be a different axes (transformation) than the model at its root. This must always be taken into account.
If you had put my code after you had jumped out of all nested contexts - model.active_path = nil - you might have achieved a better result.
As @3DxJFD pointed out, this jump may need to happen much erlief in your code.


There are other “interesting things” in your code.

It’s long and unfollowable, at least I’ve given up trying to figure out exactly what you’re trying to achieve.

It’s much better to break it into smaller parts (you can also use methods like def fooo... end) and run it that way. When one part goes well, you can do the next one. It’s much easier to debug the parts.


In line 32 of the code there is a model.start_operation, but instead of closing it properly with model.commit operation`, you "opened a new one at line 96.

1 Like

Hey @dezmo . Thank you so much for your patience. I can imagine the “roll eye” this must cause for an expert like you. I am at a rather proficient level, when it comes to Sketchup / Layout but Ruby… Oh boy. :slight_smile:

I will try to incorporate this, but your answer actually rubs a very interesting point that I am trying to figure out. (Once I am done, I will sum up this project by showing how it will work and why we actually took the plunge to try automate it. It might be interesting to some).

Anyway - while testing earlier versions of the script in a variety of files I realized that some of them don’t actually have the model axis where I thought it would be. Somebody must have set a drawing axis to a different location many years ago and we iterated from that so there are like a million files that deriviated from that “mistake”. So I was trying to be smart in this script by saying: let’s work with the drawing axis instead. What I meant was the “global/root” drawing axis but if I read your comment correctly “drawing axis” means the drawing axis in the currently active group/component? Can I somehow reference to the global/root drawing axis when “talking” to the camera?

I will try to tidy up the script. I am a bit afraid of touching it because everything works except the camera setup that I want to call before creating a scene.

(You could stop reading here, but if you’re interested here is what the script is supposed to do)

Basically what I am doing is this:

  • I check if components where selected when the script was triggered
  • I remember all the selected components
  • I check if the component “_Callouts-Container” is defined in the file
  • I check if a certain style is in the model, that I need for the scenes
  • I make sure that the script doesn’t get confused by making sure that only ONE instance of _Callouts-Container is in the root of the model. If there is none, it adds it to the desired position, if there is more then one all are deleted and one is added to the desired position, if there is only one I check if it is at the desired position and move it there if not.
  • After that I go through all the selected components and add them to the “_Callouts-Container”. I try to make sure to check if they are already there - if so - I skip them. In that process I “wrap” them each into a unique wrapper-component, because I will later on add annotations, sections cuts, etc. inside these wrappers. I create a tag for each component and add it to the assigned wrappers. I make sure that all the tags of other called out components are hidden, set the camera (ha) and create a scene. This way I get (in theory) a clean “shot” of every single component.

That’s (in theory) “it”. There is some more stuff happening - some more specialized tags and tag-folders are hidden or unhidden based on what makes sense for our process. But in the end that’s the idea. I have many components at random positions in the model. I need a isolated scene for each, I want them all in the same spot for further processing, I want it with one click… :slight_smile:

A Global axes is always the same and cannot be changed. Can be referenced with a constants: ORIGIN, X_AXIS, Y_AXIS, Z_AXIS
I drew arrows there.
In the first image, the user did not change the axes, so the model/local/editing axes are the same as the global.
In the secont image, the user changed the axes, so the model/local/editing axes are different.
Similar in the last image where we are in editing context, so the model/local/editing axes are according to the context.

(BTW. on your example model root you have different axes than the Global.)


Understanding and mastering transformations is one of the most difficult tasks when learning the SU Ruby API. Use of the transformations requires a knowledge of general geometrical transformations in 3 dimensions.
You need to pay attention to what coordinate system the methods described in the API expect, and according to which one they return their results. Combining the transformations also need an attention, the order of the transformations are matters.
You need to practice and experiment a lot.

View Parts | SketchUp Extension Warehouse