Unlinked Copy Add-In Issues

Unlinked Copy Add-In Issues

carson_akasper
Observer Observer
87 Views
1 Reply
Message 1 of 2

Unlinked Copy Add-In Issues

carson_akasper
Observer
Observer

Hey everyone,

I'm developing a custom Add-In that creates true, independent copies of components within the same Fusion file. Unlike the standard copy-paste, which creates linked references, this Add-In exports the selected component as an .f3d file and then re-imports it, resulting in a fully unlinked, stand-alone copy.

However, I’ve run into a strange issue that I can best explain with an example:

I start with Part 1 and create an unlinked copy of it using my Add-In. I then create a joint between the original and the copy. Next, I make a new component called "Component A" and move both Part 1 and its unlinked copy into it. Then, I make an unlinked copy of Component A—this becomes "Component B." When I create a joint between Components A and B, things start to get weird. The internal joint in Component B appears to "split"—the blue half of the joint icon stays with a group of three parts, while the white half is left behind with the separate part. They should be connected, but they’re not. As soon as I move one of the parts, the separated part snaps back into place and the joint appears correct again.

However, when I then create another new component ("Component C") and move Components A and B into it, that same internal joint from Component B completely loses its references. The joint still exists, but it's no longer connected to either part—it just floats in space. So it seems like whenever an unlinked copy containing joints is reparented, those joints behave unpredictably—sometimes not updating immediately, and eventually losing their references entirely.

I'm wondering if anyone has insight into why this happens or how I might be able to address it programmatically.

0 Likes
88 Views
1 Reply
Reply (1)
Message 2 of 2

carson_akasper
Observer
Observer

This is my first time trying to write a program for fusion, so ChatGPT carried me through this bit, but I think I'm starting to grasp the concepts. Here is my code. I have three files: an Initialize file that has almost nothing in it, it just tells it to run, and then an entry file and an operation file. It's all in Python.

Entry file:

import adsk.core
import adsk.fusion
import os
from ...lib import fusionAddInUtils as futil
from ... import config
from . import operation

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

# Unique command ID and metadata
CMD_ID = 'unlinkedCopy'
CMD_NAME = 'Unlinked Copy'
CMD_DESCRIPTION = 'Creates unlinked copies of the selected components'
IS_PROMOTED = True

# Panel was created in commands/__init__.py
WORKSPACE_ID = 'FusionSolidEnvironment'
PANEL_ID = 'ExpandedAssemblyPanel'
COMMAND_BESIDE_ID = 'ScriptsManagerCommand'

ICON_FOLDER = os.path.join(os.path.dirname(__file__), 'resources', '')

# Local list to prevent garbage collection
local_handlers = []

def start():
    try:
        cmd_def = ui.commandDefinitions.itemById(CMD_ID)
        if cmd_def:
            cmd_def.deleteMe()

        cmd_def = ui.commandDefinitions.addButtonDefinition(
            CMD_ID, CMD_NAME, CMD_DESCRIPTION, ICON_FOLDER
        )

        futil.add_handler(cmd_def.commandCreated, command_created)

        panel = ui.allToolbarPanels.itemById(PANEL_ID)
        if panel and not panel.controls.itemById(CMD_ID😞
            control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False)
            control.isPromoted = IS_PROMOTED

    except Exception as e:
        futil.handle_error(f'start: {str(e)}')

def stop():
    try:
        panel = ui.allToolbarPanels.itemById(PANEL_ID)
        if panel:
            control = panel.controls.itemById(CMD_ID)
            if control:
                control.deleteMe()

        cmd_def = ui.commandDefinitions.itemById(CMD_ID)
        if cmd_def:
            cmd_def.deleteMe()

    except Exception as e:
        futil.handle_error(f'stop: {str(e)}')

def command_created(args: adsk.core.CommandCreatedEventArgs😞
    futil.log(f'{CMD_NAME} Command Created')
    command = args.command
    inputs = command.commandInputs

    # Selection input (allow multiple components to be selected)
    select_input = inputs.addSelectionInput(
        'target_components',
        'Select Components',
        'Choose the components to copy'
    )
    select_input.addSelectionFilter('Occurrences')
    select_input.setSelectionLimits(1, 0)  # Allow multiple selections

    futil.add_handler(command.execute, command_execute, local_handlers=local_handlers)
    futil.add_handler(command.destroy, command_destroy, local_handlers=local_handlers)

def command_execute(args: adsk.core.CommandEventArgs😞
    # Get the selected occurrences from the command inputs
    command = args.command
    inputs = command.commandInputs
    select_input = inputs.itemById('target_components')
   
    if not select_input or select_input.selectionCount == 0:
        ui.messageBox('No components selected to copy.')
        return

    # Extract occurrences
    occurrences = [adsk.fusion.Occurrence.cast(select_input.selection(i).entity) for i in range(select_input.selectionCount)]
   
    # Now call the operation with both arguments
    operation.run_operation(args, occurrences)

def command_destroy(args: adsk.core.CommandEventArgs😞
    futil.log(f'{CMD_NAME} Command Destroyed')
    global local_handlers
    local_handlers = []

Operation file:

import adsk.core
import adsk.fusion
import os
import tempfile

def sanitize_filename(name: str) -> str:
    """Sanitize the component name to remove invalid characters."""
    invalid_chars = '<>:"/\\|?*'
    return ''.join(c for c in name if c not in invalid_chars)

def run_operation(args, occurrences😞
    app = adsk.core.Application.get()
    ui = app.userInterface
    design = adsk.fusion.Design.cast(app.activeProduct)
    root_comp = design.rootComponent

    export_mgr = design.exportManager
    import_mgr = app.importManager
    temp_dir = tempfile.gettempdir()

    for occ in occurrences:
        try:
            # Generate a safe filename
            safe_name = sanitize_filename(occ.component.name + " UC")
            temp_path = os.path.join(temp_dir, f'{safe_name}_temp.f3d')

            # Export the component to an .f3d file
            export_options = export_mgr.createFusionArchiveExportOptions(temp_path, occ.component)
            export_options.isComponentNative = True

            if not export_mgr.execute(export_options😞
                ui.messageBox(f' Failed to export component: {occ.component.name}')
                continue

            if not os.path.exists(temp_path) or os.path.getsize(temp_path) < 1000:
                ui.messageBox(f"⚠ Exported file is missing or invalid for {occ.component.name}: {temp_path}")
                continue

            before_count = root_comp.occurrences.count

            # Import the component back into the root
            import_options = import_mgr.createFusionArchiveImportOptions(temp_path)
            result = import_mgr.importToTarget(import_options, root_comp)

            if not result:
                ui.messageBox(f" Import returned False for {occ.component.name}")
                continue

            # Get the new occurrence
            after_count = root_comp.occurrences.count
            if after_count <= before_count:
                ui.messageBox(f'⚠ Import succeeded but no new component found for {occ.component.name}')
                continue

            new_occ = root_comp.occurrences.item(after_count - 1)

            if new_occ.isReferencedComponent:
                new_occ.breakLink()

            new_occ.component.name = safe_name

            # Cleanup temp file
            if os.path.exists(temp_path😞
                os.remove(temp_path)

        except Exception as e:
            ui.messageBox(f" Error during processing {occ.component.name}:\n{e}")



0 Likes