Cinema 4D - my Python scripts - expand splines

From NoskeWiki
Jump to navigation Jump to search

About

NOTE: This page is a daughter page of: Cinema 4D


This page contains code for a Python script I've written for Cinema 4D. To understand how it compiles see Cinema 4D - my Python scripts.

expand_splines.py

expand_splines.py

# For each selected open spline (line), turns it into a thick
# closed spline (polygon) along the path of the original line.
# No additional chamfer points are added, so very sharp corners will
# look funny - resulting spline will have twice as many points.
# Alternatively can also fold the spline back on itself in place
# without duplicating the first or last line.

import c4d
from c4d import gui
import math

# Unique id numbers for each of the GUI elements
LBL_USAGE = 1000
LBL_INFO1 = 1001
LBL_INFO2 = 1002
GROUP_OPTS = 10000
NUMEDIT_THICKNESS = 10001
CHK_CLOSE_SPLINE = 10003
CHK_MAKE_COPY = 10004
CMB_ACTION = 20000
CMB_ACTION_EXPAND = 20001
CMB_ACTION_REVERSE = 20002

class OptionsDialog(gui.GeDialog):
  """ Dialog for expanding a open spline.
  """
  def CreateLayout(self):
    self.SetTitle('Expand Open Spline')
    self.AddMultiLineEditText(LBL_USAGE, c4d.BFH_SCALEFIT, inith=40, initw=500,
                              style=c4d.DR_MULTILINE_READONLY)
    self.SetString(LBL_USAGE,
        "USAGE: For any selected open splines, makes them\n"
        "   thick closed polygons of specified thickness")
    # Dropdown and thickness option:
    self.GroupBegin(GROUP_OPTS, c4d.BFH_SCALEFIT, 2, 2)
    self.AddStaticText(LBL_INFO1, c4d.BFH_LEFT, name='Action: ') 
    self.AddComboBox(CMB_ACTION, c4d.BFH_SCALEFIT)
    self.AddChild(CMB_ACTION, CMB_ACTION_EXPAND, "expand spline in XZ")
    self.AddChild(CMB_ACTION, CMB_ACTION_REVERSE, "reverse on itself")
    self.SetInt32(CMB_ACTION, CMB_ACTION_EXPAND)  # Set default action.
    self.AddStaticText(LBL_INFO2, c4d.BFH_LEFT, name='Thickness: ') 
    self.AddEditNumber(NUMEDIT_THICKNESS, c4d.BFH_SCALEFIT)
    self.SetReal(NUMEDIT_THICKNESS, 10)
    self.GroupEnd()
    self.AddSeparatorH(c4d.BFH_SCALE);
    # Checkbox options:
    self.AddCheckbox(CHK_CLOSE_SPLINE, c4d.BFH_SCALEFIT,
                     initw=1, inith=1, name="close spline when done")
    self.SetBool(CHK_CLOSE_SPLINE, True)
    self.AddCheckbox(CHK_MAKE_COPY, c4d.BFH_SCALEFIT,
                     initw=1, inith=1, name="make new copy")
    self.SetBool(CHK_MAKE_COPY, True)
    self.AddSeparatorH(c4d.BFH_SCALE);
    # Buttons - an Ok and Cancel button:
    self.AddDlgGroup(c4d.DLG_OK|c4d.DLG_CANCEL);
    self.ok = False
    return True

  # React to user's input:
  def Command(self, id, msg):
    if id==c4d.DLG_CANCEL:
      self.Close()
    elif id==c4d.DLG_OK:
      self.ok = True
      action = self.GetInt32(CMB_ACTION)
      self.option_action_expand = action == CMB_ACTION_EXPAND
      self.option_action_reverse = action == CMB_ACTION_REVERSE
      self.option_thickness = self.GetReal(NUMEDIT_THICKNESS)
      self.option_close_spline = self.GetBool(CHK_CLOSE_SPLINE)
      self.option_make_copy = self.GetBool(CHK_MAKE_COPY)
      self.Close()
    return True


