[BUG] Custom Events have some weird behaviour

[BUG] Custom Events have some weird behaviour

tykapl.breuil
Advocate Advocate
928 Views
7 Replies
Message 1 of 8

[BUG] Custom Events have some weird behaviour

tykapl.breuil
Advocate
Advocate

Hello everyone,

 

So Fusion behaves very weirdly when you try to cause the Execute Preview event from a custom event, everything happens seemingly without issue, but if a fillet (and probably other features) has an error, then the fillet features of the component become "broken" and don't work anymore. This problem doesn't occur when using a separate thread to trigger that event. I the code below I have a test figure that first creates itself normally and then, when the preview event is called again either through a separate thread or a custom event, creates itself with a value for the fillet too big for it to work. Then it gradually reduces this value until it can be created. However, when the preview is caused by a custom event, the first error causes a problem with the fillet features and the creation fails altogether.

Honestly I have absolutely no idea what could cause this. Does this also happen on other machines (simply change threadMode to True of False to test both possibilities) ? Does anyone have any idea what is actually happening here ?

 

import adsk.core, adsk.fusion, adsk.cam, traceback
import threading

# Affectations usuelles
handlers = []
ui = None
app : adsk.core.Application = adsk.core.Application.get()
if app:
    ui = app.userInterface

threadMode = False
first = True


class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler):    
    def __init__(self):
        super().__init__()        
    def notify(self, args: adsk.core.CommandCreatedEventArgs):
        try:
            cmd = args.command
            inputs = cmd.commandInputs
            txt = inputs.addBoolValueInput('id', '', True,'', True)
            onExecutePreview = CommandExecuteHandler()
            cmd.executePreview.add(onExecutePreview)
            handlers.append(onExecutePreview)
            onExecutePreview = CommandExecuteHandler()
            cmd.execute.add(onExecutePreview)
            handlers.append(onExecutePreview)
            if not threadMode:
                app.unregisterCustomEvent('TestTrucCustomEvent')
                customEvent = app.registerCustomEvent('TestTrucCustomEvent')
                onTestEvent = TestEventHandler()
                onTestEvent.cmd = cmd
                customEvent.add(onTestEvent)
                handlers.append(onTestEvent)

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

class CommandExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.CommandEventArgs):
        try:
            command: adsk.core.Command = args.firingEvent.sender
            testFigure = TestFigure()
            global first
            if not first:
                testFigure.parameter = 2
            testFigure.build()
            if first:
                first = False
                if threadMode:
                    thread = BackgroundThread() 
                    thread.command = command    
                    thread.start()     
                else:         
                    app.fireCustomEvent('TestTrucCustomEvent')
            else:
                adsk.autoTerminate(True)
        
        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


class TestEventHandler(adsk.core.CustomEventHandler):
    def __init__(self):
        super().__init__()
        self.cmd = adsk.core.Command
    def notify(self, args: adsk.core.CustomEventArgs):
        try:
            app.executeTextCommand('Command.SetBool id 0')
        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


class BackgroundThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self.running = True
    def run(self):
        while self.running:
            app: adsk.core.Application = adsk.core.Application.get()
            app.executeTextCommand('Command.SetBool id 0')
            self.running = False

    
def run(context):
    ui = None
    try:
        ui = app.userInterface
        cmdDefinitions = ui.commandDefinitions
        btnCmdDefinition = cmdDefinitions.itemById('testTrucCommand')
        if btnCmdDefinition:
            btnCmdDefinition.deleteMe()
        btnCmdDefinition = cmdDefinitions.addButtonDefinition('testTrucCommand', '', '')
        onCreated = CommandCreatedHandler()
        btnCmdDefinition.commandCreated.add(onCreated)
        handlers.append(onCreated)
        btnCmdDefinition.execute()
        adsk.autoTerminate(False)
        

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


def createNewComponent() -> adsk.fusion.Component:
    # Get the active design.
    product = app.activeProduct
    design = adsk.fusion.Design.cast(product)
    rootComp = design.rootComponent
    allOccs = rootComp.occurrences
    newOcc = allOccs.addNewComponent(adsk.core.Matrix3D.create())
    return newOcc.component


