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: 

Cloning and moving a profile, while keeping it linked to the original.

10 REPLIES 10
Reply
Message 1 of 11
metd01567
1262 Views, 10 Replies

Cloning and moving a profile, while keeping it linked to the original.

I've got a repetitive design task that requires a solution to sweep's "single path/single intersecting profile" restriction.  I hacked the Pipe.py script to copy curves from a “reference” profile in place the hardcoded circle.  It worked, but the copied profile was not linked to the reference profile, and I’d like to tweak the swept bodies after they’re created.  I'm now extruding a thin body from the reference profile; re-orienting it with a MoveFeature; then using the extrusion’s first face as the sweep profile.  The swept bodies respond to reference profile changes, so that solves the basic problem.

 

The extrude feels clumsy, is there a more elegant way to clone a referenced profile and keep the clone linked to the reference?  I’m using the swept bodies as transient cutting tools, so I end up with a clutter of extrusions. I move them by hand into the target component, which helps, but they’re difficult to visually correlate with the cuts.  Since a linkage entity is probably needed in any solution, I’d appreciate suggestions on organization.

 

I’ve got a script mashed up from Pype.py and the Command Input API example.  I’ll post if you’d like but it’s not pretty.

10 REPLIES 10
Message 2 of 11
goyals
in reply to: metd01567

I just tried the same thing from Fusion UI and It looks like reference to original profile is not kept and any modification done on original profile is not reflected in copy profile. I think it might be a deliberate reason. I will update you in case I find more information.



Shyam Goyal
Sr. Software Dev. Manager
Message 3 of 11
metd01567
in reply to: goyals

I'm having trouble attaching the script, I'll try in a different post.  It presents a dialog with two inputs. The first input is a sketch, which must contain a single "reference" profile (select the sketch from the tree).  The second input is a set of "paths" for sweeping, for now you've got to select one and only one curve for each path.

 

Requiring the user to select a sketch, vs direct selection of the profile may seem odd.  The script needs to know the user's intended orientation of the profile at the sweep path's start point.  I'm using the origin and orientation of the profile within its sketch. 

 

But that isn't reliable.  For example, if you select all six paths in the "Slots" sketch, the upper left swept body is flipped relative to the others.  This may be why Fusion 360's native Sweep Command supports only one path, and requires the profile to be pre-oriented to that path.  Note the infamous Pype.py example sweeps a circle, which is symmetric and requires no interpretation of intent.

Message 4 of 11
metd01567
in reply to: metd01567

The example file.

Message 5 of 11
metd01567
in reply to: metd01567

I couldn't attach the script, here it is in-line.  I have it in a file named: SweepByReference.py.

# -*- coding: utf-8 -*-
"""
Created on Tue Aug 28 07:56:27 2018

@author: metd01567
"""

# TODO: refactor: exception handling needs work, e.g. clean up stray temp body after failure, or avoid creating it until all reasonable validation is done
#     consider adding a validate input method, and move as much validation code into it, and consider if other checks are needed

import adsk.core, adsk.fusion, traceback

# Global set of event handlers to keep them referenced for the duration of the command
_handlers = []

# Event handler that reacts to when the command is destroyed. This terminates the script.
class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:

            app = adsk.core.Application.get()
            ui = app.userInterface
            # When the command is done, terminate the script
            # This will release all globals which will remove all event handlers
            adsk.terminate()
        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


# Event handler that reacts when the command definition is executed which
# results in the command being created and this event being fired.
class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:

            # check workspace, must be in Model

            app = adsk.core.Application.get()
            ui = app.userInterface
            product = app.activeProduct
            design = adsk.fusion.Design.cast(product)
            if not design:
                ui.messageBox('This script is not supported in current workspace, please change to MODEL workspace and try again.')
                return False

            # Get the command that was created.
            cmd = adsk.core.Command.cast(args.command)

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

            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)

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

            childInputs = inputs

            ###################################
            # add controls

            # Create a selection input for sketches
            sketchSelectionInput = childInputs.addSelectionInput('sketchSelection', 'Profile Sketch', 'Sketch should have a single profile that intersects x,y: 0,0, sketch origin will be swept along the selected path')
            sketchSelectionInput.setSelectionLimits(1, 1)
            sketchSelectionInput.addSelectionFilter("Sketches")

            # Create a selection input for paths
            pathSelectionInput = childInputs.addSelectionInput('pathSelection', 'Paths', 'select any segment of the path, the complete chain will be used')
            pathSelectionInput.setSelectionLimits(1)
            pathSelectionInput.addSelectionFilter("SketchCurves")

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

