Incorrect calculation of point distance after scaling

This is a continuation of my grooving tool coding attempts posted here: "Points are no planar" error message when creating a face - #8 by KScher
I would like to ask you to solve another problem that I am facing and I don’t know how to solve it. The tool code works well on unscaled objects. But if I scale the group, the face creation points are calculated incorrectly. How can this be solved for scalable objects?

class GrooveTool
	
	def initialize
		@selected_face = nil
		@selected_edge = nil
		@transformations = []
		@step = 1
		update_status_text
	end
	
	def activate
		reset_tool
	end
	
	def deactivate(view)
		view.invalidate
	end
	
	def onMouseMove(flags, x, y, view)
		ph = view.pick_helper
		ph.do_pick(x, y)
		path = ph.path_at(0)
		
		if path
			last_entity = path.last
			if @step == 1 && last_entity.is_a?(Sketchup::Face)
				@selected_face = last_entity
				@transformations = compute_transformation(path)
				view.invalidate
				elsif @step == 2 && last_entity.is_a?(Sketchup::Edge)
				@selected_edge = last_entity
				@transformations = compute_transformation(path)
				view.invalidate
			end
		end
	end
	
	def onLButtonDown(flags, x, y, view)
		if @step == 1 && @selected_face
			@step = 2
			update_status_text
			elsif @step == 2 && @selected_edge
			prompt_for_groove_parameters
			reset_tool
		end
		view.invalidate
	end
	
	def draw(view)
		draw_highlight(view)
	end
	
	private
	
	def reset_tool
		@selected_face = nil
		@selected_edge = nil
		@transformations = []
		@step = 1
		update_status_text
	end
	
	def update_status_text
		Sketchup.status_text = case @step
			when 1
			"Оберіть площину"
			when 2
			"Оберіть край на площині"
		end
	end
	
	def prompt_for_groove_parameters
		prompts = ["Groove width (mm)", "Groove depth (mm)", "Groove offset (mm)"]
		defaults = ["10", "5", "50"]
		input = UI.inputbox(prompts, defaults, "Setting groove")
		
		if input
			@groove_width, @groove_depth, @groove_offset = input.map { |value| value.to_f / 25.4 }
			create_groove
		end
	end
	
	def create_groove
		return UI.messagebox("Необхідно виділити як площину, так і край!") if @selected_face.nil? || @selected_edge.nil?
		
		model = Sketchup.active_model
		points = get_groove_points()
		
		begin
			model.start_operation("Create Groove", true)
			groove_face = @selected_face.parent.entities.add_face(points)
			groove_face.pushpull(-@groove_depth) if groove_face && groove_face.valid?
			rescue => e
			UI.messagebox("Error creating groove: #{e.message}")
			ensure
			model.commit_operation
		end
	end
	
	def get_groove_points
		edge_vector = @selected_edge.line[1].normalize
		offset_vector = @selected_face.normal * edge_vector
		
		if @groove_offset > 0
			offset_vector.length = @groove_offset
			
			test_point = @selected_edge.start.position.offset(offset_vector)
			if @selected_face.classify_point(test_point) == Sketchup::Face::PointOutside
				offset_vector.reverse!
			end
			
			offset_start_point = @selected_edge.start.position.offset(offset_vector)
			offset_end_point = @selected_edge.end.position.offset(offset_vector)
			
			offset_vector.length = @groove_width
			width_start_point = offset_start_point.offset(offset_vector)
			width_end_point = offset_end_point.offset(offset_vector)
			else
			offset_start_point = @selected_edge.start.position
			offset_end_point = @selected_edge.end.position
			
			width_vector = @selected_face.normal * edge_vector
			width_vector.length = @groove_width
			
			test_width_point = offset_start_point.offset(width_vector)
			if @selected_face.classify_point(test_width_point) == Sketchup::Face::PointOutside
				width_vector.reverse!
			end
			
			width_start_point = offset_start_point.offset(width_vector)
			width_end_point = offset_end_point.offset(width_vector)
		end
		
		points = [
			offset_start_point, offset_end_point, width_end_point, width_start_point
		]
		
		return points
	end
	
	def compute_transformation(path)
		transformation = Geom::Transformation.new
		path.each do |entity|
			if entity.respond_to?(:transformation)
				transformation *= entity.transformation
			end
		end
		transformation
	end
	
	def draw_highlight(view)
		if @selected_face
			points = @selected_face.outer_loop.vertices.map(&:position)
			transformed_points = points.map { |point| point.transform(@transformations) } if @transformations.is_a?(Geom::Transformation)
			view.drawing_color = 'yellow'
			view.draw(GL_POLYGON, transformed_points)
		end
		
		if @selected_edge
			points = @selected_edge.vertices.map(&:position)
			transformed_points = points.map { |point| point.transform(@transformations) } if @transformations.is_a?(Geom::Transformation)
			view.line_width = 5
			view.drawing_color = 'blue'
			view.draw(GL_LINES, transformed_points)
		end
	end
	