class TestFigure:
    
    def __init__(self) -> None:
        self.parameter = 0.4

    def build(self):
        newComp = createNewComponent()
        sketch: adsk.fusion.Sketch = newComp.sketches.add(newComp.xYConstructionPlane)
        sketch.sketchCurves.sketchLines.addCenterPointRectangle(adsk.core.Point3D.create(), adsk.core.Point3D.create(1, 1, 0))
        extrudes = newComp.features.extrudeFeatures
        distance = adsk.core.ValueInput.createByReal(1)
        extrude = extrudes.addSimple(sketch.profiles.item(0), distance, 0)
        edges = extrude.bodies.item(0).edges
        fillets = newComp.features.filletFeatures
        objColl: adsk.core.ObjectCollection = adsk.core.ObjectCollection.create()
        for edge in edges:
            objColl.add(edge)
        radius1Value = self.parameter
        while radius1Value > 0.2:
            try:
                input1 = fillets.createInput()
                radius1 = adsk.core.ValueInput.createByReal(radius1Value)
                input1.addConstantRadiusEdgeSet(objColl, radius1, True)
                fillet1 = fillets.add(input1)
                radius1Value = 0
            except:
                fillets = newComp.features.filletFeatures
                fillet1 = fillets.item(fillets.count - 1)
                if fillet1.healthState: # Si le dernier fillet crée est en bonne santé, la valeur est de 0
                    fillet1.deleteMe()
                radius1Value -= 0.5
                if radius1Value <= 0.2:
                    app.log('Echec de la création du congé entre la partie supérieure et la base de la fourche')
0 Likes
Accepted solutions (1)
929 Views
7 Replies
Replies (7)
Message 2 of 8

kandennti
Mentor
Mentor

Hi @tykapl.breuil .

 

I tried it, but it gave me an error and I didn't understand what it meant.

Does it mean that I want to make the fillet as large as possible?

0 Likes
Message 3 of 8

tykapl.breuil
Advocate
Advocate

The figure that I'm creating is just emulating the "reducing the fillet until it works workflow".

The bug I stumbled upon is that if a fillet fails (probably the case with other features too) in an execute preview caused by a custom event (specific I know) the whole fusion api comes crashing down (all calls fail). When executing normally, the command creates the figure with a small fillet at first, then if it creates the figure a second time, it creates it with a larget fillet that causes an error, notices that error then deletes the fillet and reduces the radius until it is able to be created. When executing the preview through a custom event however, when the first error happens, the whole api breakd like I've said earlier.

 

I don't know if I was clear enough in my explanation.

0 Likes
Message 4 of 8

kandennti
Mentor
Mentor

@tykapl.breuil .

 

I did not know that you cannot get Radius size from FilletFeature.

I created a function to execute the fillet repeatedly using Transaction without using Custom Events.

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core


def run(context):
    ui: adsk.core.UserInterface = None
    try:
        app: adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface
        app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)

        body: adsk.fusion.BRepBody = createCube(
            app.activeProduct.rootComponent,
            adsk.core.Point3D.create(0, 0, 0)
        )
        app.activeViewport.fit()

        try_Max_Size_fillet(body.edges, 0.01, 1, 0.01)

        ui.messageBox('Done')

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


def try_Max_Size_fillet(
        edges: list,
        radiusMin: float,
        radiusMax: float,
        pitch: float) -> adsk.fusion.FilletFeature:

    comp: adsk.fusion.Component = edges[0].body.parentComponent
    fillets: adsk.fusion.FilletFeatures = comp.features.filletFeatures

    edgeObjs: adsk.core.ObjectCollection = adsk.core.ObjectCollection.create()
    [edgeObjs.add(e) for e in edges]

    count = int((radiusMax - radiusMin) / pitch)

    radiusList = [radiusMax - (pitch * idx) for idx in range(count)]
    radiusList.append(radiusMin)

    app: adsk.core.Application = adsk.core.Application.get()

    resultIpt: adsk.fusion.FilletFeatureInput = None
    for radius in radiusList:
        app.log(f'try:{radius}')
        adsk.doEvents()
        filletIpt: adsk.fusion.FilletFeatureInput = fillets.createInput()
        filletIpt.addConstantRadiusEdgeSet(
            edgeObjs,
            adsk.core.ValueInput.createByReal(radius),
            True
        )
        try:
            app.executeTextCommand(u'Transaction.Start Try_Fillet')
            fillets.add(filletIpt)
            resultIpt = filletIpt
            break
        except:
            pass
        finally:
            app.executeTextCommand(u'Transaction.Abort')

    if resultIpt:
        return fillets.add(resultIpt)
    else:
        return None