def get_yaw(start_pt, end_pt):
  """Determines the azimuth from the given start to end point.
  
  Rotation is around XZ where -X = 0
  
  Args:
    start_pt: c4d.Vector representing start point.
    end_pt: c4d.Vector representing end point.

  Returns:
    yaw angle in degrees.
  """
  rad = math.atan2(end_pt.z - start_pt.z, end_pt.x - start_pt.x)
  return math.degrees(rad)


def angle_formed_by_three_points_xy(pt1, pt2, pt3):
  """Determines the theta (in degrees) between 3 connected points.
  
       pt3 o
          /
         / theta
    pt2 o---------o pt1

  Args:
    pt1: c4d.Vector representing point before middle point.
    pt2: c4d.Vector representing middle point.
    pt3: c4d.Vector representing point after middle point.
    
  Returns:
    angle between 0 and 360.
  """
  line1_yaw = get_yaw(pt1, pt2)
  line2_yaw = get_yaw(pt3, pt2)
  return (line2_yaw - line1_yaw) % 360.0


def get_point_rel_to_end_xy(start_pt, end_pt, dist_offset, rel_angle):
  """Get a point 'dist_offset' from 'end_pt' at an angle relative to the line.
  
  Example: if dist_offset=2 and rel_angle=90:

                        o  <-- the point returned would be here

  start_pt  o-----------o  end_pt

  Args:
    start_pt: c4d.Vector representing start point.
    end_pt: c4d.Vector representing end point.
    dist_offset: Numeric distance to offset away from end_pt.
    rel_angle: Angle relative from the direction of the line to use in
       displacing the returned point. 0 is straight (in direction of line).
  
  Returns:
    angle between 0 and 360.
  """
  yaw = get_yaw(start_pt, end_pt)
  theta = math.radians(yaw + rel_angle)
  offset_x = dist_offset * math.cos(theta)
  offset_z = dist_offset * math.sin(theta)
  return c4d.Vector(offset_x + end_pt.x, end_pt.y, offset_z + end_pt.z)


def get_chamfer_point_xy(pt1, pt2, pt3, offset):
  """Get a chamfer on the outside of a point connected to two other points.
  
      -    -    -    o    <-- chamfer point
       thickness    /        (for this case would be 45 degress offset)
                   /
      pt1 o-------o pt2
                  |
                  |
                  o pt3
  
  Args:
    pt1: c4d.Vector representing point before middle point.
    pt2: c4d.Vector representing middle point.
    pt3: c4d.Vector representing point after middle point.
    offset: Numerical value representing thickness to base
       offset on. A sharper turn will mean further offset.

  Returns:
    c4d.Vector representing chamfer point.
  """
  if pt1 == pt2:
    return get_point_rel_to_end_xy(
        pt3, pt2, offset, -90)
  if pt2 == pt3:
    return get_point_rel_to_end_xy(
        pt1, pt2, offset, 90)

  
  angle_at_curr = 360 - angle_formed_by_three_points_xy(pt1, pt2, pt3)
  angle_inset = angle_at_curr / 2.0
  rad =  math.sin(math.radians(angle_inset))  # Radians
  dist_from_corner = offset
  if rad != 0:
    dist_from_corner = offset / rad

  return get_point_rel_to_end_xy(
        pt1, pt2, dist_from_corner, 180.0-angle_inset)

