Skip to content

Instantly share code, notes, and snippets.

@royalgarter
Created April 27, 2023 03:47
Show Gist options
  • Save royalgarter/8f919c69bccbf7a2028045a2f3d57306 to your computer and use it in GitHub Desktop.
Save royalgarter/8f919c69bccbf7a2028045a2f3d57306 to your computer and use it in GitHub Desktop.

Revisions

  1. royalgarter renamed this gist Apr 27, 2023. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. royalgarter created this gist Apr 27, 2023.
    511 changes: 511 additions & 0 deletions bezier.
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,511 @@
    # Copyright 2004-2005, @Last Software, Inc.

    # This software is provided as an example of using the Ruby interface
    # to SketchUp.

    # Permission to use, copy, modify, and distribute this software for
    # any purpose and without fee is hereby granted, provided that the above
    # copyright notice appear in all copies.

    # THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
    # IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
    # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    #-----------------------------------------------------------------------------
    # Name : Bezier Curve Tool 1.0
    # Description : A tool to create Bezier curves.
    # Menu Item : Draw->Bezier Curves
    # Context Menu: Edit Bezier Curve
    # Usage : Select 4 points-
    # : 1. Start point of the curve
    # : 2. Endpoint of the curve
    # : 3. Second control point. It determines the tangency at the start
    # : 4. Next to last control point. It determines the tangency at the end
    # Date : 8/26/2004
    # Type : Tool
    #-----------------------------------------------------------------------------

    # Ruby implementation of Bezier curves
    require 'sketchup.rb'

    module Bezier

    # Evaluate a Bezier curve at a parameter.
    # The curve is defined by an array of its control points.
    # The parameter ranges from 0 to 1
    # This is based on the technique described in "CAGD A Practical Guide, 4th Editoin"
    # by Gerald Farin. page 60

    def Bezier.eval(pts, t)

    degree = pts.length - 1
    if degree < 1
    return nil
    end

    t1 = 1.0 - t
    fact = 1.0
    n_choose_i = 1

    x = pts[0].x * t1
    y = pts[0].y * t1
    z = pts[0].z * t1

    for i in 1...degree
    fact = fact*t
    n_choose_i = n_choose_i*(degree-i+1)/i
    fn = fact * n_choose_i
    x = (x + fn*pts[i].x) * t1
    y = (y + fn*pts[i].y) * t1
    z = (z + fn*pts[i].z) * t1
    end

    x = x + fact*t*pts[degree].x
    y = y + fact*t*pts[degree].y
    z = z + fact*t*pts[degree].z

    Geom::Point3d.new(x, y, z)

    end # method eval

    # Evaluate the curve at a number of points and return the points in an array
    def Bezier.points(pts, numpts)

    curvepts = []
    dt = 1.0 / numpts

    # evaluate the points on the curve
    for i in 0..numpts
    t = i * dt
    curvepts[i] = Bezier.eval(pts, t)
    end

    curvepts
    end

    # Create a Bezier curve in SketchUp
    def Bezier.curve(pts, numseg = 16)

    model = Sketchup.active_model
    entities = model.active_entities
    model.start_operation "Bezier Curve"

    curvepts = Bezier.points(pts, numseg)

    # create the curve
    edges = entities.add_curve(curvepts);
    model.commit_operation
    edges

    end

    #-----------------------------------------------------------------------------
    # Define the tool class for creating Bezier curves

    class BezierTool

    def initialize(degree = 3)
    @degree = degree
    if( @degree < 1 )
    UI.messagebox "Minimum degree is 1"
    @degree = 1
    elsif( @degree > 20 )
    UI.messagebox "Maximum degree is 20"
    @degree = 20
    end
    # TODO: I should probably adjust the number of segments used for
    # display and creating the curve based on the the degree and/or the
    # maximum curvature.
    end

    def reset
    @pts = []
    @state = 0
    Sketchup::set_status_text "Click for start point"
    @drawn = false
    end

    def activate
    # There are up to 4 input points that we keep track of
    # @ip1 is the start point of the curve
    # @ip2 is the endpoint of the curve
    # @ip3 is the second control point. It determines the tangency at the start
    # @ip4 is the next to last control point. It determines the tangency at the end
    # @ip5 is an internal input point
    @ip1 = Sketchup::InputPoint.new
    @ip2 = Sketchup::InputPoint.new
    @ip3 = Sketchup::InputPoint.new
    @ip4 = Sketchup::InputPoint.new
    @ip5 = Sketchup::InputPoint.new
    # @ip is a temporary input point used to get other positions
    @ip = Sketchup::InputPoint.new
    self.reset
    Sketchup::set_status_text "Degree", SB_VCB_LABEL
    Sketchup::set_status_text @degree, SB_VCB_VALUE
    end

    def deactivate(view)
    view.invalidate if @drawn
    @ip1 = nil
    @ip2 = nil
    @ip3 = nil
    @ip4 = nil
    @ip5 = nil
    end

    def onMouseMove(flags, x, y, view)
    case @state
    when 0 # getting the first end point
    @ip.pick view, x, y
    if( @ip.valid? && @ip != @ip1 )
    @ip1.copy! @ip
    view.invalidate
    end
    when 1 # getting the second end point
    @ip.pick view, x, y, @ip1
    if( @ip.valid? && @ip != @ip2 )
    @ip2.copy! @ip
    @pts[1] = @ip2.position
    view.invalidate
    end
    when 2 # the second control point - tangency at start
    @ip.pick view, x, y, @ip1
    if( @ip.valid? && @ip != @ip3 )
    @ip3.copy! @ip
    @pts[1] = @ip3.position
    view.invalidate
    end
    when @degree # the next to last point = tangency at end
    @ip.pick view, x, y, @ip2
    if( @ip.valid? && @ip != @ip4 )
    @ip4.copy! @ip
    @pts[@degree-1] = @ip4.position
    view.invalidate
    end
    when 3..@degree-1 # internal points - if degree > 3
    @ip.pick view, x, y
    if( @ip.valid? && @ip != @ip5 )
    @ip5.copy! @ip
    @pts[@state-1] = @ip5.position
    view.invalidate
    end
    end
    view.tooltip = @ip.tooltip if @ip.valid?
    end

    def create_curve
    curve = Bezier.curve @pts, 20
    # see if this fills in any new faces
    if( curve )
    edge1 = curve[0]
    edge1.find_faces

    # Attach an attribute to the curve with the array of points
    curve = edge1.curve
    if( curve )
    curve.set_attribute "skp", "crvtype", "Bezier"
    curve.set_attribute "skp", "crvpts", @pts
    end

    end
    self.reset
    end

    def onLButtonDown(flags, x, y, view)
    # TODO: Use the two point form of the input point finder to get the new points.
    # I need a way to generate an ip at a given position from code.
    @ip.pick view, x, y
    if( @ip.valid? )
    case @state
    when 0
    @pts[0] = @ip.position
    Sketchup::set_status_text "Click for end point"
    @state = 1
    when @degree
    self.create_curve
    when 1
    @pts[2] = @ip.position
    @state = 2
    Sketchup::set_status_text "Click for point 2"
    when 2...@degree
    nextstate = @state+1
    @pts[nextstate] = @pts[@state]
    @pts[@state] = @ip.position
    @state = nextstate
    Sketchup::set_status_text "Click for point #{@state}"
    end
    end
    end

    def onCancel(flag, view)
    view.invalidate if @drawn
    reset
    end

    def onUserText(text, view)
    # get the degree from the text
    newdegree = text.to_i
    if( newdegree > 0 )
    @degree = newdegree
    self.create_curve if( @state > @degree )
    else
    UI.beep
    Sketchup::set_status_text @degree, SB_VCB_VALUE
    end
    end

    def getExtents
    bb = Geom::BoundingBox.new
    if( @state == 0 )
    # We are getting the first point
    if( @ip.valid? && @ip.display? )
    bb.add @ip.position
    end
    else
    bb.add @pts
    end
    bb
    end

    def draw(view)

    # Show the current input point
    if( @ip.valid? && @ip.display? )
    @ip.draw(view)
    @drawn = true
    end

    # show the curve
    if( @state == 1 )
    # just draw a line from the start to the end point
    view.set_color_from_line(@ip1, @ip2)
    view.draw(GL_LINE_STRIP, @pts)
    @drawn = true
    elsif( @state > 1 )
    # draw the curve
    view.drawing_color = "black"
    curvepts = Bezier.points(@pts, 12)
    view.draw(GL_LINE_STRIP, curvepts)
    # draw the control polygon
    # determine the colos for the first and last segments from the input points
    case @state
    when 2
    view.set_color_from_line(@ip1, @ip3)
    view.draw(GL_LINE_STRIP, @pts[0], @pts[1])
    view.drawing_color = "gray"
    view.draw(GL_LINE_STRIP, @pts[1..-1])
    when @degree
    view.drawing_color = "gray"
    view.draw(GL_LINE_STRIP, @pts[0..-2])
    view.set_color_from_line(@ip2, @ip4)
    view.draw(GL_LINE_STRIP, @pts[@degree-1], @pts[@degree])
    else
    view.drawing_color = "gray"
    view.draw(GL_LINE_STRIP, @pts)
    end
    @drawn = true
    end
    end

    end # class BezierTool

    #-----------------------------------------------------------------------------
    # Define the tool class for editing Bezier curves

    class EditBezierTool

    def activate

    @state = 0
    @drawn = false
    @selection = nil
    @pt_to_move = nil

    # Make sure that there is really a Bezier curve selected
    @curve = Bezier.selected_curve
    if( not @curve )
    Sketchup.active_model.select_tool nil
    return
    end

    # Get the control points
    @pts = @curve.get_attribute "skp", "crvpts"
    if( not @pts )
    UI.beep
    Sketchup.active_model.select_tool nil
    return
    end

    # Get the curve points from the vertices
    @vertices = @curve.vertices
    @crvpts = @vertices.collect {|v| v.position}
    @numseg = @vertices.length - 1

    @ip = Sketchup::InputPoint.new
    end

    def deactivate(view)
    view.invalidate if @drawn
    @ip = nil
    end

    def resume(view)
    @drawn = false
    end

    def pick_point_to_move(x, y, view)
    old_pt_to_move = @pt_to_move
    ph = view.pick_helper x, y
    @selection = ph.pick_segment @pts
    if( @selection )
    if( @selection < 0 )
    # We got a point on a segment. Compute the point closest
    # to the pick ray.
    pickray = view.pickray x, y
    i = -@selection
    segment = [@pts[i-1], @pts[i]]
    result = Geom.closest_points segment, pickray
    @pt_to_move = result[0]
    else
    # we got a control point
    @pt_to_move = @pts[@selection]
    end
    else
    @pt_to_move = nil
    end
    old_pt_to_move != @pt_to_move
    end

    def onLButtonDown(flags, x, y, view)
    # Select the segment or control point to move
    self.pick_point_to_move x, y, view
    @state = 1 if( @selection )
    end

    def onLButtonUp(flags, x, y, view)
    return if not @state == 1
    @state = 0

    # Update the actual curve. Move the vertices on the curve
    # to the new curve points
    if( @vertices.length != @crvpts.length )
    UI.messagebox "Count of curve points is wrong!"
    return
    end

    model = @vertices[0].model
    model.start_operation "Edit Bezier Curve"

    # Move the vertices
    @curve.move_vertices @crvpts

    # Update the control points stored with the curve
    @curve.set_attribute "skp", "crvpts", @pts

    model.commit_operation
    end

    def onMouseMove(flags, x, y, view)
    # Make sure that the control polygon is shown
    view.invalidate if not @drawn

    # Move the selected point if state = 1
    if( @state == 1 && @selection )
    @ip.pick view, x, y
    return if not @ip.valid?
    if( @selection >= 0 )
    # Moving a control point
    @pt_to_move = @ip.position
    @pts[@selection] = @pt_to_move
    else
    # moving a segment
    pt = @ip.position
    vec = pt - @pt_to_move
    i = -@selection
    @pts[i-1].offset! vec
    @pts[i].offset! vec
    @pt_to_move = pt
    end
    @crvpts = Bezier.points(@pts, @numseg)
    view.invalidate
    else # state != 1
    # See if we can select something to move
    view.invalidate if( self.pick_point_to_move(x, y, view) )
    end
    end

    def getMenu(menu)
    menu.add_item("Done") {Sketchup.active_model.select_tool nil}
    end

    def getExtents
    bb = Geom::BoundingBox.new
    bb.add @pts
    bb
    end

    def draw(view)
    # Draw the control polygon
    view.drawing_color = "gray"
    view.draw(GL_LINE_STRIP, @pts)

    if( @pt_to_move )
    view.draw_points(@pt_to_move, 10, 1, "red");
    end

    if( @state == 1 )
    view.drawing_color = "black"
    view.draw(GL_LINE_STRIP, @crvpts)
    end

    @drawn = true
    end

    end # class EditBezierTool

    #-----------------------------------------------------------------------------

    # Function to test to see if the selection set contains only a Bezier curve
    # Returns the curve if there is one or else nil
    def Bezier.selected_curve
    ss = Sketchup.active_model.selection
    return nil if not ss.is_curve?
    edge = ss.first
    return nil if not edge.kind_of? Sketchup::Edge
    curve = edge.curve
    return nil if not curve
    return nil if curve.get_attribute("skp", "crvtype") != "Bezier"
    curve
    end

    # Edit a selected Bezier curve
    def Bezier.edit_curve
    curve = Bezier.selected_curve
    if( not curve )
    UI.beep
    return
    end
    Sketchup.active_model.select_tool EditBezierTool.new
    end

    # Select the Bezier curve tool
    def Bezier.tool(degree=3)
    Sketchup.active_model.select_tool BezierTool.new(degree)
    end

    # Add a menu choice for creating bezier curves
    if( not file_loaded?("bezier.rb") )
    add_separator_to_menu("Draw")
    UI.menu("Draw").add_item("Bezier Curves") { Bezier.tool }

    # Add a context menu handler to let you edit a Bezier curve
    UI.add_context_menu_handler do |menu|
    if( Bezier.selected_curve )
    menu.add_separator
    menu.add_item("Edit Bezier Curve") { Bezier.edit_curve }
    end
    end

    end

    end # module Bezier
    file_loaded("bezier.rb")