def createCube(
        comp: adsk.fusion.Component,
        pnt: adsk.core.Point3D) -> adsk.fusion.BRepBody:

    vec3D = adsk.core.Vector3D
    lVec = vec3D.create(1.0, 0.0, 0.0)
    wVec = vec3D.create(0.0, 1.0, 0.0)

    bouBox3D = adsk.core.OrientedBoundingBox3D
    box = bouBox3D.create(pnt, lVec, wVec, 1, 1, 1)

    tmpMgr = adsk.fusion.TemporaryBRepManager.get()

    baseFeats = comp.features.baseFeatures
    baseFeat = baseFeats.add()
    baseFeat.startEdit()
    cube: adsk.fusion.BRepBody = comp.bRepBodies.add(
        tmpMgr.createBox(box), baseFeat)
    baseFeat.finishEdit()

    return cube

 

It simply iterates from larger size to smaller size, but if you use an algorithm like binary tree search, you should be able to get the result in less time.

0 Likes
Message 5 of 8

tykapl.breuil
Advocate
Advocate

Thank you very much for your help and i'll definetely look into transactions but this post was meant to report this weird bug, not actually find a solution to my problem !

 

EDIT : After further digging, I found that the bug also triggers when using the beginStep method in the execute event, so it is most likely a problem with events in general rather than just custom events like I thought.

See the code sample below :

import adsk.core, adsk.fusion, adsk.cam, traceback

# Affectations usuelles
handlers = []
ui = None
app : adsk.core.Application = adsk.core.Application.get()
if app:
    ui = app.userInterface

first = True


class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler):    
    def __init__(self):
        super().__init__()        
    def notify(self, args: adsk.core.CommandCreatedEventArgs):
        try:
            cmd = args.command
            inputs = cmd.commandInputs
            onExecute = CommandExecuteHandler()
            cmd.execute.add(onExecute)
            handlers.append(onExecute)

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

class CommandExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.CommandEventArgs):
        try:
            command: adsk.core.Command = args.firingEvent.sender
            testFigure = TestFigure()
            global first
            if not first:
                testFigure.parameter = 2
            testFigure.build()
            if first:
                first = False
                command.beginStep()
            else:
                adsk.autoTerminate(True)
        
        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

    
def run(context):
    ui = None
    try:
        ui = app.userInterface
        cmdDefinitions = ui.commandDefinitions
        btnCmdDefinition = cmdDefinitions.itemById('testTrucCommand')
        if btnCmdDefinition:
            btnCmdDefinition.deleteMe()
        btnCmdDefinition = cmdDefinitions.addButtonDefinition('testTrucCommand', '', '')
        onCreated = CommandCreatedHandler()
        btnCmdDefinition.commandCreated.add(onCreated)
        handlers.append(onCreated)
        btnCmdDefinition.execute()
        adsk.autoTerminate(False)
        

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


def createNewComponent() -> adsk.fusion.Component:
    # Get the active design.
    product = app.activeProduct
    design = adsk.fusion.Design.cast(product)
    rootComp = design.rootComponent
    allOccs = rootComp.occurrences
    newOcc = allOccs.addNewComponent(adsk.core.Matrix3D.create())
    return newOcc.component


