Custom Event Sample

Description

Demonstrates the ability to call into the main thread from a worker thread. This sample is an add-in. To use it, use the Scripts and Add-Ins command to create a new add-in. Delete all of the code in the newly created add-in and replace it with the code below. Have a model open that has a parameter named "Length". Load the add-in. The add-in will change the value of the parameter every two seconds using a random value between 1 and 10.

Code Samples

/**
 * Custom Event Sample
 * Demonstrates the ability to call into the main thread from a worker thread. This sample is an add-in . To use it, use the Scripts and Add-Ins command to create a new add-in. Delete all of the code in the newly created...
 */

import { adsk } from "@adsk/fusion";


const TEST_PREFIX = "CustomEventTest";
const DEFAULT_TIMEOUT_MS = 5000;

function assert(condition: boolean, message: string): void {
    if (!condition) {
        throw new Error(`${TEST_PREFIX} FAIL: ${message}`);
    }
}

/** `fireCustomEvent` queues work (see FireCustomEventTask); pump until `pred` holds or timeout. */
function pumpUntil(pred: () => boolean, label: string, timeoutMs: number = DEFAULT_TIMEOUT_MS): void {

    const start = Date.now();
    while (!pred() && Date.now() - start < timeoutMs) {
        adsk.doEvents();
    }
    if (!pred()) {
        throw new Error(`${TEST_PREFIX} FAIL: "${label}" did not complete within ${timeoutMs}ms`);
    }
}


function pumpFor(durationMs: number): void {
    const start = Date.now();
    while (Date.now() - start < durationMs) {
        adsk.doEvents();
    }
}

function fireAndWait(
    app: any,
    eventId: string,
    additionalInfo: string,
    pred: () => boolean,
    label: string,
    timeoutMs: number = DEFAULT_TIMEOUT_MS
): void {
    (app as any).fireCustomEvent(eventId, additionalInfo);
    pumpUntil(pred, label, timeoutMs);
}

function fireAndSettle(app: any, eventId: string, additionalInfo: string, settleMs: number = 200): void {
    (app as any).fireCustomEvent(eventId, additionalInfo);
    pumpFor(settleMs);
}


// ---------------------------------------------------------------------------
// 1. Basic lifecycle: register → add handler → fire → verify → remove → unregister
// ---------------------------------------------------------------------------

function testBasicLifecycle(): void {
    adsk.log(`${TEST_PREFIX}: basic lifecycle...`);
    const app = adsk.core.Application.get();

    const eventId = "CustomEventTest_Basic";
    const customEvent = (app as any).registerCustomEvent(eventId);
    assert(
        customEvent !== null && customEvent !== undefined,
        "registerCustomEvent must return a non-null event object"
    );

    let notifyCount = 0;
    let lastInfo = "";
    const handler: any = {
        notify: (args: any) => {
            notifyCount++;
            try {
                if (args && args.additionalInfo) {
                    lastInfo = args.additionalInfo;
                }
            } catch (_) {
                /* additionalInfo may not be available */
            }
            adsk.log(`${TEST_PREFIX}: handler notified (count=${notifyCount})`);
        },
    };

    customEvent.add(handler);

    fireAndWait(app, eventId, "payload-1", () => notifyCount >= 1, "basic lifecycle first notify");

    assert(notifyCount >= 1, `handler must fire at least once, got ${notifyCount}`);

    fireAndWait(app, eventId, "payload-2", () => notifyCount >= 2, "basic lifecycle second notify");

    assert(notifyCount >= 2, `handler must fire at least twice, got ${notifyCount}`);

    customEvent.remove(handler);

    const countAfterRemove = notifyCount;
    fireAndSettle(app, eventId, "post-remove");

    assert(
        notifyCount === countAfterRemove,
        "handler must not fire after remove"
    );

    (app as any).unregisterCustomEvent(eventId);
    adsk.log(`${TEST_PREFIX}: basic lifecycle PASSED`);
}

// ---------------------------------------------------------------------------
// 2. Duplicate add ΓÇö same handler object added twice is a no-op
// ---------------------------------------------------------------------------

function testDuplicateAdd(): void {
    adsk.log(`${TEST_PREFIX}: duplicate add...`);
    const app = adsk.core.Application.get();

    const eventId = "CustomEventTest_DupAdd";
    const customEvent = (app as any).registerCustomEvent(eventId);

    let notifyCount = 0;
    const handler: any = {
        notify: () => {
            notifyCount++;
        },
    };

    customEvent.add(handler);
    customEvent.add(handler);

    fireAndWait(app, eventId, "dup-add", () => notifyCount >= 1, "duplicate add notify");

    assert(
        notifyCount === 1,
        `duplicate add: second add is a no-op (${notifyCount} notifies ΓÇö expected 1; if 2, native dedupe may be missing)`
    );

    customEvent.remove(handler);
    (app as any).unregisterCustomEvent(eventId);
    adsk.log(`${TEST_PREFIX}: duplicate add PASSED`);
}