def run(context):
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface

        # Get the existing command definition or create it if it doesn't already exist.
        cmdDef = ui.commandDefinitions.itemById('sweepByReference')
        if not cmdDef:
            cmdDef = ui.commandDefinitions.addButtonDefinition('sweepByReference', 'Sweep By Reference', 'Command to sweep a profile from a reference sketch.')

        # Connect to the command created event.
        onCommandCreated = MyCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        _handlers.append(onCommandCreated)

        # Execute the command definition.
        cmdDef.execute()

        # Prevent this module from being terminated when the script returns, because we are waiting for event handlers to fire.
        adsk.autoTerminate(False)


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

# Event handler that reacts to any changes the user makes to any of the command inputs.
class MyExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:

            app = adsk.core.Application.get()
            ui = app.userInterface
            doSweeps(args)

            adsk.terminate()

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


# get the sketch input, and iterate through the selected paths
def doSweeps(args):

    # TODO: refactor, failure of the existence checks would be a software error, and logged vs prompting user

    app = adsk.core.Application.get()
    ui = app.userInterface
    eventArgs = adsk.core.CommandEventArgs.cast(args)
    inputs = eventArgs.command.commandInputs

    ###############################################
    # look for the sketch selection input, and verify that it is a sketch
    sketchSelectionInput = inputs.itemById('sketchSelection')
    if sketchSelectionInput == None:
        ui.messageBox('software error, the sketch selection was lost')
        return
    selectedSketchEntity = sketchSelectionInput.selection(0).entity
    selectedSketch = adsk.fusion.Sketch.cast(selectedSketchEntity)
    if selectedSketch == None:
        ui.messageBox('software error: sketch selection could not be used')
        return

    ###############################################
    # collect path selections
    pathSelectionInput = inputs.itemById('pathSelection')
    if pathSelectionInput == None:
        ui.messageBox('software error, the path selection was lost')
        return

    ###############################################
    # sweep each path

    # TODO: find out why direct iteration of the selection input fails.
    #    The sketch went invisible after first path was swept, and then the second index was not there
    #    Even after disabling "Auto hide sketch on feature creation" in preferences->General->Design
    #
    #    If it turns out selection inputs do sometimes change, maybe pulling the entities first is good practice.
    paths = adsk.core.ObjectCollection.create()
    for thisEntity in range(pathSelectionInput.selectionCount):
        paths.add(pathSelectionInput.selection(thisEntity).entity)

    for thisPath in paths:
        selectedPath = adsk.fusion.SketchCurve.cast(thisPath)
        if selectedPath == None:
            ui.messageBox('software error: path selection could not be used')
            return
        doSweep(selectedSketch, selectedPath)

    app.activeViewport.refresh()

