Community
Fusion API and Scripts
Got a new add-in to share? Need something specialized to be scripted? Ask questions or share what you’ve discovered with the community.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Sweep using 3D parametric curve (spline)

6 REPLIES 6
SOLVED
Reply
Message 1 of 7
ie2230
305 Views, 6 Replies

Sweep using 3D parametric curve (spline)

Hi all,

 

I hope you are doing well. New to fusion modeling, I have been recently facing some challenges with its api. I am writing a script whose goal is to make a sweep based on a circle (as a profile) and a parametric 3D curve (as a path).

 

To make the curve, I am using a sketchFittedSplines.add() function with a collection of points. Then I find the normal plane at one of its points and sketch a circle on it. I am having issues with the last step: the sweep itself. Almost all the examples - and past issues - I have seen online use design.rootComponent.features.createPath() or similar to create a path for the sweep. However, I am getting an error and I can't really see why (fusion's error messages aren't always very helpful. This is the code that sweeps:

 

    def sweep(self, prof):
        path = self.design.rootComponent.features.createPath(self.spline)
        sweeps = self.design.rootComponent.features.sweepFeatures
        sweepInput = sweeps.createInput(
                prof, 
                path, 
                adsk.core.ValueInput.createByReal(3)
        )
        sweepInput.isSolid = False
        sweeps.add(sweepInput)

 

And this is the code that generates self.spline:

 

def draw_curve(self):
        try:
            self.linspace(
                    self.tmin, 
                    self.tmax, 
                    int((self.tmax - self.tmin)*self.precision),
                )

            for index in self.indexes:
                self.points.add(
                    adsk.core.Point3D.create(
                        self.convert(float(self.x_t.subs(t, index))), 
                        self.convert(float(self.y_t.subs(t, index))), 
                        self.convert(float(self.z_t.subs(t, index)))
                    )
                )

            self.spline = self.sketch.sketchCurves.sketchFittedSplines.add(self.points)
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

 

Any help would be very much appreciated.

6 REPLIES 6
Message 2 of 7
kandennti
in reply to: ie2230

Hi @ie2230 -San.

 

I can't judge in detail because I can't try it, but this is one problem area I noticed.

・・・
        sweepInput = sweeps.createInput(
                prof, 
                path, 
                # adsk.core.ValueInput.createByReal(3)
                adsk.fusion.FeatureOperations.NewBodyFeatureOperation
        )
・・・

 

Message 3 of 7
ie2230
in reply to: kandennti

Hello, thank you very much for your response. I was initially confused about whether to use the assigned fusion object or the real value because of what was provided in the documentation. I have done the edit you suggested but the error is unfortunately still present. My code is a bit long, with other issues I wanted to deal with later but here it is:

#Author-
#Description-

import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__)))

from math import *
import adsk.core, adsk.fusion, adsk.cam, traceback, sympy

_ui = adsk.core.UserInterface.cast(None)
handlers = []
t = sympy.var('t')

handlers_dict = {
    'x(t)'               : sympy.lambdify(t, "t", "math"),
    'y(t)'               : sympy.lambdify(t, "t", "math"),
    'z(t)'               : sympy.lambdify(t, "t", "math"),
    'tmin'               : 0,
    'tmax'               : 1,
    'unit'               : 'Centimeter',
    'precision'          : 1,
    'circle radius'      : 7,
    't position'         : 0,
    'precision position' : 0,
}

