Cinema 4D - my Python scripts - reduce 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 get it working see Cinema 4D - my Python scripts.

reduce_splines.py

reduce_splines.py

# For each selected spline (line), perform polygon reduction
# by removing points which have minimal effect on the overall
# shape. This algorithm ignores any tangent points so should
# be used on LINEAR splines only. Other spline types can be
# turned to linear pretty easily in Attributes window.

import c4d
from c4d import gui
import math

# Unique id numbers for each of the GUI elements
LBL_USAGE = 1000
LBL_INFO1 = 1001
GROUP_OPTS = 10000
NUMEDIT_AREA_THRESHOLD = 10001
NUMEDIT_MIN_POINTS = 10002
CHK_AREA_THRESHOLD = 10003
CHK_MIN_POINTS = 10004
CHK_DELETE_ORIG = 10005

class OptionsDialog(gui.GeDialog):
  """ Dialog for expanding a open spline.
  """
  def CreateLayout(self):
    self.SetTitle('Spline Point Reduction')
    self.AddMultiLineEditText(LBL_USAGE, c4d.BFH_SCALEFIT, inith=40, initw=500,
                              style=c4d.DR_MULTILINE_READONLY)
    self.SetString(LBL_USAGE,
        'USAGE: Performs point reduction on any selected, \n'
        '    linear splines (by removing selected points).')
    # Dropdown and thickness option:
    self.GroupBegin(GROUP_OPTS, c4d.BFH_SCALEFIT, 2, 2)
    self.AddCheckbox(CHK_AREA_THRESHOLD, c4d.BFH_LEFT, initw=350, inith=1,
                     name='reduce by area threshold (units^2):')
    self.SetBool(CHK_AREA_THRESHOLD, True)
    self.AddEditNumber(NUMEDIT_AREA_THRESHOLD, c4d.BFH_LEFT, initw=100)
    self.SetReal(NUMEDIT_AREA_THRESHOLD, 2)
    self.AddCheckbox(CHK_MIN_POINTS, c4d.BFH_LEFT, initw=350, inith=20,
                     name='leave alone once below min points:')
    self.SetBool(CHK_MIN_POINTS, True)
    self.AddEditNumberArrows(NUMEDIT_MIN_POINTS, c4d.BFH_LEFT, initw=100)
    self.SetReal(NUMEDIT_MIN_POINTS, 3)
    self.GroupEnd()
    self.AddSeparatorH(c4d.BFH_SCALE);
    # Checkbox options:
    self.AddCheckbox(CHK_DELETE_ORIG, c4d.BFH_SCALEFIT,
                     initw=1, inith=1, name='delete old copy')
    self.SetBool(CHK_DELETE_ORIG, False)
    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
      self.opt_area_threshold = self.GetReal(NUMEDIT_AREA_THRESHOLD)
      self.opt_area_min_points = self.GetReal(NUMEDIT_MIN_POINTS)
      self.opt_use_area_threshold = self.GetBool(CHK_AREA_THRESHOLD)
      self.opt_use_min_points = self.GetBool(CHK_MIN_POINTS)
      self.opt_delete_orig = self.GetBool(CHK_DELETE_ORIG)
      self.Close()
    return True


def cross_product(p1, p2):
  """Determines the cross produce of two points.
  
  Args:
    p1: c4d.Vector. 1st point.
    p2: c4d.Vector. 2nd point.
    
  Returns:
    c4d.Vector representing cross produce of inputs.
  """
  return_pt = c4d.Vector(0,0,0)
  return_pt.x = (p1.y * p2.z) - (p1.z * p2.y)
  return_pt.y = (p1.z * p2.x) - (p1.x * p2.z)
  return_pt.z = (p1.x * p2.y) - (p1.y * p2.x)
  return return_pt


def triangle_area_3d(p1, p2, p3):
  """Determines the area of a triangle.
  
  The 3 points can be anywhere in 3D (x,y,z) space.
  
  Args:
    p1: c4d.Vector. 1st point in triangle.
    p1: c4d.Vector. 2nd point in triangle.
    p1: c4d.Vector. 3rd point in triangle.
    
  Returns:
    positive area of triangle.
  """
  n = cross_product((p1-p2), (p3-p2))
  return 0.5 * math.sqrt((n.x*n.x) + (n.y*n.y) + (n.z*n.z))