def expand_spline_xz(spline, thickness):
  """Takes a 'spline' object, and expands it by 'thickness' in XZ.
  
  Args:
    spline: Open spline object to expand.
    thickness: Number representing thickness/diameter of final line.
    
  Returns:
    number of points added.
  """
  if (spline == None or not spline.CheckType(c4d.Ospline) or
      spline.GetPointCount() < 2):
    return 0;

  len_orig = spline.GetPointCount()
  len_new = len_orig * 2
  new_spline = spline
  new_spline.ResizeObject(len_new)
  for i in range(0,len_orig):
    p_prev = spline.GetPoint(i)  # Previous point.
    p_curr = spline.GetPoint(i)  # Current point.
    p_next = spline.GetPoint(i)  # Next point.
    if i > 0:
      p_prev = spline.GetPoint(i-1)
    if i < len_orig-1:
      p_next = spline.GetPoint(i+1)
    
    p_mid_cw = get_chamfer_point_xy(p_prev, p_curr, p_next, thickness / 2.0)
    p_mid_ccw = get_chamfer_point_xy(p_next, p_curr, p_prev, thickness / 2.0)
    new_spline.SetPoint(i, p_mid_cw)
    new_spline.SetPoint(len_new-i-1, p_mid_ccw)

  doc.AddUndo(c4d.UNDOTYPE_CHANGE, spline)
  spline = new_spline
  
  return len_new - len_orig;


def fold_spline_on_itself(spline):
  """Takes a 'spline' object, and appends points which backtrack to the first.
  
  If the origional spline has points at positions:      A,B,C,D
  The new spline append s(len*2-2) points and becomes:  A,B,C,D,C,B
  Neither the last or first point are not repeated.
  
  Args:
    spline: Spline object to fold back on itself by appending.
    
  Returns:
    number of points added.
  """
  if (spline == None or not spline.CheckType(c4d.Ospline) or
      spline.GetPointCount() < 3):
    return 0;

  len_orig = spline.GetPointCount()
  len_new = (len_orig) * 2 - 2
  
  doc.AddUndo(c4d.UNDOTYPE_CHANGE, spline)
  spline.ResizeObject(len_new)
  for i in range(1, len_orig-1):
    e = len_new - i  # Working back from last point.
    point = spline.GetPoint(i)
    new_point = c4d.Vector(point.x, point.y, point.z)
    spline.SetPoint(e, new_point)

  return len_new - len_orig;


def main():
  # Get the selected objects, including children.
  selection = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
 
  if len(selection) <= 0:
    gui.MessageDialog('Must select spline object(s)!')
    return

  # Open the options dialogue to let users choose their options.
  dlg = OptionsDialog()
  dlg.Open(c4d.DLG_TYPE_MODAL, defaultw=300, defaulth=50)
  if not dlg.ok:
    return

  doc.StartUndo()  # Start undo block.
  num_splines_changed = 0
  for i in range(0,len(selection)):
    spline = selection[i]
    if not spline.CheckType(c4d.Ospline) or spline.GetPointCount() < 2:
      continue
    num_splines_changed += 1
  
    # Make copy if needed:
    if dlg.option_make_copy:
      new_spline = spline.GetClone()
      doc.InsertObject(new_spline, pred=spline)
      doc.AddUndo(c4d.UNDOTYPE_NEW, new_spline)
      spline = new_spline
    
    # Apply chosen action:
    points_added = 0
    if dlg.option_action_expand:
      points_added = expand_spline_xz(spline, dlg.option_thickness)
    elif dlg.option_action_reverse:
      points_added = fold_spline_on_itself(spline)
    
    # Close spline:
    if dlg.option_close_spline:
      spline[c4d.SPLINEOBJECT_CLOSED] = True    

  doc.EndUndo()   # End undo block.
  c4d.EventAdd()  # Update C4D to see changes.
  gui.MessageDialog(str(num_splines_changed) + ' splines changed')

if __name__=='__main__':
    main()


See Also


Code license
For all of the code on my site... if there are specific instruction or licence comments please leave them in. If you copy my code with minimum modifications to another webpage, or into any code other people will see I would love an acknowledgment to my site.... otherwise, the license for this code is more-or-less WTFPL (do what you want)! If only copying <20 lines, then don't bother. That said - if you'd like to add a web-link to my site www.andrewnoske.com or (better yet) the specific page with code, that's a really sweet gestures! Links to the page may be useful to yourself or your users and helps increase traffic to my site. Hope my code is useful! :)