Script to apply offset

I’m trying to create a script to apply offset to 2D shapes of undefined shape. I created a script in ruby ​​that “fakes” the offset, and is also limited to square and rectangular shapes.

model = Sketchup.active_model
model.start_operation("Inner Offset + Remove Outer Border", true)

selection = model.selection
group = selection[0]

unless group.is_a?(Sketchup::Group)
  UI.messagebox("Please select the group that contains the face before running the script.")
  model.abort_operation
  return
end

entities = group.entities

# Assume there is only one face in the group
face = entities.grep(Sketchup::Face).first
unless face
  UI.messagebox("No face found inside the group.")
  model.abort_operation
  return
end

# Define the offset distance (50 cm, for example)
offset_distance = 50.cm

# Identify the outer and inner loops
outer_loop = face.loops.find(&:outer?)
inner_loop = face.loops.find { |loop| !loop.outer? }

unless inner_loop
  UI.messagebox("Inner loop (hole) not found.")
  model.abort_operation
  return
end

# Get the vertices (points) of the inner loop
inner_points = inner_loop.vertices.map(&:position)

# Compute the bounding box of these points to expand by 50 cm
bbox = Geom::BoundingBox.new
inner_points.each { |pt| bbox.add(pt) }

min_x = bbox.min.x - offset_distance
min_y = bbox.min.y - offset_distance
max_x = bbox.max.x + offset_distance
max_y = bbox.max.y + offset_distance
z_plane = bbox.min.z  # Assuming all points lie in the same Z plane

# Construct the 4 points of a new, larger rectangle based on the bounding box
outer_points = [
  [min_x, min_y, z_plane],
  [max_x, min_y, z_plane],
  [max_x, max_y, z_plane],
  [min_x, max_y, z_plane]
]

# Store the edges of the outer loop for later removal
outer_edges = outer_loop.edges

# Erase the original face
face.erase!

# Remove the original outer loop edges
outer_edges.each do |edge|
  edge.erase! if edge.valid?
end

# Create the new outer face (larger) and the inner face (hole)
new_face = entities.add_face(outer_points)
hole_face = entities.add_face(inner_points)

# If the inner face was created successfully, erase it to leave the "hole"
hole_face.erase! if hole_face.valid?

model.commit_operation

I’ll leave two videos trying to better explain what I need. One shows the functionality of my script and the other shows what I want to optimize with the script.
Made by my script
What do I need to do.

Problem Description:

I have a flat face that contains an internal hole (an internal loop) and does not have a defined shape – it could be an irregular polygon, a circle, a triangle, etc. What I want to do is apply an offset from the internal hole, so that all the edges that form this hole are displaced outwards by 50 centimeters to the outer edges. In other words, I want to create a new external edge that is 50 cm away from the internal hole, removing the original external edge of the face.

My idea for solving this problem is:

1. Geometry Selection and Validation:

  • Select the group that contains the face with the hole.
  • Verify that the group actually contains a face and that this face has at least one internal loop (the hole).

2. Identify the Loops:

  • Identify the external loop (the original edge of the face) and the internal loop (the outline of the hole).
  • Confirm that the internal loop is present, as it will be the basis for the offset.

3. Offset Calculation:

  • From the inner loop, extract the vertices (points) that form the outline of the hole.
  • Calculate a new set of points that represent a polygon offset outwards by 50 centimeters from the hole.
  • This process should work for any face shape, even if it is irregular (not just based on a bounding box for rectangles, but applying an offset algorithm for polygons).

4.Removing the Original Geometry:

  • After generating the new set of offset points, delete the original face and remove the edges of the outer loop (the original edge of the face).

5. Rebuilding the New Face:

  • Create a new face using the points of the offset polygon as the new outer edge.
  • Recreate the inner hole (using the original points of the hole) on the new face, so that SketchUp understands that this area should remain empty.
  • If an internal face is automatically created, erase it to leave only the edges that define the hole.

6. End Goal:

  • Expected Result: Have a new face that has an external edge offset 50 cm outward from the internal hole contour, removing the original external edge.
    This operation should be valid for faces with undefined shapes, that is, it will work for any polygon, circle, triangle or irregular shape that contains an internal hole.

Post colorized code correctly in the forum and please edit your previous post.


I created a quick code (based on other one I also posted in a forum).
You will get a context menu “Face Around Hole”, after copy paste it to the Ruby Console.
To be able to use it you must install TIG Smart offset first!
Dezmo_FAH

module Dezmo
module FaceAroundHole

  @@loaded = false unless defined?(@@loaded)
  extend self
  
  def tig_smart_offset
    begin
      require 'TIG-Smart_offset.rb'
    rescue LoadError
      url = "https://sketchucation.com/pluginstore?pln=TIG_Smart_offset"
      UI.messagebox( "Please install TIG: Smart Offset v3.0\n\n" + url)
      return
    end
    true
  end

  def get_size
    return unless tig_smart_offset

    @offset = 50.mm unless @offset
    prompts = [
      "Offset"
    ]
    defaults = [
      @offset
    ]
    title = "Face Around Hole"
    inputbox(prompts, defaults, title)
  end
  
  def msg
    UI.messagebox("Face Around Hole :\n\nPlease Right click on a Group Contains one Face with one hole !")
    nil
  end
  
  def create_structure
    model = Sketchup.active_model
    ents = model.active_entities
    sel = model.selection
    return msg unless sel.single_object?
    return msg unless sel.first.is_a?(Sketchup::Group)
    group = sel.first
    group.make_unique
    entities = group.entities
    faces = entities.grep(Sketchup::Face)
    return msg unless faces.first
    return msg if faces.size > 1
    face = faces.first
    loops = face.loops
    return msg if loops.size != 2
    o_loop = face.outer_loop
    i_loop = face.loops.find { |loop| !loop.outer? }
    edges_to_erase = o_loop.edges
    vertices = i_loop.vertices
    input = get_size
    return unless input
    @offset = input.first
    norm = face.normal
    model.start_operation('Face Around Hole', true)
    entities.erase_entities(edges_to_erase)
    temp_face = entities.add_face(vertices)
    temp_face.reverse! unless temp_face.normal.samedirection?(norm)
    face_new = TIG::Smart_offset.new(temp_face, @offset)
    entities.erase_entities(temp_face)
    model.commit_operation
  end
  
  unless @@loaded
    cmd = UI::Command.new("Face Around Hole"){
      create_structure()
    }
    UI.add_context_menu_handler{|menu|
      menu.add_item(cmd) if Sketchup.active_model.selection.grep(Sketchup::Group).first
    }
    @@loaded = true
  end

end
end
2 Likes