def remove_point(spline, idx):
  """Removes a point from the given index position in the spline.
  
  Tangents are ignored, so is only suitable for linear splines.
  This is O(n), so can become slow on huge splines.
  
  Args:
    spline: Spline object to remove point from.
    idx: Index of point, must be >= 0 and <= points - 1.
    
  Returns:
    true if point removed successfully.
  """
  if idx < 0 or idx >= spline.GetPointCount():
    print 'ERROR: remove_point - idx out of bounds.'
    return False
  psize = spline.GetPointCount()
  for i in range(idx, psize-1):
    spline.SetPoint(i, spline.GetPoint(i+1))
  spline.ResizeObject(psize-1)
  return True


def reduce_spline_by_min_area(spline, min_area_threshold, min_points):
  """Reduces the number of points in the spline by removing the lease important points.
  
  To perform this polyogn reduction it removes points which form an
  area < area_threshold where a triangle if formed with that point,
  the point before it, and the point after.
  
  Args:
    spline: Spline object to reduce points on.
    min_area_threshold: Real representing minimum area in units squared formed
        by a triangle of three consequtive points, for the middle points to be
        left alone. If smaller than this, the middle point is a likely
        candidate for deletion.
    min_points: Integer for the minimum number of points which must
        remain before we stop deleting points.
    
  Returns:
    number of points deleted.
  """
  if (spline == None or not spline.CheckType(c4d.Ospline) or
      spline.GetPointCount() < 2):
    return 0;

  len_orig = spline.GetPointCount()
  end_offset = -1    # If open contour we want to stop before last pt. 
  if spline[c4d.SPLINEOBJECT_CLOSED]:
    end_offset = 1   # If closed contour, we want to check last and first pts.
   
  i = 1
  while True:
    psize = spline.GetPointCount()
    if i >= psize + end_offset or psize <= min_points:
      break
  
    p_prev = spline.GetPoint((i-1) % psize)     # Previous point.
    p_curr = spline.GetPoint(i % psize)         # Current point.
    p_next = spline.GetPoint((i+1) % psize)     # Next point.
    
    triangle_area = triangle_area_3d(p_prev, p_curr, p_next)
    print 'i = ' + str(i) + ' area = ' + str(triangle_area)
    if triangle_area < min_area_threshold:
      remove_point(spline, i)
    else:
      i += 1

  return len_orig - spline.GetPointCount();


def remove_tangents(spline):
  """Removes all tangents from spline.
  
  Args:
    from_spline: Spline to remove tangents from.
    
  Returns:
    number of tangents deleted.
  """
  if (spline == None):
    return 0;

  psize = spline.GetPointCount()
  doc.AddUndo(c4d.UNDOTYPE_CHANGE, spline)
  for i in range(0, psize):
    spline.SetTangent(i, c4d.Vector(0,0,0))
  return psize


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

  min_points = 0
  if dlg.opt_use_min_points:
    min_points = dlg.opt_min_points
  
  doc.StartUndo()  # Start undo block.
  num_splines_changed = 0
  tot_points_removed = 0
  for i in range(0,len(selection)):
    obj = selection[i]
    if not obj.CheckType(c4d.Ospline):
      continue
    
    # Make a copy:
    spline = obj.GetClone()
    doc.InsertObject(spline, pred=obj)
    doc.AddUndo(c4d.UNDOTYPE_NEW, spline)
    
    # Apply chosen action:
    points_removed = 0
    if dlg.opt_use_area_threshold:
      points_removed = reduce_spline_by_min_area(
          spline, dlg.opt_area_threshold, min_points)
      tot_points_removed += points_removed
    if points_removed > 0:
      num_splines_changed += 1
      
    # Delete orig if specified:
    if dlg.opt_delete_orig:
      doc.AddUndo(c4d.UNDOTYPE_DELETE, obj)
      doc.DeleteObject(obj)

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

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! :)