class MyCommandInputChangedHandler(adsk.core.InputChangedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args):
        try:
            eventArgs = adsk.core.InputChangedEventArgs.cast(args)
            #inputs = eventArgs.inputs
            cmdInput = eventArgs.input
            # onInputChange for slider controller
            #tableInput = inputs.itemById('table')
            if cmdInput.id == 'x_t':
                handlers_dict['x(t)'] = sympy.sympify(str(cmdInput.value).replace('^', '**'))
            elif cmdInput.id == 'y_t':
                handlers_dict['y(t)'] = sympy.sympify(str(cmdInput.value).replace('^', '**'))
            elif cmdInput.id == 'z_t':
                handlers_dict['z(t)'] = sympy.sympify(str(cmdInput.value).replace('^', '**'))
            elif cmdInput.id == 'tmin':
                handlers_dict['tmin'] = sympy.lambdify(t, str(cmdInput.value), "math")(1)
            elif cmdInput.id == 'tmax':
                handlers_dict['tmax'] = sympy.lambdify(t, str(cmdInput.value), "math")(1)
            elif cmdInput.id == 'unit':
                handlers_dict['unit'] = cmdInput.value
            elif cmdInput.id == 'precision':
                handlers_dict['precision'] = float(cmdInput.value)
            elif cmdInput.id == 'circle_radius':
                handlers_dict['circle radius'] = float(cmdInput.value)
            elif cmdInput.id == 't_position':
                handlers_dict['t position'] = float(cmdInput.value)
            elif cmdInput.id == 'precision_position':
                handlers_dict['precision position'] = float(cmdInput.value)

        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            # When the command is done, terminate the script
            # This will release all globals which will remove all event handlers
            adsk.terminate()
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

class MyCommandExecuteHandler(adsk.core.CommandEventHandler):

    unit_dict = {
        'Centimeter' : 1,
        'Millimeter' : 0.1,
        'Meter'      : 10,
        'Inch'       : 2.54,
        'Foot'       : 30.48,
    }
    
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            eventArgs = adsk.core.CommandEventArgs.cast(args)
            inputs = eventArgs.command.commandInputs

            self.x, self.y, self.z = sympy.var('x y z')
            
            self.x_t, self.y_t, self.z_t, self.tmin, self.tmax, self.unit, \
            self.precision, self.circle_radius, self.t_position, self.precision_position = \
                handlers_dict['x(t)'], handlers_dict['y(t)'], handlers_dict['z(t)'], \
                handlers_dict['tmin'], handlers_dict['tmax'], handlers_dict['unit'], \
                handlers_dict['precision'], handlers_dict['circle radius'], \
                handlers_dict['t position'], handlers_dict['precision position']

            self.convert = lambda x: x * self.unit_dict[self.unit]#the default units used are cm
            
            app = adsk.core.Application.get()
            self.design = app.activeProduct
            self.points = adsk.core.ObjectCollection.create()

            components = self.design.rootComponent.occurrences
            self.component = components.addNewComponent(adsk.core.Matrix3D.create()).component
            self.sketch = self.component.sketches.add(self.component.xYConstructionPlane)

            self.draw()
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
    
    def draw_circle(self):
        r_p = (
            self.x_t.diff(t),
            self.y_t.diff(t),
            self.z_t.diff(t),
        )

        idx = int(self.t_position - self.tmin + self.precision_position)
        position = adsk.core.Point3D.create(
            self.convert(float(self.x_t.subs(t, self.indexes[idx]))), 
            self.convert(float(self.y_t.subs(t, self.indexes[idx]))), 
            self.convert(float(self.z_t.subs(t, self.indexes[idx])))
        )
        
        my_vect = [ float(r_p[i].subs(t, self.indexes[idx])) for i in range(3) ]
        normal = adsk.core.Vector3D.create(my_vect[0], my_vect[1], my_vect[2])

        normal_plane = adsk.core.Plane.create(position, normal)

        planes = self.design.rootComponent.constructionPlanes
        planeInput = planes.createInput()
        planeInput.setByPlane(normal_plane)
        error_occ = False
        while True:
            try:
                construction_plane = planes.add(planeInput)
            except RuntimeError as ex:
                error_occ = True
                template = "An exception of type {0} occurred. Arguments:\n{1!r}"
                message = template.format(type(ex).__name__, ex.args)
                choice = _ui.messageBox(
                    message + str('Make sure to select "Do not capture Design History"'),
                    "Expected Error"  ,
                    3
                )
                if choice == 2:
                    self.design.designType = 0 #could set to 1 to get back to parametric modelingt
                    _ui.messageBox("Design type turned to Direct Design.")
            else:
                if error_occ:
                    self.design.designType = 1 #reactivating it
                    _ui.messageBox("Design type turned back to parametric modeling.")
                break
        
        circle_sketch = self.component.sketches.add(construction_plane)
        sketchCircles = circle_sketch.sketchCurves.sketchCircles
        centerPoint = self.points.asArray()[idx]
        
        circle = sketchCircles.addByCenterRadius(position, self.circle_radius)
        prof = self.component.createOpenProfile(circle, False) 
        return prof

    def sweep(self, prof):
        path = self.design.rootComponent.features.createPath(self.spline)
        sweeps = self.design.rootComponent.features.sweepFeatures
        sweepInput = sweeps.createInput(prof, path, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)#3 for new body 1 to cut
        sweepInput.isSolid = False
        sweeps.add(sweepInput)
    
    def draw_curve(self):
        try:
            self.linspace(
                    self.tmin, 
                    self.tmax, 
                    int((self.tmax - self.tmin)*self.precision),
                )

            for index in self.indexes:
                self.points.add(
                    adsk.core.Point3D.create(
                        self.convert(float(self.x_t.subs(t, index))), 
                        self.convert(float(self.y_t.subs(t, index))), 
                        self.convert(float(self.z_t.subs(t, index)))
                    )
                )

            self.spline = self.sketch.sketchCurves.sketchFittedSplines.add(self.points)
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
    
    def linspace(self, start, end, n):
        self.indexes = [start+(end-start)/n*i for i in range(n+1)]

    def draw(self):
        try:
            self.draw_curve()
            prof = self.draw_circle()
            self.sweep(prof)
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