end

Sketchup.active_model.select_tool(GrooveTool.new)

Actually, thinking about it, … it is acting as intended. What you are doing is drawing the groove in the definition’s unscaled entities collection. (I.e., instances do not have an entities collection.)

So, the groove is really drawn 50mm from the second selected edge. But then that instance is scaled such that everything in it’s red axis is stretched by 1/5. So, in the instance the offset is 60mm and the groove width will be 12mm.

Yes, I understand that. But I’m looking for a way to update the scaling data. The groove should always be built according to the parameters entered, regardless of the scaling of the parent object.

Then would you make the parent unique and scale its definition as per normal as per materials?

Thanks for the reply, but it won’t fix it. I often use groups that are already unique.

Silly question have you tried using components instead of groups? I have found group scaling to be problematic where a component works fine…

You need to consider scale factor at the direction of offset vector. (More in a code)

The PickHelper #transformation_at method already give combined transformation of all groups, components and images in the pick path. You can use that instead.

Also need to check if the edge is part of the selected face boundaries, to ensure the same context. You already have a @transformations calculated, do not need to compute again. The view.invalidate can be moved out of condition since it is used in both cases.

“We” do not like that!
Use String #to_l method. See more notes in code.
__
The modified, commented code:

class GrooveToolA
  
  def initialize
    @selected_face = nil
    @selected_edge = nil
    @transformations = IDENTITY
    @step = 1
    update_status_text
  end
  
  def activate
    reset_tool
  end
  
  def deactivate(view)
    view.invalidate
  end
  
  def onMouseMove(flags, x, y, view)
    ph = view.pick_helper
    ph.do_pick(x, y)
    path = ph.path_at(0)
    if path
      last_entity = path.last
      if @step == 1 && last_entity.is_a?(Sketchup::Face)
        @selected_face = last_entity
        # You can retrieve the combined transformations by this method:
        @transformations = ph.transformation_at(0)
        
      #Also need to check if the edge is part of the selected face
      # boundaries, to ensure the same context
      elsif @step == 2 && last_entity.is_a?(Sketchup::Edge) && 
            @selected_face.edges.include?(last_entity)
        @selected_edge = last_entity
        # You have already a transformation, no need to calculate again
      end
    end
    # Lets put out of the contrition's 
    view.invalidate
  end
  
  def onLButtonDown(flags, x, y, view)
    if @step == 1 && @selected_face
      @step = 2
      update_status_text
      elsif @step == 2 && @selected_edge
      prompt_for_groove_parameters
      reset_tool
    end
    view.invalidate
  end
  
  def draw(view)
    draw_highlight(view)
  end
  
  private
  
  def reset_tool
    @selected_face = nil
    @selected_edge = nil
    @transformations = IDENTITY
    @step = 1
    update_status_text
  end
  
  def update_status_text
    Sketchup.status_text = case @step
      when 1
      "Оберіть площину"
      when 2
      "Оберіть край на площині"
    end
  end
  
  def prompt_for_groove_parameters
    prompts = ["Groove width", "Groove depth", "Groove offset"]
    # Store defaults in instance variable, if not entered yet
    #  the predefined mm values will be taken
    # the UI.inputbox will display and format the values according to
    # unit settings in Model Info
    @defaults ||= [ 10, 5, 50 ].map{|s| s.mm}
    begin
      input = UI.inputbox(prompts, @defaults, "Setting groove")
      if input
        # lets use the powerful to_l to convert string to internal unit
        # this will allow users their Unit settings
        # "manual" calculation is not preferred
        @groove_width, @groove_depth, @groove_offset = input.map { |v| v.to_l }
        # Reassign defaults 
        # so last entered value can be used next time  
        @defaults = [ @groove_width, @groove_depth, @groove_offset ]
        create_groove
      end
    rescue
      UI.messagebox( "Invalid length!\nType integer as a current unit or 50mm or 1.9\" for example" )
    end
  end
  
  def create_groove
    return UI.messagebox("Необхідно виділити як площину, так і край!") if @selected_face.nil? || @selected_edge.nil?
    
    model = Sketchup.active_model
    points = get_groove_points()
    
    begin
      model.start_operation("Create Groove", true)
      groove_face = @selected_face.parent.entities.add_face(points)
      groove_face.pushpull(-@groove_depth) if groove_face && groove_face.valid?
    rescue => e
       UI.messagebox("Error creating groove: #{e.message}")
    ensure
      model.commit_operation
    end
  end
  
  # This method will calculate a scale factor at the direction of
  #  given vector
  def scale_factor(vector)
    vector.normalize.transform(@transformations).length.to_f
  end

  def get_groove_points
    edge_vector = @selected_edge.line[1].normalize
    offset_vector = @selected_face.normal * edge_vector
    
    if @groove_offset > 0
      # You need to correct the length of vector by factor 
      # determined the context transformation and direction
      offset_vector.length = @groove_offset / scale_factor(offset_vector)
      
      test_point = @selected_edge.start.position.offset(offset_vector)
      if @selected_face.classify_point(test_point) == Sketchup::Face::PointOutside
        offset_vector.reverse!
      end
      
        offset_start_point = @selected_edge.start.position.offset(offset_vector)
        offset_end_point = @selected_edge.end.position.offset(offset_vector)
        
        # consider scale factor here too
        offset_vector.length = @groove_width / scale_factor(offset_vector)
        width_start_point = offset_start_point.offset(offset_vector)
        width_end_point = offset_end_point.offset(offset_vector)
      else
        offset_start_point = @selected_edge.start.position
        offset_end_point = @selected_edge.end.position
        
        # consider scale factor here too
        width_vector = @selected_face.normal * edge_vector
        width_vector.length = @groove_width / scale_factor(width_vector)
        
        test_width_point = offset_start_point.offset(width_vector)
      if @selected_face.classify_point(test_width_point) == Sketchup::Face::PointOutside
        width_vector.reverse!
      end
      
      width_start_point = offset_start_point.offset(width_vector)
      width_end_point = offset_end_point.offset(width_vector)
    end
    
    # You do not need to use return keyword at the end
    # the  last "line" is returned. (Even you can omit to assign variable)
    [ offset_start_point, offset_end_point, width_end_point, width_start_point ]
  end
  
  # This method wont be used...
  def compute_transformation(path)
    transformation = Geom::Transformation.new
    path.each do |entity|
      if entity.respond_to?(:transformation)
        transformation *= entity.transformation
      end
    end
    transformation
    
    # ... however an alternative one-liner method:
    # transformation = path[0..-2].map(&:transformation).inject(IDENTITY, :*)
  end
  
  def draw_highlight(view)
    if @selected_face
      points = @selected_face.outer_loop.vertices.map(&:position)
      transformed_points = points.map { |point| point.transform(@transformations) }
      view.drawing_color = 'yellow'
      view.draw(GL_POLYGON, transformed_points)
    end
    
    if @selected_edge
      points = @selected_edge.vertices.map(&:position)
      transformed_points = points.map { |point| point.transform(@transformations) } 
      view.line_width = 5
      view.drawing_color = 'blue'
      view.draw(GL_LINES, transformed_points)
    end
  end
  
