diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..1c69300
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,20 @@
+{
+ "version": "0.2.0",
+ "configurations": [{
+ "name": "Python: Attach",
+ "type": "python",
+ "request": "attach",
+ "pathMappings": [{
+ "localRoot": "${workspaceRoot}",
+ "remoteRoot": "${workspaceRoot}"
+ }],
+ "osx": {
+ "filePath": "${file}"
+ },
+ "windows": {
+ "filePath": "${file}"
+ },
+ "port": 9000,
+ "host": "localhost"
+ }]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f1a38ec
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "python.autoComplete.extraPaths": ["C:/Users/t.bassi/AppData/Roaming/Autodesk/Autodesk Fusion 360/API/Python/defs"],
+ "python.analysis.extraPaths": ["C:/Users/t.bassi/AppData/Roaming/Autodesk/Autodesk Fusion 360/API/Python/defs"],
+ "python.defaultInterpreterPath": "C:/Users/t.bassi/AppData/Local/Autodesk/webdeploy/production/b4ebb90d69b5fc8cf013f75341ee2a1192c9da8e/Python/python.exe"
+}
\ No newline at end of file
diff --git a/AddInIcon.svg b/AddInIcon.svg
new file mode 100644
index 0000000..dc82408
--- /dev/null
+++ b/AddInIcon.svg
@@ -0,0 +1,2576 @@
+
+
+
+
diff --git a/LocalSave.py b/LocalSave.py
new file mode 100644
index 0000000..4d5b658
--- /dev/null
+++ b/LocalSave.py
@@ -0,0 +1,142 @@
+import adsk.core, adsk.fusion, adsk.cam, traceback
+import os
+import uuid
+
+app = None
+ui = None
+handlers = []
+
+def onExecuteSaveAs(args):
+ try:
+ doc = app.activeDocument
+ design = app.activeProduct
+
+ if not hasattr(design, 'exportManager'):
+ ui.messageBox("Current document cannot be exported.")
+ return
+
+ export_mgr = design.exportManager
+
+ # Show file save dialog
+ file_dialog = ui.createFileDialog()
+ file_dialog.title = "Export As"
+ file_dialog.filter = "Fusion 360 Files (*.f3d)"
+
+ if doc.dataFile:
+ initial_dir = os.path.dirname(doc.dataFile.name)
+ file_dialog.initialDirectory = initial_dir
+
+ if file_dialog.showSave() != adsk.core.DialogResults.DialogOK:
+ return
+
+ file_path = file_dialog.filename
+ file_ext = os.path.splitext(file_path)[1].lower()
+
+ # Verify file extension
+ if file_ext not in ['.f3d']:
+ ui.messageBox("Invalid file type. Please export as .f3d")
+ return
+
+ # Create appropriate export options based on file extension
+ options = export_mgr.createFusionArchiveExportOptions(file_path)
+
+ export_mgr.execute(options)
+
+ ui.messageBox(f"File exported to: {file_path}")
+ except Exception as e:
+ if ui:
+ ui.messageBox(f"Failed to export file: {e}")
+
+class SaveCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
+ def __init__(self, execute_function):
+ super().__init__()
+ self.execute_function = execute_function
+
+ def notify(self, args):
+ try:
+ command = args.command
+ on_execute_handler = SaveCommandExecuteHandler(self.execute_function)
+ command.execute.add(on_execute_handler)
+ handlers.append(on_execute_handler)
+ except Exception as e:
+ if ui:
+ ui.messageBox(f"Failed to create command: {e}")
+
+class SaveCommandExecuteHandler(adsk.core.CommandEventHandler):
+ def __init__(self, execute_function):
+ super().__init__()
+ self.execute_function = execute_function
+
+ def notify(self, args):
+ try:
+ self.execute_function(args)
+ except Exception as e:
+ if ui:
+ ui.messageBox(f"Failed to execute command: {e}")
+
+def run(context):
+ global app, ui
+ app = adsk.core.Application.get()
+ ui = app.userInterface
+
+ try:
+ tab_id = f'LocalSaveTab_{uuid.uuid4()}'
+ panel_id = f'LocalSavePanel_{uuid.uuid4()}'
+ save_as_command_id = f"SaveAsLocalCommand_{uuid.uuid4()}"
+
+ current_dir = os.path.dirname(os.path.abspath(__file__))
+ save_as_icon_path = os.path.join(current_dir, 'resources', '32x32-saveas.png')
+ panel_icon_path = os.path.join(current_dir, 'resources', '32x32-panel.png')
+
+ workspace = ui.workspaces.itemById('FusionSolidEnvironment')
+
+ for tab in workspace.toolbarTabs:
+ if tab.id.startswith('LocalSaveTab'):
+ tab.deleteMe()
+ break
+
+ tab = workspace.toolbarTabs.add(tab_id, 'Local Save')
+ panel = tab.toolbarPanels.add(panel_id, 'Save Options', panel_icon_path)
+
+ try:
+ save_as_command_def = ui.commandDefinitions.addButtonDefinition(
+ save_as_command_id,
+ 'Save',
+ 'Save the file locally. Use this for initial save and override of file.',
+ save_as_icon_path
+ )
+
+ save_as_created_handler = SaveCommandCreatedHandler(onExecuteSaveAs)
+ save_as_command_def.commandCreated.add(save_as_created_handler)
+
+ handlers.append(save_as_created_handler)
+
+ panel.controls.addCommand(save_as_command_def)
+
+ except Exception as e:
+ ui.messageBox(f"Error creating commands: {e}")
+ return
+
+ except Exception as e:
+ if ui:
+ ui.messageBox(f"Failed to run the plugin: {e}")
+
+def stop(context):
+ try:
+ workspace = ui.workspaces.itemById('FusionSolidEnvironment')
+ if workspace:
+ tab = workspace.toolbarTabs.itemById('LocalSaveTab')
+ if tab:
+ tab.deleteMe()
+
+ command_definitions_to_delete = []
+ for cmd_def in ui.commandDefinitions:
+ if cmd_def.id.startswith('SaveAsLocalCommand_'):
+ command_definitions_to_delete.append(cmd_def)
+
+ for cmd_def in command_definitions_to_delete:
+ cmd_def.deleteMe()
+
+ except Exception as e:
+ if ui:
+ ui.messageBox(f"Failed to stop the plugin: {e}")
\ No newline at end of file
diff --git a/README.md b/README.md
index 8420fc5..383113a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,24 @@
-# Fusion360-LocalSave
+Fusion 360 Local Save Extension
+This Fusion 360 extension brings back easy local saving by letting you export .f3d files directly to your computer, bypassing the cloud-only model almost completely. Conversion and native export are still needed for f3z archives.
+## Features
+Adds a "Local Save" tab with a "Save" button
+
+Saves the current document as a local .f3d file
+
+Ideal for backups, version control, and offline work
+
+## How to Use
+Copy the script folder into your Fusion 360 Add-Ins folder:
+`~/Library/Application Support/Autodesk/Autodesk Fusion 360/API/AddIns` (macOS)
+
+`C:\Users\\AppData\Roaming\Autodesk\Autodesk Fusion 360\API\AddIns\` (Windows)
+
+Open Fusion 360 → Tools → Scripts and Add-Ins
+
+Find the extension under the `My Add-Ins` tab and click Run
+
+Use the new "Local Save" tab in the toolbar to export files
+
+## Output
+Exports files in .f3d format, compatible with Fusion 360 for re-import or sharing.
\ No newline at end of file
diff --git a/commands/__init__.py b/commands/__init__.py
new file mode 100644
index 0000000..0499301
--- /dev/null
+++ b/commands/__init__.py
@@ -0,0 +1,30 @@
+# Here you define the commands that will be added to your add-in.
+
+# TODO Import the modules corresponding to the commands you created.
+# If you want to add an additional command, duplicate one of the existing directories and import it here.
+# You need to use aliases (import "entry" as "my_module") assuming you have the default module named "entry".
+from .commandDialog import entry as commandDialog
+from .paletteShow import entry as paletteShow
+from .paletteSend import entry as paletteSend
+
+# TODO add your imported modules to this list.
+# Fusion will automatically call the start() and stop() functions.
+commands = [
+ commandDialog,
+ paletteShow,
+ paletteSend
+]
+
+
+# Assumes you defined a "start" function in each of your modules.
+# The start function will be run when the add-in is started.
+def start():
+ for command in commands:
+ command.start()
+
+
+# Assumes you defined a "stop" function in each of your modules.
+# The stop function will be run when the add-in is stopped.
+def stop():
+ for command in commands:
+ command.stop()
\ No newline at end of file
diff --git a/commands/commandDialog/__init__.py b/commands/commandDialog/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/commands/commandDialog/entry.py b/commands/commandDialog/entry.py
new file mode 100644
index 0000000..5a4c514
--- /dev/null
+++ b/commands/commandDialog/entry.py
@@ -0,0 +1,158 @@
+import adsk.core
+import os
+from ...lib import fusionAddInUtils as futil
+from ... import config
+app = adsk.core.Application.get()
+ui = app.userInterface
+
+
+# TODO *** Specify the command identity information. ***
+CMD_ID = f'{config.COMPANY_NAME}_{config.ADDIN_NAME}_cmdDialog'
+CMD_NAME = 'Command Dialog Sample'
+CMD_Description = 'A Fusion Add-in Command with a dialog'
+
+# Specify that the command will be promoted to the panel.
+IS_PROMOTED = True
+
+# TODO *** Define the location where the command button will be created. ***
+# This is done by specifying the workspace, the tab, and the panel, and the
+# command it will be inserted beside. Not providing the command to position it
+# will insert it at the end.
+WORKSPACE_ID = 'FusionSolidEnvironment'
+PANEL_ID = 'SolidScriptsAddinsPanel'
+COMMAND_BESIDE_ID = 'ScriptsManagerCommand'
+
+# Resource location for command icons, here we assume a sub folder in this directory named "resources".
+ICON_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', '')
+
+# Local list of event handlers used to maintain a reference so
+# they are not released and garbage collected.
+local_handlers = []
+
+
+# Executed when add-in is run.
+def start():
+ # Create a command Definition.
+ cmd_def = ui.commandDefinitions.addButtonDefinition(CMD_ID, CMD_NAME, CMD_Description, ICON_FOLDER)
+
+ # Define an event handler for the command created event. It will be called when the button is clicked.
+ futil.add_handler(cmd_def.commandCreated, command_created)
+
+ # ******** Add a button into the UI so the user can run the command. ********
+ # Get the target workspace the button will be created in.
+ workspace = ui.workspaces.itemById(WORKSPACE_ID)
+
+ # Get the panel the button will be created in.
+ panel = workspace.toolbarPanels.itemById(PANEL_ID)
+
+ # Create the button command control in the UI after the specified existing command.
+ control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False)
+
+ # Specify if the command is promoted to the main toolbar.
+ control.isPromoted = IS_PROMOTED
+
+
+# Executed when add-in is stopped.
+def stop():
+ # Get the various UI elements for this command
+ workspace = ui.workspaces.itemById(WORKSPACE_ID)
+ panel = workspace.toolbarPanels.itemById(PANEL_ID)
+ command_control = panel.controls.itemById(CMD_ID)
+ command_definition = ui.commandDefinitions.itemById(CMD_ID)
+
+ # Delete the button command control
+ if command_control:
+ command_control.deleteMe()
+
+ # Delete the command definition
+ if command_definition:
+ command_definition.deleteMe()
+
+
+# Function that is called when a user clicks the corresponding button in the UI.
+# This defines the contents of the command dialog and connects to the command related events.
+def command_created(args: adsk.core.CommandCreatedEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Command Created Event')
+
+ # https://help.autodesk.com/view/fusion360/ENU/?contextId=CommandInputs
+ inputs = args.command.commandInputs
+
+ # TODO Define the dialog for your command by adding different inputs to the command.
+
+ # Create a simple text box input.
+ inputs.addTextBoxCommandInput('text_box', 'Some Text', 'Enter some text.', 1, False)
+
+ # Create a value input field and set the default using 1 unit of the default length unit.
+ defaultLengthUnits = app.activeProduct.unitsManager.defaultLengthUnits
+ default_value = adsk.core.ValueInput.createByString('1')
+ inputs.addValueInput('value_input', 'Some Value', defaultLengthUnits, default_value)
+
+ # TODO Connect to the events that are needed by this command.
+ futil.add_handler(args.command.execute, command_execute, local_handlers=local_handlers)
+ futil.add_handler(args.command.inputChanged, command_input_changed, local_handlers=local_handlers)
+ futil.add_handler(args.command.executePreview, command_preview, local_handlers=local_handlers)
+ futil.add_handler(args.command.validateInputs, command_validate_input, local_handlers=local_handlers)
+ futil.add_handler(args.command.destroy, command_destroy, local_handlers=local_handlers)
+
+
+# This event handler is called when the user clicks the OK button in the command dialog or
+# is immediately called after the created event not command inputs were created for the dialog.
+def command_execute(args: adsk.core.CommandEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Command Execute Event')
+
+ # TODO ******************************** Your code here ********************************
+
+ # Get a reference to your command's inputs.
+ inputs = args.command.commandInputs
+ text_box: adsk.core.TextBoxCommandInput = inputs.itemById('text_box')
+ value_input: adsk.core.ValueCommandInput = inputs.itemById('value_input')
+
+ # Do something interesting
+ text = text_box.text
+ expression = value_input.expression
+ msg = f'Your text: {text} Your value: {expression}'
+ ui.messageBox(msg)
+
+
+# This event handler is called when the command needs to compute a new preview in the graphics window.
+def command_preview(args: adsk.core.CommandEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Command Preview Event')
+ inputs = args.command.commandInputs
+
+
+# This event handler is called when the user changes anything in the command dialog
+# allowing you to modify values of other inputs based on that change.
+def command_input_changed(args: adsk.core.InputChangedEventArgs):
+ changed_input = args.input
+ inputs = args.inputs
+
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Input Changed Event fired from a change to {changed_input.id}')
+
+
+# This event handler is called when the user interacts with any of the inputs in the dialog
+# which allows you to verify that all of the inputs are valid and enables the OK button.
+def command_validate_input(args: adsk.core.ValidateInputsEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Validate Input Event')
+
+ inputs = args.inputs
+
+ # Verify the validity of the input values. This controls if the OK button is enabled or not.
+ valueInput = inputs.itemById('value_input')
+ if valueInput.value >= 0:
+ args.areInputsValid = True
+ else:
+ args.areInputsValid = False
+
+
+# This event handler is called when the command terminates.
+def command_destroy(args: adsk.core.CommandEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Command Destroy Event')
+
+ global local_handlers
+ local_handlers = []
diff --git a/commands/commandDialog/resources/16x16.png b/commands/commandDialog/resources/16x16.png
new file mode 100644
index 0000000..03babdc
Binary files /dev/null and b/commands/commandDialog/resources/16x16.png differ
diff --git a/commands/commandDialog/resources/32x32.png b/commands/commandDialog/resources/32x32.png
new file mode 100644
index 0000000..863b2e3
Binary files /dev/null and b/commands/commandDialog/resources/32x32.png differ
diff --git a/commands/commandDialog/resources/64x64.png b/commands/commandDialog/resources/64x64.png
new file mode 100644
index 0000000..dd285fd
Binary files /dev/null and b/commands/commandDialog/resources/64x64.png differ
diff --git a/commands/paletteSend/__init__.py b/commands/paletteSend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/commands/paletteSend/entry.py b/commands/paletteSend/entry.py
new file mode 100644
index 0000000..dfdc9d5
--- /dev/null
+++ b/commands/paletteSend/entry.py
@@ -0,0 +1,149 @@
+import json
+import adsk.core
+import os
+from ...lib import fusionAddInUtils as futil
+from ... import config
+
+app = adsk.core.Application.get()
+ui = app.userInterface
+
+# TODO ********************* Change these names *********************
+CMD_ID = f'{config.COMPANY_NAME}_{config.ADDIN_NAME}_palette_send'
+CMD_NAME = 'Send to Palette'
+CMD_Description = 'Send some information to the palette'
+IS_PROMOTED = False
+
+# Using "global" variables by referencing values from /config.py
+PALETTE_ID = config.sample_palette_id
+
+# TODO *** Define the location where the command button will be created. ***
+# This is done by specifying the workspace, the tab, and the panel, and the
+# command it will be inserted beside. Not providing the command to position it
+# will insert it at the end.
+WORKSPACE_ID = 'FusionSolidEnvironment'
+PANEL_ID = 'SolidScriptsAddinsPanel'
+COMMAND_BESIDE_ID = 'ScriptsManagerCommand'
+
+# Resource location for command icons, here we assume a sub folder in this directory named "resources".
+ICON_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', '')
+
+# Local list of event handlers used to maintain a reference so
+# they are not released and garbage collected.
+local_handlers = []
+
+
+# Executed when add-in is run.
+def start():
+ # Create a command Definition.
+ cmd_def = ui.commandDefinitions.addButtonDefinition(CMD_ID, CMD_NAME, CMD_Description, ICON_FOLDER)
+
+ # Add command created handler. The function passed here will be executed when the command is executed.
+ futil.add_handler(cmd_def.commandCreated, command_created)
+
+ # ******** Add a button into the UI so the user can run the command. ********
+ # Get the target workspace the button will be created in.
+ workspace = ui.workspaces.itemById(WORKSPACE_ID)
+
+ # Get the panel the button will be created in.
+ panel = workspace.toolbarPanels.itemById(PANEL_ID)
+
+ # Create the button command control in the UI after the specified existing command.
+ control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False)
+
+ # Specify if the command is promoted to the main toolbar.
+ control.isPromoted = IS_PROMOTED
+
+
+# Executed when add-in is stopped.
+def stop():
+ # Get the various UI elements for this command
+ workspace = ui.workspaces.itemById(WORKSPACE_ID)
+ panel = workspace.toolbarPanels.itemById(PANEL_ID)
+ command_control = panel.controls.itemById(CMD_ID)
+ command_definition = ui.commandDefinitions.itemById(CMD_ID)
+
+ # Delete the button command control
+ if command_control:
+ command_control.deleteMe()
+
+ # Delete the command definition
+ if command_definition:
+ command_definition.deleteMe()
+
+
+# Event handler that is called when the user clicks the command button in the UI.
+# To have a dialog, you create the desired command inputs here. If you don't need
+# a dialog, don't create any inputs and the execute event will be immediately fired.
+# You also need to connect to any command related events here.
+def command_created(args: adsk.core.CommandCreatedEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME} Command Created Event')
+
+ # TODO Create the event handlers you will need for this instance of the command
+ futil.add_handler(args.command.execute, command_execute, local_handlers=local_handlers)
+ futil.add_handler(args.command.inputChanged, command_input_changed, local_handlers=local_handlers)
+ futil.add_handler(args.command.executePreview, command_preview, local_handlers=local_handlers)
+ futil.add_handler(args.command.destroy, command_destroy, local_handlers=local_handlers)
+
+ # Create the user interface for your command by adding different inputs to the CommandInputs object
+ # https://help.autodesk.com/view/fusion360/ENU/?contextId=CommandInputs
+ inputs = args.command.commandInputs
+
+ # TODO ******************************** Define your UI Here ********************************
+
+ # Simple text input box
+ inputs.addTextBoxCommandInput('text_input', 'Text Message', 'Enter some text', 1, False)
+
+ # To create a numerical input with units, we need to get the current units and create a "ValueInput"
+ # https://help.autodesk.com/view/fusion360/ENU/?contextId=ValueInput
+ users_current_units = app.activeProduct.unitsManager.defaultLengthUnits
+ default_value = adsk.core.ValueInput.createByString(f'1 {users_current_units}')
+ inputs.addValueInput('value_input', 'Value Message', users_current_units, default_value)
+
+
+# This function will be called when the user hits the OK button in the command dialog
+def command_execute(args: adsk.core.CommandEventArgs):
+ # General logging for debug
+ futil.log(f'{CMD_NAME} Command Execute Event')
+
+ inputs = args.command.commandInputs
+
+ # TODO ******************************** Your code here ********************************
+
+ # Get a reference to your command's inputs
+ text_input: adsk.core.TextBoxCommandInput = inputs.itemById('text_input')
+ value_input: adsk.core.ValueCommandInput = inputs.itemById('value_input')
+
+ # Construct a message
+ message_action = 'updateMessage'
+ message_data = {
+ 'myValue': f'{value_input.value} cm',
+ 'myExpression': value_input.expression,
+ 'myText': text_input.formattedText
+ }
+ # JSON strings are a useful way to translate between javascript objects and python dictionaries
+ message_json = json.dumps(message_data)
+
+ # Get a reference to the palette and send the message to the palette javascript
+ palette = ui.palettes.itemById(PALETTE_ID)
+ palette.sendInfoToHTML(message_action, message_json)
+
+
+# This function will be called when the command needs to compute a new preview in the graphics window
+def command_preview(args: adsk.core.CommandEventArgs):
+ inputs = args.command.commandInputs
+ futil.log(f'{CMD_NAME} Command Preview Event')
+
+
+# This function will be called when the user changes anything in the command dialog
+def command_input_changed(args: adsk.core.InputChangedEventArgs):
+ changed_input = args.input
+ inputs = args.inputs
+ futil.log(f'{CMD_NAME} Input Changed Event fired from a change to {changed_input.id}')
+
+
+# This event handler is called when the command terminates.
+def command_destroy(args: adsk.core.CommandEventArgs):
+ global local_handlers
+ local_handlers = []
+ futil.log(f'{CMD_NAME} Command Destroy Event')
diff --git a/commands/paletteSend/resources/16x16.png b/commands/paletteSend/resources/16x16.png
new file mode 100644
index 0000000..c18250a
Binary files /dev/null and b/commands/paletteSend/resources/16x16.png differ
diff --git a/commands/paletteSend/resources/32x32.png b/commands/paletteSend/resources/32x32.png
new file mode 100644
index 0000000..f6b4c18
Binary files /dev/null and b/commands/paletteSend/resources/32x32.png differ
diff --git a/commands/paletteSend/resources/64x64.png b/commands/paletteSend/resources/64x64.png
new file mode 100644
index 0000000..ed1ae27
Binary files /dev/null and b/commands/paletteSend/resources/64x64.png differ
diff --git a/commands/paletteShow/__init__.py b/commands/paletteShow/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/commands/paletteShow/entry.py b/commands/paletteShow/entry.py
new file mode 100644
index 0000000..88544fe
--- /dev/null
+++ b/commands/paletteShow/entry.py
@@ -0,0 +1,193 @@
+import json
+import adsk.core
+import os
+from ...lib import fusionAddInUtils as futil
+from ... import config
+from datetime import datetime
+
+app = adsk.core.Application.get()
+ui = app.userInterface
+
+# TODO ********************* Change these names *********************
+CMD_ID = f'{config.COMPANY_NAME}_{config.ADDIN_NAME}_PalleteShow'
+CMD_NAME = 'Show My Palette'
+CMD_Description = 'A Fusion Add-in Palette'
+PALETTE_NAME = 'My Palette Sample'
+IS_PROMOTED = False
+
+# Using "global" variables by referencing values from /config.py
+PALETTE_ID = config.sample_palette_id
+
+# Specify the full path to the local html. You can also use a web URL
+# such as 'https://www.autodesk.com/'
+PALETTE_URL = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', 'html', 'index.html')
+
+# The path function builds a valid OS path. This fixes it to be a valid local URL.
+PALETTE_URL = PALETTE_URL.replace('\\', '/')
+
+# Set a default docking behavior for the palette
+PALETTE_DOCKING = adsk.core.PaletteDockingStates.PaletteDockStateRight
+
+# TODO *** Define the location where the command button will be created. ***
+# This is done by specifying the workspace, the tab, and the panel, and the
+# command it will be inserted beside. Not providing the command to position it
+# will insert it at the end.
+WORKSPACE_ID = 'FusionSolidEnvironment'
+PANEL_ID = 'SolidScriptsAddinsPanel'
+COMMAND_BESIDE_ID = 'ScriptsManagerCommand'
+
+# Resource location for command icons, here we assume a sub folder in this directory named "resources".
+ICON_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', '')
+
+# Local list of event handlers used to maintain a reference so
+# they are not released and garbage collected.
+local_handlers = []
+
+
+# Executed when add-in is run.
+def start():
+ # Create a command Definition.
+ cmd_def = ui.commandDefinitions.addButtonDefinition(CMD_ID, CMD_NAME, CMD_Description, ICON_FOLDER)
+
+ # Add command created handler. The function passed here will be executed when the command is executed.
+ futil.add_handler(cmd_def.commandCreated, command_created)
+
+ # ******** Add a button into the UI so the user can run the command. ********
+ # Get the target workspace the button will be created in.
+ workspace = ui.workspaces.itemById(WORKSPACE_ID)
+
+ # Get the panel the button will be created in.
+ panel = workspace.toolbarPanels.itemById(PANEL_ID)
+
+ # Create the button command control in the UI after the specified existing command.
+ control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False)
+
+ # Specify if the command is promoted to the main toolbar.
+ control.isPromoted = IS_PROMOTED
+
+
+# Executed when add-in is stopped.
+def stop():
+ # Get the various UI elements for this command
+ workspace = ui.workspaces.itemById(WORKSPACE_ID)
+ panel = workspace.toolbarPanels.itemById(PANEL_ID)
+ command_control = panel.controls.itemById(CMD_ID)
+ command_definition = ui.commandDefinitions.itemById(CMD_ID)
+ palette = ui.palettes.itemById(PALETTE_ID)
+
+ # Delete the button command control
+ if command_control:
+ command_control.deleteMe()
+
+ # Delete the command definition
+ if command_definition:
+ command_definition.deleteMe()
+
+ # Delete the Palette
+ if palette:
+ palette.deleteMe()
+
+
+# Event handler that is called when the user clicks the command button in the UI.
+# To have a dialog, you create the desired command inputs here. If you don't need
+# a dialog, don't create any inputs and the execute event will be immediately fired.
+# You also need to connect to any command related events here.
+def command_created(args: adsk.core.CommandCreatedEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME}: Command created event.')
+
+ # Create the event handlers you will need for this instance of the command
+ futil.add_handler(args.command.execute, command_execute, local_handlers=local_handlers)
+ futil.add_handler(args.command.destroy, command_destroy, local_handlers=local_handlers)
+
+
+# Because no command inputs are being added in the command created event, the execute
+# event is immediately fired.
+def command_execute(args: adsk.core.CommandEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME}: Command execute event.')
+
+ palettes = ui.palettes
+ palette = palettes.itemById(PALETTE_ID)
+ if palette is None:
+ palette = palettes.add(
+ id=PALETTE_ID,
+ name=PALETTE_NAME,
+ htmlFileURL=PALETTE_URL,
+ isVisible=True,
+ showCloseButton=True,
+ isResizable=True,
+ width=650,
+ height=600,
+ useNewWebBrowser=True
+ )
+ futil.add_handler(palette.closed, palette_closed)
+ futil.add_handler(palette.navigatingURL, palette_navigating)
+ futil.add_handler(palette.incomingFromHTML, palette_incoming)
+ futil.log(f'{CMD_NAME}: Created a new palette: ID = {palette.id}, Name = {palette.name}')
+
+ if palette.dockingState == adsk.core.PaletteDockingStates.PaletteDockStateFloating:
+ palette.dockingState = PALETTE_DOCKING
+
+ palette.isVisible = True
+
+
+# Use this to handle a user closing your palette.
+def palette_closed(args: adsk.core.UserInterfaceGeneralEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME}: Palette was closed.')
+
+
+# Use this to handle a user navigating to a new page in your palette.
+def palette_navigating(args: adsk.core.NavigationEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME}: Palette navigating event.')
+
+ # Get the URL the user is navigating to:
+ url = args.navigationURL
+
+ log_msg = f"User is attempting to navigate to {url}\n"
+ futil.log(log_msg, adsk.core.LogLevels.InfoLogLevel)
+
+ # Check if url is an external site and open in user's default browser.
+ if url.startswith("http"):
+ args.launchExternally = True
+
+
+# Use this to handle events sent from javascript in your palette.
+def palette_incoming(html_args: adsk.core.HTMLEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME}: Palette incoming event.')
+
+ message_data: dict = json.loads(html_args.data)
+ message_action = html_args.action
+
+ log_msg = f"Event received from {html_args.firingEvent.sender.name}\n"
+ log_msg += f"Action: {message_action}\n"
+ log_msg += f"Data: {message_data}"
+ futil.log(log_msg, adsk.core.LogLevels.InfoLogLevel)
+
+ # TODO ******** Your palette reaction code here ********
+
+ # Read message sent from palette javascript and react appropriately.
+ if message_action == 'messageFromPalette':
+ arg1 = message_data.get('arg1', 'arg1 not sent')
+ arg2 = message_data.get('arg2', 'arg2 not sent')
+
+ msg = 'An event has been fired from the html to Fusion with the following data: '
+ msg += f'Action: {message_action} arg1: {arg1} arg2: {arg2}'
+ ui.messageBox(msg)
+
+ # Return value.
+ now = datetime.now()
+ currentTime = now.strftime('%H:%M:%S')
+ html_args.returnData = f'OK - {currentTime}'
+
+
+# This event handler is called when the command terminates.
+def command_destroy(args: adsk.core.CommandEventArgs):
+ # General logging for debug.
+ futil.log(f'{CMD_NAME}: Command destroy event.')
+
+ global local_handlers
+ local_handlers = []
diff --git a/commands/paletteShow/resources/16x16.png b/commands/paletteShow/resources/16x16.png
new file mode 100644
index 0000000..f1b8e57
Binary files /dev/null and b/commands/paletteShow/resources/16x16.png differ
diff --git a/commands/paletteShow/resources/32x32.png b/commands/paletteShow/resources/32x32.png
new file mode 100644
index 0000000..a61a47e
Binary files /dev/null and b/commands/paletteShow/resources/32x32.png differ
diff --git a/commands/paletteShow/resources/64x64.png b/commands/paletteShow/resources/64x64.png
new file mode 100644
index 0000000..4dd5dfc
Binary files /dev/null and b/commands/paletteShow/resources/64x64.png differ
diff --git a/commands/paletteShow/resources/html/index.html b/commands/paletteShow/resources/html/index.html
new file mode 100644
index 0000000..d5e5bbb
--- /dev/null
+++ b/commands/paletteShow/resources/html/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+ Title
+
+
+
+
+
+
diff --git a/commands/paletteShow/resources/html/static/palette.js b/commands/paletteShow/resources/html/static/palette.js
new file mode 100644
index 0000000..dacdafc
--- /dev/null
+++ b/commands/paletteShow/resources/html/static/palette.js
@@ -0,0 +1,48 @@
+function getDateString() {
+ const today = new Date();
+ const date = `${today.getDate()}/${today.getMonth() + 1}/${today.getFullYear()}`;
+ const time = `${today.getHours()}:${today.getMinutes()}:${today.getSeconds()}`;
+ return `Date: ${date}, Time: ${time}`;
+}
+
+function sendInfoToFusion() {
+ const args = {
+ arg1: document.getElementById("sampleData").value,
+ arg2: getDateString()
+ };
+
+ // Send the data to Fusion as a JSON string. The return value is a Promise.
+ adsk.fusionSendData("messageFromPalette", JSON.stringify(args)).then((result) =>
+ document.getElementById("returnValue").innerHTML = `${result}`
+ );
+
+}
+
+function updateMessage(messageString) {
+ // Message is sent from the add-in as a JSON string.
+ const messageData = JSON.parse(messageString);
+
+ // Update a paragraph with the data passed in.
+ document.getElementById("fusionMessage").innerHTML =
+ `Your text: ${messageData.myText} ` +
+ `Your expression: ${messageData.myExpression} ` +
+ `Your value: ${messageData.myValue}`;
+}
+
+window.fusionJavaScriptHandler = {
+ handle: function (action, data) {
+ try {
+ if (action === "updateMessage") {
+ updateMessage(data);
+ } else if (action === "debugger") {
+ debugger;
+ } else {
+ return `Unexpected command type: ${action}`;
+ }
+ } catch (e) {
+ console.log(e);
+ console.log(`Exception caught with command: ${action}, data: ${data}`);
+ }
+ return "OK";
+ },
+};
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..774f687
--- /dev/null
+++ b/config.py
@@ -0,0 +1,21 @@
+# Application Global Variables
+# This module serves as a way to share variables across different
+# modules (global variables).
+
+import os
+
+# Flag that indicates to run in Debug mode or not. When running in Debug mode
+# more information is written to the Text Command window. Generally, it's useful
+# to set this to True while developing an add-in and set it to False when you
+# are ready to distribute it.
+DEBUG = True
+
+# Gets the name of the add-in from the name of the folder the py file is in.
+# This is used when defining unique internal names for various UI elements
+# that need a unique name. It's also recommended to use a company name as
+# part of the ID to better ensure the ID is unique.
+ADDIN_NAME = os.path.basename(os.path.dirname(__file__))
+COMPANY_NAME = 'ACME'
+
+# Palettes
+sample_palette_id = f'{COMPANY_NAME}_{ADDIN_NAME}_palette_id'
\ No newline at end of file
diff --git a/resources/32x32-panel.png b/resources/32x32-panel.png
new file mode 100644
index 0000000..84946af
Binary files /dev/null and b/resources/32x32-panel.png differ
diff --git a/resources/32x32-saveas.png b/resources/32x32-saveas.png
new file mode 100644
index 0000000..b2eeb1e
Binary files /dev/null and b/resources/32x32-saveas.png differ