class MyCommandValidateInputsHandler(adsk.core.ValidateInputsEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        pass

class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            # Get the command that was created.
            cmd = adsk.core.Command.cast(args.command)

            # Get the CommandInputs collection associated with the command.
            inputs = cmd.commandInputs

            # Create a tab input.
            tabCmdInput1 = inputs.addTabCommandInput('tab_1', '3D parametric curve')
            tab1ChildInputs = tabCmdInput1.children

            # Create group input.
            EqCmdGroup = tab1ChildInputs.addGroupCommandInput('eqs', 'x,y,z equations')
            EqCmdGroup.isExpanded = True
            EqCmdGroup.isEnabledCheckBoxDisplayed = False
            EqCmdGroupChildren = EqCmdGroup.children

            # Create a message that spans the entire width of the dialog by leaving out the "name" argument.
            message = '<div align="center">A "full width" message using <a href="http:fusion360.autodesk.com">html.</a></div>'
            EqCmdGroupChildren.addTextBoxCommandInput('fullWidth_textBox', '', message, 1, True)            
            
            # Create a string value input.
            EqCmdGroupChildren.addStringValueInput('x_t', 'x(t)', 'Enter your x(t) equation here'),
            EqCmdGroupChildren.addStringValueInput('y_t', 'y(t)', 'Enter your y(t) equation here'),
            EqCmdGroupChildren.addStringValueInput('z_t', 'z(t)', 'Enter your z(t) equation here')

            InterCmdGroup = tab1ChildInputs.addGroupCommandInput('t_inter', 'Range for t')
            InterCmdGroup.isExpanded = True
            InterCmdGroup.isEnabledCheckBoxDisplayed = False
            InterCmdGroupChildren = InterCmdGroup.children

            InterCmdGroupChildren.addStringValueInput('tmin', 'tmin', 'Enter the expression for tmin'),
            InterCmdGroupChildren.addStringValueInput('tmax', 'tmax', 'Enter the expression for tmax')

            ScaleCmdGroup = tab1ChildInputs.addGroupCommandInput('scale', 'Units and precision')
            ScaleCmdGroup.isExpanded = True
            ScaleCmdGroup.isEnabledCheckBoxDisplayed = False
            ScaleCmdGroupChildren = ScaleCmdGroup.children
            
            unit = ScaleCmdGroupChildren.addDropDownCommandInput('unit', 'unit', adsk.core.DropDownStyles.LabeledIconDropDownStyle)
            unitItems = unit.listItems
            #Centimeter, Millimiter, Meter, Inch, Foot
            unitItems.add('Centimeter', True)
            unitItems.add('Millimiter', False)
            unitItems.add('Meter', False)
            unitItems.add('Inch', False)
            unitItems.add('Foot', False)

            ScaleCmdGroupChildren.addStringValueInput('precision', 'precision', 'Enter the number of points per step in t'),

            SweepCmdGroup = tab1ChildInputs.addGroupCommandInput('Sweep', 'Sweep parameters')
            SweepCmdGroup.isExpanded = True
            SweepCmdGroup.isEnabledCheckBoxDisplayed = False
            SweepCmdGroupChildren = SweepCmdGroup.children

            SweepCmdGroupChildren.addStringValueInput('circle_radius', 'Circle radius', 'Enter the radius of the circle used for the sweep')
            SweepCmdGroupChildren.addStringValueInput('t_position', 't-position', 'Value of t at which we start the sweep')
            SweepCmdGroupChildren.addStringValueInput('precision-position', 'Segmtent position', f'On which segment of t does the circle lie (between 0 and {handlers_dict["precision"]-1})')

            # Connect to the command destroyed event.
            onDestroy = MyCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            handlers.append(onDestroy)

            # Connect to the input changed event.           
            onInputChanged = MyCommandInputChangedHandler()
            cmd.inputChanged.add(onInputChanged)
            handlers.append(onInputChanged)

            # Connect to the execute event.           
            onExecute = MyCommandExecuteHandler()
            cmd.execute.add(onExecute)
            handlers.append(onExecute)

            # Connect to the input validation.           
            onValidateInputs = MyCommandValidateInputsHandler()
            cmd.validateInputs.add(onValidateInputs)
            handlers.append(onValidateInputs)

        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

def run(context):
    try:
        app = adsk.core.Application.get()
        global _ui
        _ui  = app.userInterface
    
        """
        if not design:
            ui.messageBox('No active Fusion design', 'No Design')
            return
        """
        
        cmdDef = _ui.commandDefinitions.itemById('cmdInputsParametricCurve')
        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition('cmdInputsParametricCurve', 'Parametric Curve', 'Command innputs for our parametrization script')
        
        onCommandCreated = MyCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        handlers.append(onCommandCreated)

        cmdDef.execute()
        adsk.autoTerminate(False)

    except:
        if _ui:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

 I am using the simply python module which has a dependency on mpmath (I put a copy of the two modules in my script folder, hence the line with sys.path.append(...).

Message 4 of 7
Jorge_Jaramillo
in reply to: ie2230

Hi,
Which error are you getting?
And could you share a model to test it?

They are needed in order to help with a solution.

Regards,
Jorge Jaramillo

Message 5 of 7
ie2230
in reply to: Jorge_Jaramillo

Hi, here is the error I am getting (not very instructive unfortunately):

Screenshot 2023-07-17 at 4.33.08 PM.png

Concerning models to test the code, I have been using a curve <t,t,t> from t=0 to t=5 with precision 2, units centimeters, profile radius (I think I should've labeled it diameter) 3, t position 0 and same for segment position.

Screenshot 2023-07-17 at 4.41.59 PM.pngScreenshot 2023-07-17 at 4.40.00 PM.png

Message 6 of 7
Jorge_Jaramillo
in reply to: ie2230

Hi @ie2230 ,

 

The problem comes from the fact that you're using path and profile from a component while trying to create the sweep feature in the root component.

There is not reference in the documentation that the curves and profile need to be in the same target component.

 

I made the following change in you code and it worked:

 

 

    def sweep(self, prof):
        # path = self.design.rootComponent.features.createPath(self.spline)
        # sweeps = self.design.rootComponent.features.sweepFeatures
        path = self.component.features.createPath(self.spline)
        sweeps = self.component.features.sweepFeatures
        sweepInput = sweeps.createInput(prof, path, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)#3 for new body 1 to cut
        sweepInput.isSolid = False
        sweeps.add(sweepInput)

 

 

 

Even if the code fail with objects from other components, I was able to create the sweep in the UI to the root component:

wtallerdemadera_0-1689648622548.png

I wonder if Autodesk's Fusion 360 team could clarify this issue.

 

I hope this could allow you to complete your script.

 

Best regards,

Jorge Jaramillo

 

 

 

Message 7 of 7
ie2230
in reply to: Jorge_Jaramillo

Thank you very much, it indeed worked! Now that you said it it makes perfect sense: it is naturally not possible to create a path based on a spline that is in a different component of the design. After hours of trying to resolve this by myself you saved me a lot of time. Thank you again!

Can't find what you're looking for? Ask the community or share your knowledge.

Post to forums  

Autodesk DevCon in Munich May 28-29th


Autodesk Design & Make Report