// ---------------------------------------------------------------------------
// 3. Remove-after-remove ΓÇö second remove is a safe no-op
// ---------------------------------------------------------------------------

function testRemoveAfterRemove(): void {
    adsk.log(`${TEST_PREFIX}: remove after remove...`);
    const app = adsk.core.Application.get();

    const eventId = "CustomEventTest_RemRem";
    const customEvent = (app as any).registerCustomEvent(eventId);

    const handler: any = { notify: () => {} };
    customEvent.add(handler);
    customEvent.remove(handler);
    customEvent.remove(handler);

    (app as any).unregisterCustomEvent(eventId);
    adsk.log(`${TEST_PREFIX}: remove after remove PASSED`);
}

// ---------------------------------------------------------------------------
// 4a. Multiple distinct handlers on the same event (concurrent contract)
// ---------------------------------------------------------------------------

function testMultipleHandlersConcurrent(): void {
    adsk.log(`${TEST_PREFIX}: multiple handlers (concurrent)...`);
    const app = adsk.core.Application.get();

    const eventId = "CustomEventTest_MultiConcurrent";
    const customEvent = (app as any).registerCustomEvent(eventId);

    let count1 = 0;
    let count2 = 0;
    const handler1: any = { notify: () => { count1++; } };
    const handler2: any = { notify: () => { count2++; } };

    customEvent.add(handler1);
    customEvent.add(handler2);

    fireAndWait(
        app,
        eventId,
        "multi-concurrent-1",
        () => count1 >= 1 && count2 >= 1,
        "concurrent handlers first notify"
    );

    assert(count1 >= 1, `handler1 must fire, got ${count1}`);
    assert(count2 >= 1, `handler2 must fire, got ${count2}`);

    customEvent.remove(handler1);
    const prevCount1 = count1;
    const prevCount2 = count2;

    fireAndWait(
        app,
        eventId,
        "multi-concurrent-2",
        () => count1 === prevCount1 && count2 > prevCount2,
        "concurrent handlers after removing handler1"
    );
    assert(count1 === prevCount1, "handler1 must not fire after removal");
    assert(count2 > prevCount2, "handler2 must still fire after handler1 removal");

    customEvent.remove(handler2);
    (app as any).unregisterCustomEvent(eventId);
    adsk.log(`${TEST_PREFIX}: multiple handlers (concurrent) PASSED`);
}

// ---------------------------------------------------------------------------
// 4b. Distinct handlers can each be attached and notified on one event
// ---------------------------------------------------------------------------

function testMultipleHandlersSequential(): void {
    adsk.log(`${TEST_PREFIX}: multiple handlers (sequential)...`);
    const app = adsk.core.Application.get();

    const eventId = "CustomEventTest_MultiSequential";
    const customEvent = (app as any).registerCustomEvent(eventId);

    let count1 = 0;
    let count2 = 0;
    const handler1: any = { notify: () => { count1++; } };
    const handler2: any = { notify: () => { count2++; } };

    customEvent.add(handler1);
    fireAndWait(app, eventId, "multi-sequential-1", () => count1 >= 1, "sequential handler1 notify");
    assert(count1 >= 1, `handler1 must fire, got ${count1}`);
    customEvent.remove(handler1);

    customEvent.add(handler2);
    fireAndWait(app, eventId, "multi-sequential-2", () => count2 >= 1, "sequential handler2 notify");
    assert(
        count2 >= 1,
        `handler2 must fire when added after handler1 removal, got ${count2}`
    );
    customEvent.remove(handler2);

    const prevCount1 = count1;
    const prevCount2 = count2;
    fireAndSettle(app, eventId, "multi-sequential-3");
    assert(count1 === prevCount1, "handler1 must not fire after removal");
    assert(count2 === prevCount2, "handler2 must not fire after removal");
    (app as any).unregisterCustomEvent(eventId);
    adsk.log(`${TEST_PREFIX}: multiple handlers (sequential) PASSED`);
}

// ---------------------------------------------------------------------------
// 5. Handler left registered ΓÇö unregisterCustomEvent should clean up
// ---------------------------------------------------------------------------

function testUnregisterCleansUp(): void {
    adsk.log(`${TEST_PREFIX}: unregister cleans up...`);
    const app = adsk.core.Application.get();

    const eventId = "CustomEventTest_Unreg";
    const customEvent = (app as any).registerCustomEvent(eventId);

    let notifyCount = 0;
    const handler: any = { notify: () => { notifyCount++; } };
    customEvent.add(handler);

    fireAndWait(app, eventId, "unreg", () => notifyCount >= 1, "unregister cleanup precondition");

    assert(notifyCount >= 1, "handler must fire before unregister");

    (app as any).unregisterCustomEvent(eventId);

    adsk.log(`${TEST_PREFIX}: unregister cleans up PASSED`);
}

// ---------------------------------------------------------------------------
// Run all tests
// ---------------------------------------------------------------------------