# TODO: refactor, function is too big
def doSweep(selectedSketch, selectedPath):

    ###############################################
    # set up infrastructure
    ###############################################
    app = adsk.core.Application.get()
    ui = app.userInterface
    product = app.activeProduct
    design = adsk.fusion.Design.cast(product)
    comp = design.rootComponent

    ###############################################
    #  derive usable entities and parameters, and verify inputs meet requirements
    ###############################################

    ###############################################
    # process path

    # convert the path selection object to a usable path - chained curve will follow connected segments
    feats = comp.features
    chainedOption = adsk.fusion.ChainedCurveOptions.connectedChainedCurves
    if adsk.fusion.BRepEdge.cast(selectedPath):
        chainedOption = adsk.fusion.ChainedCurveOptions.tangentChainedCurves
    path = adsk.fusion.Path.create(selectedPath, chainedOption)
    path = feats.createPath(selectedPath)

    ###############################################
    # process the "reference" sketch containing a single profile

    if selectedSketch.profiles.count != 1:
        ui.messageBox('reference sketch must have a single profile, ' + str(selectedSketch.profiles.count) + ' were found\n\noperation not complete')
        return

    # grab the profile
    refProfile = selectedSketch.profiles[0]

    ###############################################
    # create a thin extrusion and make it normal to the sweep path
    ###############################################

    # extrude a thin body from the reference profile.  thickness is arbitrary, but keep it well above the model's resolution
    thickness = adsk.core.ValueInput.createByReal(0.1)
    extrude = comp.features.extrudeFeatures.addSimple(refProfile, thickness, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)

    # name the body for user convenience
    tempBody = extrude.bodies.item(0)
    if tempBody == None:
        ui.messageBox('temp extrude failed\n\noperation not complete')
        return
    tempBody.name = "generatedSweepProfile"

    ###############################################
    # derive the move transform parameters:
    ###############################################

    # translate from the reference sketch's control point (which must be at 0,0) to the start point of the path
    firstEntity = path.item(0)
    (returnValue, startPoint, endPoint) = firstEntity.curve.evaluator.getEndPoints()
    if not returnValue:
        ui.messageBox('could not fetch start point of path')
        return
    moveVector = adsk.core.Vector3D.create(startPoint.x, startPoint.y, 0)

    # TODO: fixme, User's concept of path orientation isn't known.  additional input required.
    # rotate to the tangent at the path start point
    (returnValue, pathTangent) = firstEntity.curve.evaluator.getTangent(0)
    if not returnValue:
        ui.messageBox('could not fetch path tangent')
        return

    # create the move transform
    transform = adsk.core.Matrix3D.create()
    transform.setToRotateTo(selectedSketch.referencePlane.geometry.normal, pathTangent)
    transform.translation = moveVector

    # move the temp body
    bodies = adsk.core.ObjectCollection.create()
    bodies.add(tempBody)
    moveInput = comp.features.moveFeatures.createInput(bodies, transform)
    comp.features.moveFeatures.add(moveInput)

    ###############################################
    # now perform the sweep
    ###############################################

    # extract the start face
    sweepFace = extrude.startFaces.item(0)
    if sweepFace == None:
        ui.messageBox('extrude has no faces\n\noperation not complete')
        return

    # Set up the sweep operation
    sweepFeats = feats.sweepFeatures
    sweepInput = sweepFeats.createInput(sweepFace, path, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
    sweepInput.orientation = adsk.fusion.SweepOrientationTypes.PerpendicularOrientationType
    sweepFeats.add(sweepInput)
Message 6 of 11
metd01567
in reply to: metd01567

OK, I didn't really expect a simple answer.  And at this point I could write the script I'd originally intended.  But I'll press on for a while.

 

I've spawned other topics, but technically you can't move a profile without knowing where it goes.  So I'll declare myself on-topic and continue.

 

The discussion below assumes the profile is 2D, and each path resides in a 2D plane.  3D will require a bit more work on the users's part.

 

For a profile that is symmetric about one axis, there are only two choices for orientation: the tangent plane of the path (e.g. returned by ...getTangent on the first PathEntity), or the tangent plane flipped about the axis of asymmetry.  The second choice is also the tangent plane of the path, but running in the opposite direction.  For a quick test, I accept the tangent plane if multiple paths are selected, but flip it when a single path is selected.  I get what I want if I choose the upper left path by itself, then sweep all the rest in a single operation.  Here's an x axis flip in doSweep, just to see how it works.  "reverseSweep" is determined from the count of the SketchCurve selections in doSweeps.

    # create the move transform
    transform = adsk.core.Matrix3D.create()
    transform.setToRotateTo(selectedSketch.referencePlane.geometry.normal, pathTangent)
    if reverseSweep:
        flipTransform = adsk.core.Matrix3D.create()
        axisVector = adsk.core.Vector3D.create(1.0, 0, 0)
        originPoint = adsk.core.Point3D.create(0, 0, 0)
        flipTransform.setToRotation(math.pi, axisVector, originPoint)
        transform.transformBy(flipTransform)
        ui.messageBox("flipping")
    transform.translation = moveVector

Rather than trying to automatically determine symmetry, might add a "flip last" checkbox for X and Y in the dialog.  If I extend to 3D, I'll need a free rotate option.

 

But it's always nice to have a good default.  I could bias the orientation assuming the user clicks near the intended start end of the path.  Since there is no way for the user to know path direction, I probably need to render the sweep as soon as the user selects (or reselects) a path.  I'll experiment with rendering each sweep in an inputChanged handler, to give the user a preview of each sweep's orientation as the selections are made.  I could also use red arrows like the 2D Contour CAM dialog, but that probably requires custom graphics and I don't particularly like them anyway.

Message 7 of 11
metd01567
in reply to: metd01567

Just to give you a view of the latest problem, here's a view from the upper right corner of the offending sweep:

Screen Shot 2018-08-31 at 9.13.03 AM.png

Message 8 of 11
metd01567
in reply to: metd01567

Geometry can't be denied I was fooling myself.  Only the orientation of the tangent plane relative to the profile's frame of reference matters.  For the oddball path (upper left), both ends have the same tangent plane orientation.  So starting from the opposite end had no effect.  The endpoints of all my other paths face the opposite direction, and that's why they happen to turn out as I intended.  It's pretty obvious once you think about it.

 

Even for a 2D profile and 2D path, I don't think there's a way to anticipate the user's intent.  So I'll just let the profile fall as it may, and allow the user to correct as paths are selected.  Giving a preview of the completed sweep will be critical.

 

 

 

Message 9 of 11
metd01567
in reply to: metd01567

Sorry for thrashing.  An even number of wrongs makes something that looks right, until you've done enough testing.  You'll notice that the profile in the SlotProfile sketch is oriented oddly, so my test case was skewed (a.k.a. wrong).   It should have been draw as if looking along one of the paths, with the slot below rather than to the right.  In effect that's what a Fusion 360's user does by creating a "plane along path", then drawing the sweep profile on that construction plane.

 

I recently read Bob Ekin's paper on Inventor math and geometry, which he says is mostly applicable to Fusion 360.  It reminded me about U,V coordinates and that's the way to look at my profile's reference sketch.  In effect I create a matrix to map the sketch's curves to the path's start plane.  As the method name suggests, the matrix is: set to rotate [the reference plane's z axis vector] to [the path's tangent vector].

transform.setToRotateTo(selectedSketch.referencePlane.geometry.normal, pathTangent)

Applying that matrix to the sketch curves (or entities derived from them) has the desired effect.

 

It does matter that the sketch's axis of asymmetry is oriented to the path's plane, so I can only get reliable defaults when all paths start in a common 2D plane.  When that's not the case, or the profile is completely asymmetric, additional input may needed for each path.  The user must be able to flip and/or rotate the profile independently for each path.

 

This is good enough to accomplish my original task.  I'll clean things up and test it a bit more before posting the script and sample file again.  Hopefully I'll just stop at that point.

Message 10 of 11
metd01567
in reply to: metd01567

Since the thin extrusion overlaps the swept body, I could just join them.  I tried with the user interface after running the script.  I end up with a single body that responds to changes in the "reference" profile as desired.  Note I intersected to trim the extrusion before joining.  Some of my paths start with an arc, and although the extrusion is thin it could deviate slightly.  Since they are guaranteed to fully intersect at the start face, I won't loose anything.

 

I'm not completely satisfied with this approach.   The model is clean but the timeline shows the extrusion, intersection and join operations.  A user will probably be confused.  I don't mind so much that it's messy under the covers, but I don't want to give the user any surprises. 

 

I'll code this up, but I'm still open to better ideas ...

Message 11 of 11
metd01567
in reply to: metd01567

I finally noticed that Matrix3D.setRotationTo has an optional third parameter.  It is described as: "The optional axis argument may be used when the two vectors are perpendicular and in opposite directions to specify a specific solution".  "perpendicular and opposite directions" is exactly the test case that has been failing.  Once I figured out how to get the normal to the sketch containing the path curves, my test cases now work without additional input.  I still won't claim thorough testing, but I'm happy enough for now.

 

 

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