class TestFigure:
    
    def __init__(self) -> None:
        self.parameter = 0.4

    def build(self):
        newComp = createNewComponent()
        sketch: adsk.fusion.Sketch = newComp.sketches.add(newComp.xYConstructionPlane)
        sketch.sketchCurves.sketchLines.addCenterPointRectangle(adsk.core.Point3D.create(), adsk.core.Point3D.create(1, 1, 0))
        extrudes = newComp.features.extrudeFeatures
        distance = adsk.core.ValueInput.createByReal(1)
        extrude = extrudes.addSimple(sketch.profiles.item(0), distance, 0)
        edges = extrude.bodies.item(0).edges
        fillets = newComp.features.filletFeatures
        objColl: adsk.core.ObjectCollection = adsk.core.ObjectCollection.create()
        for edge in edges:
            objColl.add(edge)
        radius1Value = self.parameter
        while radius1Value > 0.2:
            try:
                input1 = fillets.createInput()
                radius1 = adsk.core.ValueInput.createByReal(radius1Value)
                input1.addConstantRadiusEdgeSet(objColl, radius1, True)
                fillet1 = fillets.add(input1)
                radius1Value = 0
            except:
                fillets = newComp.features.filletFeatures
                fillet1 = fillets.item(fillets.count - 1)
                if fillet1.healthState: # Si le dernier fillet crée est en bonne santé, la valeur est de 0
                    fillet1.deleteMe()
                radius1Value -= 0.5
                if radius1Value <= 0.2:
                    app.log('Echec de la création du congé entre la partie supérieure et la base de la fourche')
0 Likes
Message 6 of 8

kandennti
Mentor
Mentor

@tykapl.breuil .

 

I don't understand the Command.beginStep method, but I think it is for Custom Features.

0 Likes
Message 7 of 8

tykapl.breuil
Advocate
Advocate

You can replace it with command.doExecute(False) and the problem still arises.

0 Likes
Message 8 of 8

tykapl.breuil
Advocate
Advocate
Accepted solution

I ended up solving the problem by finding this sample code :

https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-a4dc7230-60f3-11e5-a4e9-989096c56ffe

showing how to properly change a fillet features radius (it is possible).

By modifying the radius through this method, it is possible to try a fillet without causing an error properly and it solves my problem !

Thank you very much @kandennti for your help. Here is the function I ended using, based on the one you did for me :

def tryForFilletSize(edges: list, radiusMin: float, radiusMax: float, step: float, isRound: bool = False) -> adsk.fusion.FilletFeature:
    '''Crée un congé en testant plusieurs valeurs jusqu'à que le fillet puisse se créer.
    
    edges: Liste des edges pour le congé.
    radiusMin: Le rayon minimal à tester (attention, un fillet de taille 0 échouera toujours).
    Il est présumé que le fillet de rayon minimal fonctionera.
    step: L'écart entre les différents fillets à tester.
    isRound: Argument optionnnel dont la valeur par défaut est False spécifiant si la valeur 
    des fillets doit être arrondie au dixième de millimètre pour avoir des fillets rond.
    
    Retourne la FilletFeature créée.'''

    design: adsk.fusion.Design = app.activeProduct
    timeline: adsk.fusion.Timeline = design.timeline
    comp: adsk.fusion.Component = edges[0].body.parentComponent
    fillets: adsk.fusion.FilletFeatures = comp.features.filletFeatures
    edgeObjs: adsk.core.ObjectCollection = adsk.core.ObjectCollection.create()
    [edgeObjs.add(e) for e in edges]
    
    # Liste des valeurs à tester
    count = int((radiusMax - radiusMin) / step)
    if isRound:
        radiusList = [round(radiusMax - (step*idx), 2) for idx in range(count + 1)]
    else:
        radiusList = [radiusMax - (step * idx) for idx in range(count + 1)]
    if radiusList[-1] > radiusMin:
        radiusList.append(radiusMin)
    else:
        radiusList[-1] = radiusMin

    # Création de la première FilletFeature de rayon radiusMin sur laquelle
    # on va itérer en modifiant le rayon.
    filletIpt: adsk.fusion.FilletFeatureInput = fillets.createInput()
    filletIpt.addConstantRadiusEdgeSet(
        edgeObjs,
        adsk.core.ValueInput.createByReal(radiusMin),
        True
    )
    fillet = fillets.add(filletIpt)

    for radius in radiusList:
        edgeSets1 = fillet.edgeSets
        for edgeSet in edgeSets1:
            constantEdgeSet = adsk.fusion.ConstantRadiusFilletEdgeSet.cast(edgeSet)
            # Tester le fillet alors qu'il n'est pas dans la timeline permet
            # d'empêcher de soulever une erreur dans l'API -PB
            fillet.timelineObject.rollTo(True)
            # L'API fonctionnant en cm, on l'impose içi par soucis de cohérence -PB
            constantEdgeSet.radius.expression = f"{radius} cm"
            timeline.moveToEnd()
        if fillet.healthState == 0:
            break

    return fillet