adsk.log(`${TEST_PREFIX}: starting tests...`);
testBasicLifecycle();
testDuplicateAdd();
testRemoveAfterRemove();
testMultipleHandlersConcurrent();
testMultipleHandlersSequential();
testUnregisterCleansUp();
adsk.log(`${TEST_PREFIX}: all tests PASSED`);
#include <Core/Utils.h>
#include <Core/Application/Application.h>
#include <Core/Application/Product.h>
#include <Core/Application/CustomEvents.h>
#include <Core/UserInterface/UserInterface.h>
#include <Core/UserInterface/CommandDefinitions.h>
#include <Core/UserInterface/CommandDefinition.h>
#include <Fusion/Fusion/Design.h>
#include <Fusion/Components/Component.h>
#include <Fusion/Fusion/ModelParameters.h>
#include <Fusion/Fusion/ModelParameter.h>

#include <thread>
#include <sstream>
#include <chrono>
#include <random>

using namespace adsk::core;
using namespace adsk::fusion;

const std::string myCustomEvent = "MyCustomEventId1";

Ptr<Application> app;
Ptr<UserInterface> ui;
Ptr<CustomEvent> customEvent;
bool stopFlag;
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(1000, 10000);

class ThreadEventHandler : public CustomEventHandler
{
  public:
    void notify(const Ptr<CustomEventArgs>& eventArgs) override
    {
        if (eventArgs)
        {
            // Make sure a command isn't running before changes are made.
            if (ui->activeCommand() != "SelectCommand")
            {
                Ptr<CommandDefinitions> cmdDefs = ui->commandDefinitions();
                cmdDefs->itemById("SelectCommand")->execute();
            }

            Ptr<Design> design = app->activeProduct();
            if (!design)
                return;

            Ptr<Component> rootComp = design->rootComponent();
            if (!rootComp)
                return;

            Ptr<ModelParameters> params = rootComp->modelParameters();
            if (!params)
                return;

            Ptr<ModelParameter> param = params->itemByName("Length");
            if (!param)
                return;

            // Get the value that was passed in from other thread and set the paraemter value.
            std::string info = eventArgs->additionalInfo();
            param->value(std::stod(info));
        }
    }
} onCustomEvent_;

void myThreadRun()
{
    while (!stopFlag)
    {
        double randVal = distribution(generator);
        std::string additionalInfo = std::to_string(randVal / 1000.0);
        app->fireCustomEvent(myCustomEvent, additionalInfo);

        std::this_thread::sleep_for(std::chrono::seconds(2));
    }
}

extern "C" XI_EXPORT bool run(const char* context)
{
    app = Application::get();
    if (!app)
        return false;

    ui = app->userInterface();
    if (!ui)
        return false;

    customEvent = app->registerCustomEvent(myCustomEvent);
    if (!customEvent)
        return false;
    customEvent->add(&onCustomEvent_);

    stopFlag = false;
    std::thread myThread(myThreadRun);
    myThread.detach();

    return true;
}

extern "C" XI_EXPORT bool stop(const char* context)
{
    if (ui)
    {
        customEvent->remove(&onCustomEvent_);
        stopFlag = true;
        app->unregisterCustomEvent(myCustomEvent);
        ui->messageBox("Stop addin");
        ui = nullptr;
    }

    return true;
}
#Author-
#Description-

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

app = None
ui = adsk.core.UserInterface.cast(None)
handlers = []
stopFlag = None
myCustomEvent = 'MyCustomEventId'
customEvent = None


# The event handler that responds to the custom event being fired.
class ThreadEventHandler(adsk.core.CustomEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            # Make sure a command isn't running before changes are made.
            if ui.activeCommand != 'SelectCommand':
                ui.commandDefinitions.itemById('SelectCommand').execute()
                            
            # Get the value from the JSON data passed through the event.
            eventArgs = json.loads(args.additionalInfo)
            newValue = float(eventArgs['Value'])
            
            # Set the parameter value.
            design = adsk.fusion.Design.cast(app.activeProduct)
            param = design.rootComponent.modelParameters.itemByName('Length')
            param.value = newValue
        except:
            if ui:
                ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
            adsk.autoTerminate(False)


# The class for the new thread.
class MyThread(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.stopped = event

    def run(self):
        # Every five seconds fire a custom event, passing a random number.
        while not self.stopped.wait(2):
            args = {'Value': random.randint(1000, 10000)/1000}
            app.fireCustomEvent(myCustomEvent, json.dumps(args)) 
        
        
def run(context):
    global ui
    global app
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface
        
        # Register the custom event and connect the handler.
        global customEvent
        customEvent = app.registerCustomEvent(myCustomEvent)
        onThreadEvent = ThreadEventHandler()
        customEvent.add(onThreadEvent)
        handlers.append(onThreadEvent)

        # Create a new thread for the other processing.        
        global stopFlag        
        stopFlag = threading.Event()
        myThread = MyThread(stopFlag)
        myThread.start()
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def stop(context):
    try:
        if handlers.count:
            customEvent.remove(handlers[0])
        stopFlag.set() 
        app.unregisterCustomEvent(myCustomEvent)
        ui.messageBox('Stop addin')
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))