end

Sketchup.active_model.select_tool(GrooveToolA.new)

_


_
Other notes:

  • Your code above contains tabs characters, this is why the forum engine can not properly show the indentation level. Use only spaces for indentation. No hard tabs. (<< in this link you can find other useful rules, advises)
  • The PickHelper class is very powerful.
    There is a #picked_edge method , #picked_face method and these can be retrieved at once (in one pick). The pick can return multiple pick records (pick path). For example, if the pick hits at the border of an edge and face inside a group there will be two pick paths - one for the face and one for the edge. Actually if you exchange the methods below you can get the face and edge in one step and get the transformation from one of the pick path:
  def onMouseMove(flags, x, y, view)
    ph = view.pick_helper
    ph.do_pick(x, y)
    face = ph.picked_face
    edge = ph.picked_edge
    if edge && face && face.edges.include?(edge)
      index = ph.count.times.find { |i| ph.leaf_at(i) == face }
      @transformations = index ? ph.transformation_at(index) : IDENTITY
      @selected_face = face
      @selected_edge = edge
    else
      @selected_face = nil
      @selected_edge = nil
      @transformations = IDENTITY
    end
    view.invalidate
  end
  
  def onLButtonDown(flags, x, y, view)
    if @selected_face && @selected_edge
      prompt_for_groove_parameters
      reset_tool
    end
    view.invalidate
  end

See also: PickHelper — A Visual Guide – Procrastinators Revolt!

4 Likes

Köszönöm, @desmo. It works great. And your comments on the code and here on the forum have clarified many important coding questions for me. Thank you.

1 Like

Be careful with it :wink::

Groups in SketchUp are very similar to components, but can from a user point of view be thought of as unique objects. Groups can be instanced when copied but are silently made unique when edited through the GUI. To honor this behavior, make sure to call #make_unique before modifying a group through the API.

group_uniq

4 Likes

Sorry, I would like to ask again for help in fixing the code. I just try to make each band unique before I add a groove. It seems like it should be simple, but I can’t do it. What am I missing?
First I did this:

def create_groove
  return UI.messagebox("Необхідно виділити як площину, так і край!") if @selected_face.nil? || @selected_edge.nil?
  
  model = Sketchup.active_model
  points = get_groove_points()
  
  begin
    model.start_operation("Create Groove", true)
    
    # make the parent group of the selected face unique
    @selected_face.parent.make_unique if @selected_face.parent.is_a?(Sketchup::Group)
    
    groove_face = @selected_face.parent.entities.add_face(points)
    groove_face.pushpull(-@groove_depth) if groove_face && groove_face.valid?
  rescue => e
     UI.messagebox("Error creating groove: #{e.message}")
  ensure
    model.commit_operation
  end
end

Then I came to this:

def create_groove
  return UI.messagebox("Необхідно виділити як площину, так і край!") if @selected_face.nil? || @selected_edge.nil?
  
  model = Sketchup.active_model
  points = get_groove_points()
  
  begin
    model.start_operation("Create Groove", true)
    
    # keep the link to the original group
    original_group = @selected_face.parent
    
    # make the parent group of the selected face unique
    if original_group.is_a?(Sketchup::Group)
      original_group.make_unique
      # get a new copy of the face and edge in the original group
      @selected_face = original_group.entities[@selected_face.entityID]
      @selected_edge = @selected_face.edges.find { |e| e.entityID == @selected_edge.entityID }
    end
    
    groove_face = original_group.entities.add_face(points)
    groove_face.pushpull(-@groove_depth) if groove_face && groove_face.valid?
  rescue => e
     UI.messagebox("Error creating groove: #{e.message}")
  ensure
    model.commit_operation
  end
end

But none of this works. What am I missing?

I think I managed to fix it. I did not consider the correct context (path).

	def onMouseMove(flags, x, y, view)
		ph = view.pick_helper
		ph.do_pick(x, y)
		path = ph.path_at(0) # get the path to the selected element
		
		if path
			last_entity = path.last
			
			if @step == 1 && last_entity.is_a?(Sketchup::Face)
				@selected_face = last_entity
				@selected_path = path # keep the path to the face
				@transformations = ph.transformation_at(0)
				elsif @step == 2 && last_entity.is_a?(Sketchup::Edge) && @selected_face.edges.include?(last_entity)
				@selected_edge = last_entity
				# There is no need to store the path to the edge, as it must be in the same context as the face
			end
		end
		view.invalidate
	end
	def create_groove
		return UI.messagebox("Необхідно виділити як площину, так і край!") if @selected_face.nil? || @selected_edge.nil?
		
		model = Sketchup.active_model
		points = get_groove_points()
		model.start_operation("Create Groove", true)
		
		# We define the context based on the stored path
		container = @selected_path[-2] # The penultimate element in the path is the parent container
		
		entities = nil
		if container.is_a?(Sketchup::ComponentInstance) || container.is_a?(Sketchup::Group)
			# We make the container unique if it is not the root context of the model
			unique_container = container.make_unique
			entities = unique_container.definition.entities
			else
			entities = model.active_entities # If not in a group or component, we use the model context
		end
		
		if entities
			groove_face = entities.add_face(points)
			groove_face.pushpull(-@groove_depth) if groove_face.valid?
			else
			UI.messagebox("Не вдалося ідентифікувати контекст для створення пазу.")
		end
		
		model.commit_operation
	end
2 Likes

You figured it out.:+1:
The trick is that the Entity #parent method returns (Sketchup::ComponentDefinition, or Sketchup::Model), and these don’t have make_unique mehtod. Therefore - as you did - you need to use the last but one element of path to get the instance.

1 Like