├── lib ├── generalUtils │ ├── __init__.py │ ├── debug_utils.py │ ├── value_utils.py │ ├── extrude_utils.py │ ├── persist_utils.py │ └── sketch_utils.py ├── panelUtils │ ├── __init__.py │ ├── panel_command.py │ ├── panel_options.py │ ├── panel_generate.py │ └── panel_inputs.py └── fusionAddInUtils │ ├── __init__.py │ ├── general_utils.py │ └── event_utils.py ├── commands ├── commandDialog │ ├── __init__.py │ ├── resources │ │ ├── 16x16.png │ │ ├── 32x32.png │ │ └── 64x64.png │ └── entry.py └── __init__.py ├── FUNDING.yml ├── resources ├── menu.png ├── dialog.png ├── 3u-6hp-none.png ├── 3u-6hp-sketch.png ├── project-logo.png ├── 1u-12hp-solid-top.png ├── 3u-2hp-solid-top.png ├── 3u-4hp-shell-top.png ├── 3u-2hp-solid-bottom.png ├── 3u-4hp-shell-bottom.png ├── project-logo-readme.png └── 3u-6hp-sketch-edit-width.png ├── .vscode ├── extensions.json └── launch.json ├── .gitignore ├── .github ├── build.sh └── workflows │ └── release.yml ├── ModularSynthPanelGenerator.manifest ├── ModularSynthPanelGenerator.py ├── config.py ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE.md /lib/generalUtils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/panelUtils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commands/commandDialog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: benalman 2 | -------------------------------------------------------------------------------- /lib/fusionAddInUtils/__init__.py: -------------------------------------------------------------------------------- 1 | from .general_utils import * 2 | from .event_utils import * 3 | -------------------------------------------------------------------------------- /resources/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/menu.png -------------------------------------------------------------------------------- /resources/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/dialog.png -------------------------------------------------------------------------------- /resources/3u-6hp-none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-6hp-none.png -------------------------------------------------------------------------------- /resources/3u-6hp-sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-6hp-sketch.png -------------------------------------------------------------------------------- /resources/project-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/project-logo.png -------------------------------------------------------------------------------- /resources/1u-12hp-solid-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/1u-12hp-solid-top.png -------------------------------------------------------------------------------- /resources/3u-2hp-solid-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-2hp-solid-top.png -------------------------------------------------------------------------------- /resources/3u-4hp-shell-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-4hp-shell-top.png -------------------------------------------------------------------------------- /resources/3u-2hp-solid-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-2hp-solid-bottom.png -------------------------------------------------------------------------------- /resources/3u-4hp-shell-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-4hp-shell-bottom.png -------------------------------------------------------------------------------- /resources/project-logo-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/project-logo-readme.png -------------------------------------------------------------------------------- /resources/3u-6hp-sketch-edit-width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/resources/3u-6hp-sketch-edit-width.png -------------------------------------------------------------------------------- /commands/commandDialog/resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/commands/commandDialog/resources/16x16.png -------------------------------------------------------------------------------- /commands/commandDialog/resources/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/commands/commandDialog/resources/32x32.png -------------------------------------------------------------------------------- /commands/commandDialog/resources/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowboy/ModularSynthPanelGenerator/main/commands/commandDialog/resources/64x64.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.vscode-pylance", 5 | "charliermarsh.ruff" 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | persist_defaults/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[codz] 6 | *$py.class 7 | 8 | # Environments 9 | .env 10 | 11 | # Visual Studio Code 12 | .vscode/settings.json 13 | -------------------------------------------------------------------------------- /.github/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | # set -o xtrace 7 | 8 | VERSION=$(jq --raw-output '.release.tag_name' "$GITHUB_EVENT_PATH") 9 | 10 | sed -i "s/(in-development)/$VERSION/" ModularSynthPanelGenerator.manifest 11 | -------------------------------------------------------------------------------- /ModularSynthPanelGenerator.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion", 3 | "type": "addin", 4 | "author": "\"Cowboy\" Ben Alman", 5 | "description": { 6 | "": "A Fusion Add-in for generating modular synth panels" 7 | }, 8 | "version": "(in-development)", 9 | "runOnStartup": true, 10 | "supportedOS": "windows|mac", 11 | "editEnabled": true, 12 | "iconFilename": "resources/project-logo.png" 13 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "name": "Python: Attach", 5 | "type": "python", 6 | "request": "attach", 7 | "pathMappings": [{ 8 | "localRoot": "${workspaceRoot}", 9 | "remoteRoot": "${workspaceRoot}" 10 | }], 11 | "osx": { 12 | "filePath": "${file}" 13 | }, 14 | "windows": { 15 | "filePath": "${file}" 16 | }, 17 | "port": 9000, 18 | "host": "localhost" 19 | }] 20 | } -------------------------------------------------------------------------------- /lib/generalUtils/debug_utils.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | from ..fusionAddInUtils import log 4 | 5 | app = adsk.core.Application.get() 6 | ui = app.userInterface 7 | 8 | 9 | def alert(msg): 10 | ui.messageBox(str(msg)) 11 | 12 | 13 | def identifyFaces(body: adsk.fusion.BRepBody): 14 | component = body.parentComponent 15 | sketches = component.sketches 16 | for i, face in enumerate(body.faces): 17 | try: 18 | sketch = sketches.add(face) 19 | sketch.name = "Face {}".format(i) 20 | except Exception as err: 21 | log(f"Error occurred, {err}") 22 | -------------------------------------------------------------------------------- /lib/generalUtils/value_utils.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | 4 | # ValueInput Object 5 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-bdeb52e0-a6af-4909-93e8-3b13acd0e39c 6 | 7 | app = adsk.core.Application.get() 8 | 9 | 10 | def getNormalizedValueInput(length: float): 11 | design = adsk.fusion.Design.cast(app.activeProduct) 12 | unitsMgr = design.fusionUnitsManager 13 | defaultLengthUnits = app.activeProduct.unitsManager.defaultLengthUnits 14 | converted = round(unitsMgr.convert(length, "cm", defaultLengthUnits), 3) 15 | return adsk.core.ValueInput.createByString(f"{converted} {defaultLengthUnits}") 16 | -------------------------------------------------------------------------------- /ModularSynthPanelGenerator.py: -------------------------------------------------------------------------------- 1 | # Assuming you have not changed the general structure of the template no modification is needed in this file. 2 | from . import commands 3 | from .lib import fusionAddInUtils as futil 4 | 5 | 6 | def run(context): 7 | try: 8 | # This will run the start function in each of your commands as defined in commands/__init__.py 9 | commands.start() 10 | 11 | except: 12 | futil.handle_error('run') 13 | 14 | 15 | def stop(context): 16 | try: 17 | # Remove all of the event handlers your app has created 18 | futil.clear_handlers() 19 | 20 | # This will run the start function in each of your commands as defined in commands/__init__.py 21 | commands.stop() 22 | 23 | except: 24 | futil.handle_error('stop') -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Cut Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | cut-release: 9 | runs-on: ubuntu-latest 10 | concurrency: 11 | group: release 12 | steps: 13 | - uses: actions/checkout@v4 14 | - run: .github/build.sh 15 | - uses: vimtor/action-zip@v1.2 16 | with: 17 | files: 18 | ModularSynthPanelGenerator.manifest ModularSynthPanelGenerator.py config.py LICENSE.md commands/ lib/ 19 | resources/project-logo.png 20 | dest: build/ModularSynthPanelGenerator-${{ github.ref_name }}.zip 21 | recursive: false 22 | - uses: skx/github-action-publish-binaries@master 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | args: build/ModularSynthPanelGenerator-${{ github.ref_name }}.zip 27 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # Application Global Variables 2 | # This module serves as a way to share variables across different 3 | # modules (global variables). 4 | 5 | import os 6 | 7 | # Flag that indicates to run in Debug mode or not. When running in Debug mode 8 | # more information is written to the Text Command window. Generally, it's useful 9 | # to set this to True while developing an add-in and set it to False when you 10 | # are ready to distribute it. 11 | DEBUG = True 12 | 13 | # Gets the name of the add-in from the name of the folder the py file is in. 14 | # This is used when defining unique internal names for various UI elements 15 | # that need a unique name. It's also recommended to use a company name as 16 | # part of the ID to better ensure the ID is unique. 17 | ADDIN_NAME = os.path.basename(os.path.dirname(__file__)) 18 | COMPANY_NAME = 'cowboy' 19 | 20 | # Palettes 21 | sample_palette_id = f'{COMPANY_NAME}_{ADDIN_NAME}_palette_id' -------------------------------------------------------------------------------- /commands/__init__.py: -------------------------------------------------------------------------------- 1 | # Here you define the commands that will be added to your add-in. 2 | 3 | # TODO Import the modules corresponding to the commands you created. 4 | # If you want to add an additional command, duplicate one of the existing directories and import it here. 5 | # You need to use aliases (import "entry" as "my_module") assuming you have the default module named "entry". 6 | from .commandDialog import entry as commandDialog 7 | 8 | # TODO add your imported modules to this list. 9 | # Fusion will automatically call the start() and stop() functions. 10 | commands = [ 11 | commandDialog 12 | ] 13 | 14 | 15 | # Assumes you defined a "start" function in each of your modules. 16 | # The start function will be run when the add-in is started. 17 | def start(): 18 | for command in commands: 19 | command.start() 20 | 21 | 22 | # Assumes you defined a "stop" function in each of your modules. 23 | # The stop function will be run when the add-in is stopped. 24 | def stop(): 25 | for command in commands: 26 | command.stop() -------------------------------------------------------------------------------- /lib/generalUtils/extrude_utils.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | from .value_utils import getNormalizedValueInput 4 | from typing import TypedDict, NotRequired, Unpack, cast 5 | 6 | # Extrude Feature API Sample 7 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-CB1A2357-C8CD-474D-921E-992CA3621D04 8 | 9 | app = adsk.core.Application.get() 10 | 11 | 12 | class ExtrudeKwargs(TypedDict): 13 | operation: NotRequired[adsk.fusion.FeatureOperations] 14 | offsetFrom: NotRequired[adsk.core.Base] 15 | 16 | 17 | def extrude( 18 | component: adsk.fusion.Component, 19 | sketch: adsk.fusion.Sketch, 20 | profileIndices: list[int], 21 | height: float, 22 | name: str, 23 | **kwargs: Unpack[ExtrudeKwargs], 24 | ): 25 | operation = kwargs.get("operation", cast(adsk.fusion.FeatureOperations, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)) 26 | offsetFrom = kwargs.get("offsetFrom") 27 | 28 | profiles = adsk.core.ObjectCollection.create() 29 | for i in profileIndices: 30 | profiles.add(sketch.profiles.item(i)) 31 | 32 | features = component.features 33 | extrudeFeatures = features.extrudeFeatures 34 | 35 | extrudeInput = extrudeFeatures.createInput(profiles, operation) 36 | extent = adsk.fusion.DistanceExtentDefinition.create(getNormalizedValueInput(height)) 37 | extrudeInput.setOneSideExtent(extent, cast(adsk.fusion.ExtentDirections, adsk.fusion.ExtentDirections.PositiveExtentDirection)) 38 | if offsetFrom: 39 | extrudeInput.startExtent = adsk.fusion.FromEntityStartDefinition.create(offsetFrom, adsk.core.ValueInput.createByReal(0)) 40 | 41 | extrude = extrudeFeatures.add(extrudeInput) 42 | extrude.name = "Extrude {}".format(name) 43 | body = extrude.bodies.item(0) 44 | return body 45 | -------------------------------------------------------------------------------- /lib/fusionAddInUtils/general_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 by Autodesk, Inc. 2 | # Permission to use, copy, modify, and distribute this software in object code form 3 | # for any purpose and without fee is hereby granted, provided that the above copyright 4 | # notice appears in all copies and that both that copyright notice and the limited 5 | # warranty and restricted rights notice below appear in all supporting documentation. 6 | # 7 | # AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. AUTODESK SPECIFICALLY 8 | # DISCLAIMS ANY IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. 9 | # AUTODESK, INC. DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 10 | # UNINTERRUPTED OR ERROR FREE. 11 | 12 | import os 13 | import traceback 14 | import adsk.core 15 | 16 | app = adsk.core.Application.get() 17 | ui = app.userInterface 18 | 19 | # Attempt to read DEBUG flag from parent config. 20 | try: 21 | from ... import config 22 | DEBUG = config.DEBUG 23 | except: 24 | DEBUG = False 25 | 26 | 27 | def log(message: str, level: adsk.core.LogLevels = adsk.core.LogLevels.InfoLogLevel, force_console: bool = False): 28 | """Utility function to easily handle logging in your app. 29 | 30 | Arguments: 31 | message -- The message to log. 32 | level -- The logging severity level. 33 | force_console -- Forces the message to be written to the Text Command window. 34 | """ 35 | # Always print to console, only seen through IDE. 36 | print(message) 37 | 38 | # Log all errors to Fusion log file. 39 | if level == adsk.core.LogLevels.ErrorLogLevel: 40 | log_type = adsk.core.LogTypes.FileLogType 41 | app.log(message, level, log_type) 42 | 43 | # If config.DEBUG is True write all log messages to the console. 44 | if DEBUG or force_console: 45 | log_type = adsk.core.LogTypes.ConsoleLogType 46 | app.log(message, level, log_type) 47 | 48 | 49 | def handle_error(name: str, show_message_box: bool = False): 50 | """Utility function to simplify error handling. 51 | 52 | Arguments: 53 | name -- A name used to label the error. 54 | show_message_box -- Indicates if the error should be shown in the message box. 55 | If False, it will only be shown in the Text Command window 56 | and logged to the log file. 57 | """ 58 | 59 | log('===== Error =====', adsk.core.LogLevels.ErrorLogLevel) 60 | log(f'{name}\n{traceback.format_exc()}', adsk.core.LogLevels.ErrorLogLevel) 61 | 62 | # If desired you could show an error as a message box. 63 | if show_message_box: 64 | ui.messageBox(f'{name}\n{traceback.format_exc()}') 65 | -------------------------------------------------------------------------------- /commands/commandDialog/entry.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import os 3 | from ...lib import fusionAddInUtils as futil 4 | from ... import config 5 | from ...lib.panelUtils.panel_command import command_created, CMD_NAME, CMD_Description 6 | 7 | app = adsk.core.Application.get() 8 | ui = app.userInterface 9 | 10 | 11 | # TODO *** Specify the command identity information. *** 12 | CMD_ID = f"{config.COMPANY_NAME}_{config.ADDIN_NAME}_cmdDialog" 13 | 14 | # Specify that the command will be promoted to the panel. 15 | IS_PROMOTED = True 16 | 17 | # TODO *** Define the location where the command button will be created. *** 18 | # This is done by specifying the workspace, the tab, and the panel, and the 19 | # command it will be inserted beside. Not providing the command to position it 20 | # will insert it at the end. 21 | WORKSPACE_ID = "FusionSolidEnvironment" 22 | PANEL_ID = "SolidCreatePanel" 23 | COMMAND_BESIDE_ID = "ScriptsManagerCommand" 24 | 25 | # Resource location for command icons, here we assume a sub folder in this directory named "resources". 26 | ICON_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "") 27 | 28 | 29 | # Executed when add-in is run. 30 | def start(): 31 | # Create a command Definition. 32 | cmd_def = ui.commandDefinitions.addButtonDefinition(CMD_ID, CMD_NAME, CMD_Description, ICON_FOLDER) 33 | 34 | # Define an event handler for the command created event. It will be called when the button is clicked. 35 | futil.add_handler(cmd_def.commandCreated, command_created) 36 | 37 | # ******** Add a button into the UI so the user can run the command. ******** 38 | # Get the target workspace the button will be created in. 39 | workspace = ui.workspaces.itemById(WORKSPACE_ID) 40 | 41 | # Get the panel the button will be created in. 42 | panel = workspace.toolbarPanels.itemById(PANEL_ID) 43 | 44 | # Create the button command control in the UI after the specified existing command. 45 | control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False) 46 | 47 | # Specify if the command is promoted to the main toolbar. 48 | control.isPromoted = IS_PROMOTED 49 | 50 | 51 | # Executed when add-in is stopped. 52 | def stop(): 53 | # Get the various UI elements for this command 54 | workspace = ui.workspaces.itemById(WORKSPACE_ID) 55 | panel = workspace.toolbarPanels.itemById(PANEL_ID) 56 | command_control = panel.controls.itemById(CMD_ID) 57 | command_definition = ui.commandDefinitions.itemById(CMD_ID) 58 | 59 | # Delete the button command control 60 | if command_control: 61 | command_control.deleteMe() 62 | 63 | # Delete the command definition 64 | if command_definition: 65 | command_definition.deleteMe() 66 | -------------------------------------------------------------------------------- /lib/generalUtils/persist_utils.py: -------------------------------------------------------------------------------- 1 | from os.path import normpath, join, dirname, abspath, exists 2 | from os import mkdir, remove 3 | import json 4 | from ...lib import fusionAddInUtils as futil 5 | 6 | persistDir = normpath(join(dirname(abspath(__file__)), "../../persist_defaults")) 7 | 8 | 9 | class Persistable: 10 | def __init__(self, persistFile: str, defaults: dict): 11 | self.persistFile = join(persistDir, persistFile) 12 | self._defaults = defaults 13 | self.restoreDefaults() 14 | 15 | def saveDefaults(self): 16 | try: 17 | if not exists(persistDir): 18 | mkdir(persistDir) 19 | with open(self.persistFile, "w") as persistFile: 20 | data = {} 21 | for key in self._defaults.keys(): 22 | data[key] = getattr(self, key) 23 | json.dump(data, persistFile, indent=4) 24 | futil.log(f"saved defaults file {self.persistFile}") 25 | return True 26 | except Exception as err: 27 | futil.log(f"error when attempting to save defaults file {self.persistFile}: {err}") 28 | return False 29 | 30 | def __loadDefaults(self): 31 | try: 32 | data = False 33 | if exists(self.persistFile): 34 | with open(self.persistFile) as persistFile: 35 | data = json.load(persistFile) 36 | futil.log(f"loaded defaults file {self.persistFile}") 37 | else: 38 | futil.log(f"no defaults file to load {self.persistFile}") 39 | return data 40 | except Exception as err: 41 | futil.log(f"error when attempting to load defaults file {self.persistFile}: {err}") 42 | return False 43 | 44 | def restoreDefaults(self): 45 | data = self._defaults | (self.__loadDefaults() or {}) 46 | for key, value in data.items(): 47 | setattr(self, key, value) 48 | 49 | # Ensure invalid values loaded from persistFile don't break things 50 | def ensureDefaultKeyIsValid(self, keyName, obj): 51 | key = getattr(self, keyName) 52 | if key not in obj: 53 | futil.log(f'Warning: {keyName} "{key}" invalid, restoring default value "{self._defaults[keyName]}"') 54 | setattr(self, keyName, self._defaults[keyName]) 55 | 56 | def eraseDefaults(self): 57 | try: 58 | if exists(self.persistFile): 59 | remove(self.persistFile) 60 | futil.log(f"erased defaults file {self.persistFile}") 61 | else: 62 | futil.log(f"no defaults file to erase {self.persistFile}") 63 | return True 64 | except Exception as err: 65 | futil.log(f"error when attempting to erase defaults file {self.persistFile}: {err}") 66 | return False 67 | -------------------------------------------------------------------------------- /lib/fusionAddInUtils/event_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 by Autodesk, Inc. 2 | # Permission to use, copy, modify, and distribute this software in object code form 3 | # for any purpose and without fee is hereby granted, provided that the above copyright 4 | # notice appears in all copies and that both that copyright notice and the limited 5 | # warranty and restricted rights notice below appear in all supporting documentation. 6 | # 7 | # AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. AUTODESK SPECIFICALLY 8 | # DISCLAIMS ANY IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. 9 | # AUTODESK, INC. DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 10 | # UNINTERRUPTED OR ERROR FREE. 11 | 12 | import sys 13 | from typing import Callable 14 | 15 | import adsk.core 16 | from .general_utils import handle_error 17 | 18 | 19 | # Global Variable to hold Event Handlers 20 | _handlers = [] 21 | 22 | 23 | def add_handler( 24 | event: adsk.core.Event, 25 | callback: Callable, 26 | *, 27 | name: str = None, 28 | local_handlers: list = None 29 | ): 30 | """Adds an event handler to the specified event. 31 | 32 | Arguments: 33 | event -- The event object you want to connect a handler to. 34 | callback -- The function that will handle the event. 35 | name -- A name to use in logging errors associated with this event. 36 | Otherwise the name of the event object is used. This argument 37 | must be specified by its keyword. 38 | local_handlers -- A list of handlers you manage that is used to maintain 39 | a reference to the handlers so they aren't released. 40 | This argument must be specified by its keyword. If not 41 | specified the handler is added to a global list and can 42 | be cleared using the clear_handlers function. You may want 43 | to maintain your own handler list so it can be managed 44 | independently for each command. 45 | 46 | :returns: 47 | The event handler that was created. You don't often need this reference, but it can be useful in some cases. 48 | """ 49 | module = sys.modules[event.__module__] 50 | handler_type = module.__dict__[event.add.__annotations__['handler']] 51 | handler = _create_handler(handler_type, callback, event, name, local_handlers) 52 | event.add(handler) 53 | return handler 54 | 55 | 56 | def clear_handlers(): 57 | """Clears the global list of handlers. 58 | """ 59 | global _handlers 60 | _handlers = [] 61 | 62 | 63 | def _create_handler( 64 | handler_type, 65 | callback: Callable, 66 | event: adsk.core.Event, 67 | name: str = None, 68 | local_handlers: list = None 69 | ): 70 | handler = _define_handler(handler_type, callback, name)() 71 | (local_handlers if local_handlers is not None else _handlers).append(handler) 72 | return handler 73 | 74 | 75 | def _define_handler(handler_type, callback, name: str = None): 76 | name = name or handler_type.__name__ 77 | 78 | class Handler(handler_type): 79 | def __init__(self): 80 | super().__init__() 81 | 82 | def notify(self, args): 83 | try: 84 | callback(args) 85 | except: 86 | handle_error(name) 87 | 88 | return Handler 89 | -------------------------------------------------------------------------------- /lib/panelUtils/panel_command.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | import traceback 4 | 5 | from .. import fusionAddInUtils as futil 6 | from .panel_inputs import Inputs 7 | from .panel_options import PanelOptions 8 | from .panel_generate import generatePanelComponent 9 | 10 | app = adsk.core.Application.get() 11 | ui = app.userInterface 12 | 13 | # CommandInputs Object 14 | # https://help.autodesk.com/view/fusion360/ENU/?contextId=CommandInputs 15 | # 16 | # Command Inputs 17 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-8B9041D5-75CC-4515-B4BB-4CF2CD5BC359 18 | # 19 | # Creating Custom Fusion Commands 20 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-3922697A-7BF1-4799-9A5B-C8539DF57051 21 | # 22 | # Command Inputs API Sample 23 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-e5c4dbe8-ee48-11e4-9823-f8b156d7cd97 24 | 25 | CMD_NAME = "Modular Synth Panel Generator" 26 | CMD_Description = "Create a modular synth panel" 27 | 28 | OPTIONS = PanelOptions("modular_synth_panel_generator.json") 29 | INPUTS: Inputs 30 | 31 | # Local list of event handlers used to maintain a reference so 32 | # they are not released and garbage collected. 33 | LOCAL_HANDLERS = [] 34 | 35 | 36 | def log(msg): 37 | futil.log(f"[{CMD_NAME}] {str(msg)}") 38 | 39 | 40 | def getErrorMessage(text="An unknown error occurred, please validate your inputs and try again"): 41 | stackTrace = traceback.format_exc() 42 | return f"{text}:
{stackTrace}" 43 | 44 | 45 | # Named for easy importing into commandDialog/entry.py 46 | def command_created(args: adsk.core.CommandCreatedEventArgs): 47 | log("Command Created Event") 48 | OPTIONS.restoreDefaults() 49 | global INPUTS 50 | INPUTS = Inputs(args.command.commandInputs, OPTIONS) 51 | 52 | args.command.setDialogMinimumSize(400, 450) 53 | args.command.setDialogSize(400, 450) 54 | 55 | # Register event handlers 56 | futil.add_handler(args.command.execute, onCommandExecute, local_handlers=LOCAL_HANDLERS) 57 | futil.add_handler(args.command.executePreview, onCommandPreview, local_handlers=LOCAL_HANDLERS) 58 | futil.add_handler(args.command.inputChanged, onCommandInputChanged, local_handlers=LOCAL_HANDLERS) 59 | futil.add_handler(args.command.validateInputs, onCommandValidateInput, local_handlers=LOCAL_HANDLERS) 60 | futil.add_handler(args.command.destroy, onCommandDestroy, local_handlers=LOCAL_HANDLERS) 61 | 62 | 63 | # This event handler is called when the user clicks the OK button in the command dialog or 64 | # is immediately called after the created event not command inputs were created for the dialog. 65 | def onCommandExecute(args: adsk.core.CommandEventArgs): 66 | log("Command Execute Event") 67 | generatePanel(args) 68 | 69 | 70 | # This event handler is called when the command needs to compute a new preview in the graphics window. 71 | def onCommandPreview(args: adsk.core.CommandEventArgs): 72 | log("Command Preview Event") 73 | if INPUTS.isValid: 74 | generatePanel(args) 75 | else: 76 | args.executeFailed = True 77 | args.executeFailedMessage = "Some inputs are invalid, unable to generate preview" 78 | 79 | 80 | # This event handler is called when the user changes anything in the command dialog 81 | # allowing you to modify values of other inputs based on that change. 82 | def onCommandInputChanged(args: adsk.core.InputChangedEventArgs): 83 | changedInputId = args.input.id 84 | log(f"Command Input Changed: {changedInputId}") 85 | INPUTS.handleInputChange(changedInputId) 86 | 87 | 88 | # This event handler is called when the user interacts with any of the inputs in the dialog 89 | # which allows you to verify that all of the inputs are valid and enables the OK button. 90 | def onCommandValidateInput(args: adsk.core.ValidateInputsEventArgs): 91 | args.areInputsValid = INPUTS.isValid 92 | log(f"Validate Input Event: isValid={INPUTS.isValid}") 93 | 94 | if INPUTS.isValid: 95 | INPUTS.updateOptionsFromInputs() 96 | 97 | 98 | # This event handler is called when the command terminates. 99 | def onCommandDestroy(args: adsk.core.CommandEventArgs): 100 | log("Command Destroy Event") 101 | global LOCAL_HANDLERS 102 | LOCAL_HANDLERS = [] 103 | 104 | 105 | def generatePanel(args: adsk.core.CommandEventArgs): 106 | try: 107 | des = adsk.fusion.Design.cast(app.activeProduct) 108 | if des.designType == 0: 109 | args.executeFailed = True 110 | args.executeFailedMessage = "Projects with direct modeling are not supported, please enable parametric modeling (timeline) to proceed." 111 | return False 112 | 113 | root = adsk.fusion.Component.cast(des.rootComponent) 114 | componentName = "{} {} {} Panel".format(OPTIONS.formatName, OPTIONS.widthInHp, OPTIONS.widthUnitName) 115 | 116 | # create new component 117 | newCmpOcc = adsk.fusion.Occurrences.cast(root.occurrences).addNewComponent(adsk.core.Matrix3D.create()) 118 | newCmpOcc.component.name = componentName 119 | newCmpOcc.activate() 120 | 121 | panelComponent: adsk.fusion.Component = newCmpOcc.component 122 | 123 | generatePanelComponent(panelComponent, OPTIONS) 124 | 125 | # group features in timeline 126 | count = panelComponent.sketches.count + panelComponent.features.count + panelComponent.constructionAxes.count + panelComponent.constructionPlanes.count 127 | if count > 1: 128 | panelGroup = des.timeline.timelineGroups.add(newCmpOcc.timelineObject.index, newCmpOcc.timelineObject.index + count) 129 | panelGroup.name = componentName 130 | except Exception as err: 131 | args.executeFailed = True 132 | args.executeFailedMessage = getErrorMessage() 133 | log(f"Error occurred, {err}, {getErrorMessage()}") 134 | return False 135 | -------------------------------------------------------------------------------- /lib/generalUtils/sketch_utils.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | import math 4 | from typing import TypedDict, NotRequired, Unpack, cast 5 | 6 | # Sketch Object 7 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-2367ed6a-0ad1-4c8f-935e-b52738d1ce2b 8 | # 9 | # Sketch Sample API Sample 10 | # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-da476794-86f9-11e7-937e-6c0b84aa5a3f 11 | 12 | 13 | def point(x: float, y: float): 14 | return adsk.core.Point3D.create(x, y, 0) 15 | 16 | 17 | def addPoints(a: adsk.core.Point3D, b: adsk.core.Point3D): 18 | return point(a.x + b.x, a.y + b.y) 19 | 20 | 21 | def multPoints(a: adsk.core.Point3D, b: adsk.core.Point3D): 22 | return point(a.x * b.x, a.y * b.y) 23 | 24 | 25 | def midpoint(a: adsk.core.Point3D, b: adsk.core.Point3D): 26 | return point((a.x + b.x) / 2, (a.y + b.y) / 2) 27 | 28 | 29 | def lineMidpoint(line: adsk.fusion.SketchLine, offset: float = 0): 30 | m = midpoint(line.startSketchPoint.geometry, line.endSketchPoint.geometry) 31 | if offset: 32 | m = addPoints(m, lineOffset(line, offset)) 33 | return m 34 | 35 | 36 | def lineOffset(line: adsk.fusion.SketchLine, offset: float): 37 | ratio = offset / line.length 38 | deltaX = (line.endSketchPoint.geometry.y - line.startSketchPoint.geometry.y) * ratio 39 | deltaY = (line.endSketchPoint.geometry.x - line.startSketchPoint.geometry.x) * ratio 40 | return point(deltaX, deltaY) 41 | 42 | 43 | def sketchLineMidpoint(sketch: adsk.fusion.Sketch, refLine: adsk.fusion.SketchLine): 44 | points = sketch.sketchPoints 45 | constraints = sketch.geometricConstraints 46 | 47 | anchorPoint = points.add(lineMidpoint(refLine)) 48 | constraints.addMidPoint(anchorPoint, refLine) 49 | 50 | return anchorPoint 51 | 52 | 53 | class SketchRectangleKwargs(TypedDict): 54 | offset: NotRequired[float] 55 | 56 | 57 | def sketchRectangle( 58 | sketch: adsk.fusion.Sketch, 59 | startPoint: adsk.core.Point3D, 60 | width: float, 61 | length: float, 62 | **kwargs: Unpack[SketchRectangleKwargs], 63 | ): 64 | lines = sketch.sketchCurves.sketchLines 65 | constraints = sketch.geometricConstraints 66 | 67 | offset = kwargs.get("offset", 0) 68 | rectangleLines = lines.addTwoPointRectangle( 69 | addPoints(startPoint, point(offset, offset)), 70 | addPoints(startPoint, point(width - offset, length - offset)), 71 | ) 72 | 73 | constraints.addHorizontal(rectangleLines.item(0)) 74 | constraints.addVertical(rectangleLines.item(1)) 75 | constraints.addHorizontal(rectangleLines.item(2)) 76 | constraints.addVertical(rectangleLines.item(3)) 77 | 78 | return rectangleLines 79 | 80 | 81 | def sketchSlot( 82 | sketch: adsk.fusion.Sketch, 83 | startPoint: adsk.core.Point3D, 84 | endPoint: adsk.core.Point3D, 85 | diameter: float, 86 | ): 87 | lines = sketch.sketchCurves.sketchLines 88 | constraints = sketch.geometricConstraints 89 | dimensions = sketch.sketchDimensions 90 | 91 | # Sadly, this doesn't seem to return anything of use 92 | # result = sketch.addCenterToCenterSlot(startPoint, endPoint, adsk.core.ValueInput.createByReal(diameter), True) 93 | 94 | centerLine = lines.addByTwoPoints(startPoint, endPoint) 95 | centerLine.isConstruction = True 96 | delta = lineOffset(centerLine, diameter / 2) 97 | 98 | line1Delta = multPoints(delta, point(-1, 1)) 99 | line1Start = addPoints(startPoint, line1Delta) 100 | line1End = addPoints(endPoint, line1Delta) 101 | line1 = lines.addByTwoPoints(line1Start, line1End) 102 | 103 | line2Delta = multPoints(delta, point(1, -1)) 104 | line2Start = addPoints(startPoint, line2Delta) 105 | line2End = addPoints(endPoint, line2Delta) 106 | line2 = lines.addByTwoPoints(line2Start, line2End) 107 | 108 | arcs = sketch.sketchCurves.sketchArcs 109 | arc1 = arcs.addByCenterStartSweep(centerLine.startSketchPoint, line1.startSketchPoint, math.pi) 110 | arc2 = arcs.addByCenterStartSweep(centerLine.endSketchPoint, line2.endSketchPoint, math.pi) 111 | 112 | constraints.addParallel(line1, line2) 113 | 114 | constraints.addCoincident(arc1.centerSketchPoint, centerLine.startSketchPoint) 115 | constraints.addCoincident(arc2.centerSketchPoint, centerLine.endSketchPoint) 116 | constraints.addCoincident(arc1.endSketchPoint, line2.startSketchPoint) 117 | constraints.addCoincident(arc2.endSketchPoint, line1.endSketchPoint) 118 | 119 | constraints.addTangent(line1, arc1) 120 | constraints.addTangent(line1, arc2) 121 | constraints.addTangent(line2, arc1) 122 | constraints.addTangent(line2, arc2) 123 | 124 | dimensions.addDiameterDimension(arc1, midpoint(arc1.endSketchPoint.geometry, centerLine.startSketchPoint.geometry)) 125 | 126 | constrainPointToPoint(sketch, centerLine.startSketchPoint, centerLine.endSketchPoint) 127 | 128 | return centerLine, lines, arcs 129 | 130 | 131 | def constrainRectangleWidthHeight( 132 | sketch: adsk.fusion.Sketch, 133 | lines: adsk.fusion.SketchLineList, 134 | labelOffset: float = 0.2, 135 | ): 136 | dimensions = sketch.sketchDimensions 137 | dimensions.addDistanceDimension( 138 | lines.item(0).startSketchPoint, 139 | lines.item(0).endSketchPoint, 140 | cast(adsk.fusion.DimensionOrientations, adsk.fusion.DimensionOrientations.HorizontalDimensionOrientation), 141 | lineMidpoint(lines.item(0), -labelOffset), 142 | ) 143 | dimensions.addDistanceDimension( 144 | lines.item(1).startSketchPoint, 145 | lines.item(1).endSketchPoint, 146 | cast(adsk.fusion.DimensionOrientations, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation), 147 | lineMidpoint(lines.item(1), labelOffset), 148 | ) 149 | 150 | 151 | def constrainPointToPoint( 152 | sketch: adsk.fusion.Sketch, 153 | sketchPoint: adsk.fusion.SketchPoint, 154 | referencePoint: adsk.fusion.SketchPoint, 155 | ): 156 | constraints = sketch.geometricConstraints 157 | dimensions = sketch.sketchDimensions 158 | 159 | if sketchPoint.geometry.isEqualTo(referencePoint.geometry): 160 | constraints.addCoincident(referencePoint, sketchPoint) 161 | else: 162 | dimensions.addDistanceDimension( 163 | referencePoint, 164 | sketchPoint, 165 | cast(adsk.fusion.DimensionOrientations, adsk.fusion.DimensionOrientations.HorizontalDimensionOrientation), 166 | midpoint(point(referencePoint.geometry.x, sketchPoint.geometry.y), sketchPoint.geometry), 167 | ) 168 | dimensions.addDistanceDimension( 169 | referencePoint, 170 | sketchPoint, 171 | cast(adsk.fusion.DimensionOrientations, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation), 172 | midpoint(point(sketchPoint.geometry.x, referencePoint.geometry.y), sketchPoint.geometry), 173 | ) 174 | -------------------------------------------------------------------------------- /lib/panelUtils/panel_options.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | from ..generalUtils.persist_utils import Persistable 4 | from .. import fusionAddInUtils as futil # noqa: F401 5 | 6 | app = adsk.core.Application.get() 7 | 8 | # All numeric values are in cm, which is the default for the Fusion API 9 | 10 | SLOT_DIMENSIONS = { 11 | "slotDiameter": 0.35, 12 | "slotLength": 0.14, 13 | "slotOffsetY": 0.3, 14 | "slotOffsetX": 0.6, 15 | } 16 | 17 | EURORACK_DEFAULTS = { 18 | "hpWidth": 0.508, 19 | "widthUnitMinimum": 2, 20 | "panelHoleMinimum": 6, 21 | "widthUnitName": "HP", 22 | } 23 | 24 | FORMAT_DATA = { 25 | "3u_eurorack": { 26 | "name": "3U Eurorack", 27 | **SLOT_DIMENSIONS, 28 | **EURORACK_DEFAULTS, 29 | "panelLength": 12.85, 30 | "maxPcbLength": 11.0, 31 | }, 32 | "1u_intellijel": { 33 | "name": "1U (Intellijel)", 34 | **SLOT_DIMENSIONS, 35 | **EURORACK_DEFAULTS, 36 | "panelLength": 3.965, 37 | "maxPcbLength": 2.25, 38 | }, 39 | "1u_pulplogic": { 40 | "name": "1U Tile (Pulp Logic)", 41 | **SLOT_DIMENSIONS, 42 | **EURORACK_DEFAULTS, 43 | "panelLength": 4.318, 44 | "maxPcbLength": 2.87, 45 | "slotOffsetX": 0.433, 46 | }, 47 | "ae": { 48 | "name": "AE Modular", 49 | **SLOT_DIMENSIONS, 50 | "hpWidth": 2.5, 51 | "widthUnitMinimum": 1, 52 | "widthUnitName": "Unit", 53 | "widthUnitNamePlural": "Units", 54 | "panelHoleMinimum": 2, 55 | "panelLength": 10.1, 56 | "maxPcbLength": 8, 57 | "slotOffsetX": 1.18, 58 | }, 59 | } 60 | 61 | ANCHOR_POINTS = { 62 | f"{prefix.lower()}-{suffix.lower()}": "Center" if prefix == "Middle" and suffix == "Center" else f"{prefix} {suffix}" 63 | for prefix in ["Top", "Middle", "Bottom"] 64 | for suffix in ["Left", "Center", "Right"] 65 | } 66 | 67 | SUPPORT_TYPES = { 68 | "none": "No reinforcements", 69 | "solid": "Solid (good for larger blanks)", 70 | "shell": "Shell (leaves space for components)", 71 | } 72 | 73 | 74 | class PanelOptions(Persistable): 75 | def __init__(self, persistFile: str): 76 | Persistable.__init__( 77 | self, 78 | persistFile, 79 | { 80 | "formatId": "3u_eurorack", 81 | "widthInHp": 6, 82 | "sketchOnly": False, 83 | "panelHeight": 0.2, 84 | "anchorPoint": "top-left", 85 | "supportType": "none", 86 | "supportSolidHeight": 0.2, 87 | "supportShellHeight": 0.9, 88 | "supportShellWallThickness": 0.1, 89 | }, 90 | ) 91 | self.formatId: str 92 | self.widthInHp: int 93 | self.sketchOnly: bool 94 | self.panelHeight: float 95 | self.anchorPoint: str 96 | self.supportType: str 97 | self.supportSolidHeight: float 98 | self.supportShellHeight: float 99 | self.supportShellWallThickness: float 100 | 101 | def restoreDefaults(self): 102 | super().restoreDefaults() 103 | self.ensureDefaultKeyIsValid("formatId", FORMAT_DATA) 104 | self.ensureDefaultKeyIsValid("anchorPoint", ANCHOR_POINTS) 105 | self.ensureDefaultKeyIsValid("supportType", SUPPORT_TYPES) 106 | 107 | # anchorPoint getters and setters by name for the Fusion UI 108 | @property 109 | def anchorPointNames(self): 110 | return ANCHOR_POINTS.values() 111 | 112 | @property 113 | def anchorPointName(self): 114 | return ANCHOR_POINTS[self.anchorPoint] 115 | 116 | def getIdForAnchorPointName(self, name: str): 117 | return next(key for key, value in ANCHOR_POINTS.items() if value == name) 118 | 119 | @anchorPointName.setter 120 | def anchorPointName(self, name: str): 121 | self.anchorPoint = self.getIdForAnchorPointName(name) 122 | 123 | # supportType getters and setters by name for the Fusion UI 124 | @property 125 | def supportTypeNames(self): 126 | return SUPPORT_TYPES.values() 127 | 128 | @property 129 | def supportTypeName(self): 130 | return SUPPORT_TYPES[self.supportType] 131 | 132 | def getIdForSupportTypeName(self, name: str): 133 | return next(key for key, value in SUPPORT_TYPES.items() if value == name) 134 | 135 | @supportTypeName.setter 136 | def supportTypeName(self, name: str): 137 | self.supportType = self.getIdForSupportTypeName(name) 138 | 139 | # All format names and details 140 | @property 141 | def formatDetails(self): 142 | return {formatId: {**obj} for formatId, obj in FORMAT_DATA.items()} 143 | 144 | @property 145 | def formatNames(self): 146 | return [obj["name"] for obj in FORMAT_DATA.values()] 147 | 148 | # formatId getters and setters by name for the Fusion UI 149 | @property 150 | def formatName(self): 151 | return FORMAT_DATA[self.formatId]["name"] 152 | 153 | def getIdForFormatName(self, name: str): 154 | return next(formatId for formatId, obj in FORMAT_DATA.items() if obj["name"] == name) 155 | 156 | @formatName.setter 157 | def formatName(self, name: str): 158 | self.formatId = self.getIdForFormatName(name) 159 | 160 | # Format data getters 161 | def __formatValue(self, key: str, defaultValue=False): 162 | return FORMAT_DATA[self.formatId].get(key, defaultValue) 163 | 164 | @property 165 | def width(self): 166 | return self.__formatValue("hpWidth") * self.widthInHp 167 | 168 | @property 169 | def widthUnitMinimum(self): 170 | return self.__formatValue("widthUnitMinimum") 171 | 172 | @property 173 | def widthUnitName(self): 174 | return self.__formatValue("widthUnitName") 175 | 176 | @property 177 | def widthUnitNamePlural(self): 178 | return self.__formatValue("widthUnitNamePlural", defaultValue=self.widthUnitName) 179 | 180 | @property 181 | def widthAsExpression(self): 182 | # Ensure value is specified as HP * hpWidth in the user's default units for easy adjustments later 183 | design = adsk.fusion.Design.cast(app.activeProduct) 184 | unitsMgr = design.fusionUnitsManager 185 | return "{} * {}".format(self.widthInHp, unitsMgr.formatValue(self.__formatValue("hpWidth"))) 186 | 187 | @property 188 | def panelLength(self): 189 | return self.__formatValue("panelLength") 190 | 191 | @property 192 | def panelHoleMinimum(self): 193 | return self.__formatValue("panelHoleMinimum") 194 | 195 | @property 196 | def maxPcbLength(self): 197 | return self.__formatValue("maxPcbLength") 198 | 199 | @property 200 | def slotDiameter(self): 201 | return self.__formatValue("slotDiameter") 202 | 203 | @property 204 | def slotLength(self): 205 | return self.__formatValue("slotLength") 206 | 207 | @property 208 | def slotOffsetY(self): 209 | return self.__formatValue("slotOffsetY") 210 | 211 | @property 212 | def slotOffsetX(self): 213 | return self.__formatValue("slotOffsetX") 214 | -------------------------------------------------------------------------------- /lib/panelUtils/panel_generate.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | import adsk.fusion 3 | from typing import cast 4 | from ..generalUtils.sketch_utils import ( 5 | addPoints, 6 | constrainPointToPoint, 7 | constrainRectangleWidthHeight, 8 | lineMidpoint, 9 | midpoint, 10 | point, 11 | sketchLineMidpoint, 12 | sketchRectangle, 13 | sketchSlot, 14 | ) 15 | from ..generalUtils.extrude_utils import extrude 16 | from .panel_options import PanelOptions 17 | from .. import fusionAddInUtils as futil # noqa: F401 18 | 19 | app = adsk.core.Application.get() 20 | ui = app.userInterface 21 | 22 | 23 | def generatePanelComponent(component: adsk.fusion.Component, opts: PanelOptions): 24 | sketches = component.sketches 25 | xyPlane = component.xYConstructionPlane 26 | sketch = sketches.add(xyPlane) 27 | lines = sketch.sketchCurves.sketchLines 28 | constraints = sketch.geometricConstraints 29 | dimensions = sketch.sketchDimensions 30 | 31 | sketch.name = "Panel" 32 | sketch.areDimensionsShown = True 33 | 34 | # Panel 35 | anchorPointVertical, anchorPointHorizontal = opts.anchorPoint.split("-") 36 | match anchorPointVertical: 37 | case "top": 38 | panelStartY = -opts.panelLength 39 | case "middle": 40 | panelStartY = -opts.panelLength / 2 41 | case "bottom": 42 | panelStartY = 0 43 | case _: 44 | raise ValueError("Invalid anchorPoint value") 45 | 46 | match anchorPointHorizontal: 47 | case "left": 48 | panelStartX = 0 49 | case "center": 50 | panelStartX = -opts.width / 2 51 | case "right": 52 | panelStartX = -opts.width 53 | case _: 54 | raise ValueError("Invalid anchorPoint value") 55 | 56 | panelStartPoint = point(panelStartX, panelStartY) 57 | 58 | rectangleLines = sketchRectangle(sketch, panelStartPoint, opts.width, opts.panelLength) 59 | constrainRectangleWidthHeight(sketch, rectangleLines) 60 | dimensions.item(dimensions.count - 2).parameter.expression = opts.widthAsExpression 61 | 62 | panelBottomLine = rectangleLines.item(0) 63 | panelRightLine = rectangleLines.item(1) 64 | panelTopLine = rectangleLines.item(2) 65 | panelLeftLine = rectangleLines.item(3) 66 | 67 | topLeftPoint = panelTopLine.endSketchPoint 68 | topRightPoint = panelTopLine.startSketchPoint 69 | bottomLeftPoint = panelBottomLine.startSketchPoint 70 | bottomRightPoint = panelBottomLine.endSketchPoint 71 | 72 | def createPanelHorizontalLine(offset: float, isConstruction: bool): 73 | line = lines.addByTwoPoints( 74 | point(panelTopLine.startSketchPoint.geometry.x, offset), 75 | point(panelTopLine.endSketchPoint.geometry.x, offset), 76 | ) 77 | line.isConstruction = isConstruction 78 | constraints.addHorizontal(line) 79 | constraints.addCoincident(line.startSketchPoint, panelLeftLine) 80 | constraints.addCoincident(line.endSketchPoint, panelRightLine) 81 | return line 82 | 83 | def createPanelMidLine(): 84 | line = createPanelHorizontalLine(lineMidpoint(panelLeftLine).y, True) 85 | constraints.addMidPoint(line.startSketchPoint, panelLeftLine) 86 | return line 87 | 88 | match opts.anchorPoint: 89 | case "top-left": 90 | anchorPoint = topLeftPoint 91 | case "top-center": 92 | anchorPoint = sketchLineMidpoint(sketch, panelTopLine) 93 | case "top-right": 94 | anchorPoint = topRightPoint 95 | case "middle-left": 96 | anchorPoint = sketchLineMidpoint(sketch, panelLeftLine) 97 | case "middle-center": 98 | anchorPoint = sketchLineMidpoint(sketch, createPanelMidLine()) 99 | case "middle-right": 100 | anchorPoint = sketchLineMidpoint(sketch, panelRightLine) 101 | case "bottom-left": 102 | anchorPoint = bottomLeftPoint 103 | case "bottom-center": 104 | anchorPoint = sketchLineMidpoint(sketch, panelBottomLine) 105 | case "bottom-right": 106 | anchorPoint = bottomRightPoint 107 | case _: 108 | raise ValueError("Invalid anchorPoint value") 109 | 110 | constrainPointToPoint(sketch, anchorPoint, sketch.originPoint) 111 | 112 | # Max extents for anything extruded from the bottom 113 | def addRefLine(panelLine: adsk.fusion.SketchLine, offset: float): 114 | line = createPanelHorizontalLine(panelLine.startSketchPoint.geometry.y + offset, opts.supportType == "none") 115 | dimensions.addDistanceDimension( 116 | panelLine.startSketchPoint, 117 | line.startSketchPoint, 118 | cast(adsk.fusion.DimensionOrientations, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation), 119 | midpoint(lineMidpoint(line), lineMidpoint(panelLine)), 120 | ) 121 | return line 122 | 123 | railLength = (opts.panelLength - opts.maxPcbLength) / 2 124 | topRefLine = addRefLine(panelTopLine, -railLength) 125 | bottomRefLine = addRefLine(panelBottomLine, railLength) 126 | 127 | dimensions.addDistanceDimension( 128 | topRefLine.startSketchPoint, 129 | bottomRefLine.startSketchPoint, 130 | cast(adsk.fusion.DimensionOrientations, adsk.fusion.DimensionOrientations.VerticalDimensionOrientation), 131 | addPoints( 132 | midpoint(topRefLine.startSketchPoint.geometry, bottomRefLine.startSketchPoint.geometry), 133 | point(-0.2, 0), 134 | ), 135 | False, 136 | ) 137 | 138 | if opts.supportType == "shell": 139 | shellRectLines = sketchRectangle( 140 | sketch, 141 | bottomRefLine.startSketchPoint.geometry, 142 | opts.width, 143 | opts.maxPcbLength, 144 | offset=opts.supportShellWallThickness, 145 | ) 146 | shellBottomLine = shellRectLines.item(0) 147 | shellRightLine = shellRectLines.item(1) 148 | shellTopLine = shellRectLines.item(2) 149 | shellLeftLine = shellRectLines.item(3) 150 | dimensions.addOffsetDimension(bottomRefLine, shellBottomLine, lineMidpoint(shellBottomLine)) 151 | dimensions.addOffsetDimension(panelRightLine, shellRightLine, lineMidpoint(shellRightLine)) 152 | dimensions.addOffsetDimension(topRefLine, shellTopLine, lineMidpoint(shellTopLine)) 153 | dimensions.addOffsetDimension(panelLeftLine, shellLeftLine, lineMidpoint(shellLeftLine)) 154 | 155 | # Screw holes 156 | slots = [] 157 | slotsLeft = True 158 | slotsRight = True 159 | 160 | if opts.widthInHp < opts.panelHoleMinimum: 161 | slotsRight = False 162 | 163 | if slotsLeft: 164 | slots.append([topLeftPoint, -1, 1]) 165 | slots.append([bottomLeftPoint, 1, 1]) 166 | 167 | if slotsRight: 168 | slots.append([topRightPoint, -1, -1]) 169 | slots.append([bottomRightPoint, 1, -1]) 170 | 171 | slotFaceCount = 4 * len(slots) 172 | 173 | for referencePoint, yOffsetDirection, xOffsetDirection in slots: 174 | slotStartPoint = addPoints( 175 | referencePoint.geometry, 176 | point(xOffsetDirection * opts.slotOffsetX, yOffsetDirection * opts.slotOffsetY), 177 | ) 178 | slotEndPoint = addPoints(slotStartPoint, point(xOffsetDirection * opts.slotLength, 0)) 179 | slotCenterLine = sketchSlot(sketch, slotStartPoint, slotEndPoint, opts.slotDiameter)[0] 180 | constrainPointToPoint(sketch, slotCenterLine.startSketchPoint, referencePoint) 181 | 182 | if opts.sketchOnly: 183 | return 184 | 185 | # Extrusions 186 | if opts.supportType == "solid": 187 | body1 = extrude(component, sketch, [0, 1, 2], -opts.panelHeight, "Panel") 188 | body = extrude( 189 | component, 190 | sketch, 191 | [1], 192 | -opts.supportSolidHeight, 193 | "Support", 194 | offsetFrom=body1.faces.item(5 + slotFaceCount), 195 | operation=cast(adsk.fusion.FeatureOperations, adsk.fusion.FeatureOperations.JoinFeatureOperation), 196 | ) 197 | elif opts.supportType == "shell": 198 | body1 = extrude(component, sketch, [0, 1, 2, 3], -opts.panelHeight, "Panel") 199 | body = extrude( 200 | component, 201 | sketch, 202 | [1], 203 | -opts.supportShellHeight, 204 | "Support Shell", 205 | offsetFrom=body1.faces.item(5 + slotFaceCount), 206 | operation=cast(adsk.fusion.FeatureOperations, adsk.fusion.FeatureOperations.JoinFeatureOperation), 207 | ) 208 | else: 209 | body = extrude(component, sketch, [0], -opts.panelHeight, "Panel") 210 | 211 | body.name = "Panel" 212 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant 3.0 Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We pledge to make our community welcoming, safe, and equitable for all. 6 | 7 | We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all 8 | individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, 9 | neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or 10 | religion, national or social origin, socio-economic position, level of education, or other status. The same privileges 11 | of participation are extended to everyone who participates in good faith and in accordance with this Covenant. 12 | 13 | ## Encouraged Behaviors 14 | 15 | While acknowledging differences in social norms, we all strive to meet our community's expectations for positive 16 | behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, 17 | background, or native language. 18 | 19 | With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared 20 | values, including: 21 | 22 | 1. Respecting the **purpose of our community**, our activities, and our ways of gathering. 23 | 2. Engaging **kindly and honestly** with others. 24 | 3. Respecting **different viewpoints** and experiences. 25 | 4. **Taking responsibility** for our actions and contributions. 26 | 5. Gracefully giving and accepting **constructive feedback**. 27 | 6. Committing to **repairing harm** when it occurs. 28 | 7. Behaving in other ways that promote and sustain the **well-being of our community**. 29 | 30 | ## Restricted Behaviors 31 | 32 | We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are 33 | violations of this Code of Conduct. 34 | 35 | 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any 36 | clear request to stop. 37 | 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of 38 | people. 39 | 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable 40 | identities or traits. 41 | 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or 42 | purpose of the community. 43 | 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their 44 | permission. 45 | 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. 46 | 7. Behaving in other ways that **threaten the well-being** of our community. 47 | 48 | ### Other Restrictions 49 | 50 | 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade 51 | enforcement actions. 52 | 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. 53 | 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the 54 | community. 55 | 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other 56 | restricted behaviors. 57 | 58 | ## Reporting an Issue 59 | 60 | Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict 61 | represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help 62 | avoid conflicts and minimize harm. 63 | 64 | When an incident does occur, it is important to report it promptly. To report a possible violation, **[NOTE: describe 65 | your means of reporting here.]** 66 | 67 | Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They 68 | will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing 69 | witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as 70 | possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried 71 | out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon 72 | resolution. 73 | 74 | ## Addressing and Repairing Harm 75 | 76 | **[NOTE: The remedies and repairs outlined below are suggestions based on best practices in code of conduct enforcement. 77 | If your community has its own established enforcement process, be sure to edit this section to describe your own 78 | policies.]** 79 | 80 | If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following 81 | enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals 82 | involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be 83 | skipped. 84 | 85 | 1. Warning 86 | 1. Event: A violation involving a single incident or series of incidents. 87 | 2. Consequence: A private, written warning from the Community Moderators. 88 | 3. Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking 89 | clarification on expectations. 90 | 2. Temporarily Limited Activities 91 | 1. Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more 92 | serious violation. 93 | 2. Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness 94 | of the situation and give the community members involved time to process the incident. The cooldown period may be 95 | limited to particular communication channels or interactions with particular community members. 96 | 3. Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and 97 | impact, and being thoughtful about re-entering community spaces after the period is over. 98 | 3. Temporary Suspension 99 | 1. Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a 100 | single serious violation. 101 | 2. Consequence: A private written warning with conditions for return from suspension. In general, temporary 102 | suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. 103 | 3. Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for 104 | return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. 105 | 4. Permanent Ban 106 | 1. Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or 107 | a violation so serious that the Community Moderators determine there is no way to keep the community safe with 108 | this person as a member. 109 | 2. Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent 110 | bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working 111 | through other remedies has failed to change the behavior. 112 | 3. Repair: There is no possible repair in cases of this severity. 113 | 114 | This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their 115 | discretion and judgment, in keeping with the best interests of our community. 116 | 117 | ## Scope 118 | 119 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing 120 | the community in public or other spaces. Examples of representing our community include using an official email address, 121 | posting via an official social media account, or acting as an appointed representative at an online or offline event. 122 | 123 | ## Attribution 124 | 125 | This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at 126 | [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). 127 | 128 | Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy 129 | of this license, visit 130 | [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) 131 | 132 | For answers to common questions about Contributor Covenant, see the FAQ at 133 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at 134 | [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional 135 | enforcement and community guideline resources can be found at 136 | [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder 137 | was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). 138 | -------------------------------------------------------------------------------- /lib/panelUtils/panel_inputs.py: -------------------------------------------------------------------------------- 1 | import adsk.core 2 | from enum import Enum 3 | from typing import cast 4 | from .. import fusionAddInUtils as futil # noqa: F401 5 | 6 | app = adsk.core.Application.get() 7 | ui = app.userInterface 8 | 9 | 10 | class Actions(Enum): 11 | RESTORE_DEFAULTS = "RESTORE_DEFAULTS" 12 | SAVE_DEFAULTS = "SAVE_DEFAULTS" 13 | ERASE_DEFAULTS = "ERASE_DEFAULTS" 14 | 15 | 16 | class Inputs: 17 | def __init__(self, inputs: adsk.core.CommandInputs, options): 18 | self.inputs = inputs 19 | self.options = options 20 | 21 | self.initializeInputs() 22 | 23 | for key in self.options.formatDetails.keys(): 24 | setattr(self, f"widthInHp_{key}", adsk.core.IntegerSpinnerCommandInput.cast(self.inputs.itemById(f"widthInHp_{key}"))) 25 | 26 | self.panelHeight = adsk.core.ValueCommandInput.cast(self.inputs.itemById("panelHeight")) 27 | self.sketchOnly = adsk.core.BoolValueCommandInput.cast(self.inputs.itemById("sketchOnly")) 28 | self.supportSolidHeight = adsk.core.ValueCommandInput.cast(self.inputs.itemById("supportSolidHeight")) 29 | self.supportShellHeight = adsk.core.ValueCommandInput.cast(self.inputs.itemById("supportShellHeight")) 30 | self.supportShellWallThickness = adsk.core.ValueCommandInput.cast(self.inputs.itemById("supportShellWallThickness")) 31 | self.formatType = adsk.core.DropDownCommandInput.cast(self.inputs.itemById("formatType")) 32 | self.anchorPoint = adsk.core.DropDownCommandInput.cast(self.inputs.itemById("anchorPoint")) 33 | self.supportType = adsk.core.DropDownCommandInput.cast(self.inputs.itemById("supportType")) 34 | 35 | self.updateUiState() 36 | 37 | def updateUiState(self): 38 | sketchOnly = self.sketchOnly.value 39 | self.panelHeight.isVisible = not sketchOnly 40 | 41 | supportTypeName = self.supportType.selectedItem.name 42 | supportTypeId = self.options.getIdForSupportTypeName(supportTypeName) 43 | self.supportSolidHeight.isVisible = supportTypeId == "solid" and not sketchOnly 44 | self.supportShellHeight.isVisible = supportTypeId == "shell" and not sketchOnly 45 | self.supportShellWallThickness.isVisible = supportTypeId == "shell" 46 | 47 | for key in self.options.formatDetails.keys(): 48 | input = self.widthInputByFormatId(key) 49 | input.isVisible = input is self.currentWidthInput 50 | 51 | def updateOptionsFromInputs(self): 52 | self.options.widthInHp = int(self.currentWidthInput.value) 53 | self.options.panelHeight = self.panelHeight.value 54 | self.options.sketchOnly = self.sketchOnly.value 55 | self.options.supportSolidHeight = self.supportSolidHeight.value 56 | self.options.supportShellHeight = self.supportShellHeight.value 57 | self.options.supportShellWallThickness = self.supportShellWallThickness.value 58 | self.options.formatName = self.formatType.selectedItem.name 59 | self.options.anchorPointName = self.anchorPoint.selectedItem.name 60 | self.options.supportTypeName = self.supportType.selectedItem.name 61 | 62 | def updateInputsFromOptions(self): 63 | self.updateWidthInputsFromOptions() 64 | self.panelHeight.value = self.options.panelHeight 65 | self.sketchOnly.value = self.options.sketchOnly 66 | self.supportSolidHeight.value = self.options.supportSolidHeight 67 | self.supportShellHeight.value = self.options.supportShellHeight 68 | self.supportShellWallThickness.value = self.options.supportShellWallThickness 69 | for listItem in self.formatType.listItems: 70 | listItem.isSelected = listItem.name == self.options.formatName 71 | for listItem in self.anchorPoint.listItems: 72 | listItem.isSelected = listItem.name == self.options.anchorPointName 73 | for listItem in self.supportType.listItems: 74 | listItem.isSelected = listItem.name == self.options.supportTypeName 75 | 76 | @property 77 | def isValid(self): 78 | # This check happens after handleInputChange() but before updateOptionsFromInputs() 79 | obj = self.options.formatDetails[self.currentFormatId] 80 | return self.currentWidthInput.value >= obj["widthUnitMinimum"] 81 | 82 | @property 83 | def widthInputs(self): 84 | return [self.widthInputByFormatId(formatId) for formatId in self.options.formatDetails.keys()] 85 | 86 | @property 87 | def currentFormatId(self): 88 | return self.options.getIdForFormatName(self.formatType.selectedItem.name) 89 | 90 | def widthInputByFormatId(self, formatId: str): 91 | return getattr(self, f"widthInHp_{formatId}") 92 | 93 | @property 94 | def currentWidthInput(self): 95 | return self.widthInputByFormatId(self.currentFormatId) 96 | 97 | def updateWidthInputsFromOptions(self, skip=None): 98 | for formatId, obj in self.options.formatDetails.items(): 99 | if formatId != skip: 100 | input = self.widthInputByFormatId(formatId) 101 | input.value = max(self.options.widthInHp, obj["widthUnitMinimum"]) 102 | 103 | def handleInputChange(self, inputName: str): 104 | if inputName.startswith("widthInHp"): 105 | input = getattr(self, inputName) 106 | self.options.widthInHp = int(input.value) 107 | self.updateWidthInputsFromOptions(skip=inputName) 108 | 109 | else: 110 | match inputName: 111 | case Actions.RESTORE_DEFAULTS.value: 112 | self.options.restoreDefaults() 113 | self.updateInputsFromOptions() 114 | self.updateUiState() 115 | case Actions.SAVE_DEFAULTS.value: 116 | if not self.options.saveDefaults(): 117 | ui.messageBox(f"Unable to save defaults file {self.options.persistFile}. Is it writable?", "Warning") 118 | case Actions.ERASE_DEFAULTS.value: 119 | if not self.options.eraseDefaults(): 120 | ui.messageBox(f"Unable to erase defaults file {self.options.persistFile}. Is it writable?", "Warning") 121 | 122 | self.updateUiState() 123 | 124 | def initializeInputs(self): 125 | defaultLengthUnits = app.activeProduct.unitsManager.defaultLengthUnits 126 | 127 | message = 'For more information, read the documentation.' 128 | self.inputs.addTextBoxCommandInput("infoTextBox", "Information", message, 1, True) 129 | 130 | heightDropdown = self.inputs.addDropDownCommandInput( 131 | "formatType", "Panel format", cast(adsk.core.DropDownStyles, adsk.core.DropDownStyles.TextListDropDownStyle) 132 | ) 133 | for name in self.options.formatNames: 134 | heightDropdown.listItems.add(name, name == self.options.formatName) 135 | 136 | for formatId, obj in self.options.formatDetails.items(): 137 | self.inputs.addIntegerSpinnerCommandInput( 138 | f"widthInHp_{formatId}", 139 | f"Panel width in {obj.get('widthUnitNamePlural', obj['widthUnitName'])}", 140 | obj["widthUnitMinimum"], 141 | 9000, 142 | 1, 143 | max(obj["widthUnitMinimum"], self.options.widthInHp), 144 | ) 145 | 146 | self.inputs.addValueInput( 147 | "panelHeight", 148 | "Panel height", 149 | defaultLengthUnits, 150 | adsk.core.ValueInput.createByReal(self.options.panelHeight), 151 | ) 152 | 153 | anchorPointDropdown = self.inputs.addDropDownCommandInput( 154 | "anchorPoint", "Anchor point", cast(adsk.core.DropDownStyles, adsk.core.DropDownStyles.TextListDropDownStyle) 155 | ) 156 | for name in self.options.anchorPointNames: 157 | anchorPointDropdown.listItems.add(name, name == self.options.anchorPointName) 158 | 159 | self.inputs.addBoolValueInput("sketchOnly", "Sketch only", True, "", self.options.sketchOnly) 160 | 161 | supportGroup = self.inputs.addGroupCommandInput("supportGroup", "Reinforcement") 162 | supportGroup.isExpanded = True 163 | 164 | supportTypeDropdown = supportGroup.children.addDropDownCommandInput( 165 | "supportType", "Type", cast(adsk.core.DropDownStyles, adsk.core.DropDownStyles.TextListDropDownStyle) 166 | ) 167 | for name in self.options.supportTypeNames: 168 | supportTypeDropdown.listItems.add(name, name == self.options.supportTypeName) 169 | 170 | supportGroup.children.addValueInput( 171 | "supportSolidHeight", 172 | "Support height", 173 | defaultLengthUnits, 174 | adsk.core.ValueInput.createByReal(self.options.supportSolidHeight), 175 | ) 176 | supportGroup.children.addValueInput( 177 | "supportShellHeight", 178 | "Shell height", 179 | defaultLengthUnits, 180 | adsk.core.ValueInput.createByReal(self.options.supportShellHeight), 181 | ) 182 | supportGroup.children.addValueInput( 183 | "supportShellWallThickness", 184 | "Shell wall thickness", 185 | defaultLengthUnits, 186 | adsk.core.ValueInput.createByReal(self.options.supportShellWallThickness), 187 | ) 188 | 189 | # Save, restore and erase defaults 190 | persistGroup = self.inputs.addGroupCommandInput("persistGroup", "Defaults") 191 | persistGroup.isExpanded = True 192 | restoreDefaultsInput = persistGroup.children.addBoolValueInput(Actions.RESTORE_DEFAULTS.name, "Reset", False, "", False) 193 | restoreDefaultsInput.text = "Reset all inputs to default values" 194 | saveDefaultsInput = persistGroup.children.addBoolValueInput(Actions.SAVE_DEFAULTS.name, "Update defaults", False, "", False) 195 | saveDefaultsInput.text = "Save current input values as new defaults" 196 | eraseDefaultsInput = persistGroup.children.addBoolValueInput(Actions.ERASE_DEFAULTS.name, "Factory reset", False, "", False) 197 | eraseDefaultsInput.text = "Erase saved input value defaults" 198 | 199 | message = 'Does this add-in save you time? Say thanks by buying me a coffee.' 200 | coffeeTextBox = self.inputs.addTextBoxCommandInput("coffeeTextBox", "Thanks", message, 1, True) 201 | coffeeTextBox.isFullWidth = True 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modular Synth Panel Generator 2 | 3 | [![Latest release](https://img.shields.io/github/v/release/cowboy/ModularSynthPanelGenerator.svg?style=flat)][latest-release] 4 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa] 5 | [![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat)][coc] 6 | 7 | ## Description 8 | 9 | 10 | 11 | TL;DR: This is a modular synth panel generator [add-in][addins] for [Autodesk Fusion][fusion] 12 | 13 | If you're like me and you like making DIY synth modules but absolutely hate milling aluminum panels because little bits 14 | of metal go flying everywhere and it's impossible to get all the holes to line up just right and it's just so damn 15 | tedious, maybe this thing will help you out. 16 | 17 | I've been 3D printing DIY Eurorack module panels for a while now, but until I made this add-in, I'd been manually 18 | copying and pasting "template" components over and over again, which resulted in a number of very large and very slow 19 | Fusion project files. 20 | 21 | ## Features 22 | 23 | With this add-in, you can... 24 | 25 | - generate modular synth panels (currently Eurorack and 1U formats are supported), suitable for 3D printing (or CNC?) 26 | - choose any width from 2 to 9000 HP 27 | - set a custom panel thickness 28 | - choose multiple reinforcement types, each of which thicken the center area of the panel, while leaving the panel 29 | thickness in the mounting screw area unchanged: 30 | - `Shell`: This creates a hollow shell, adding strength while leaving space inside for jacks and switches. Generally 31 | useful for 4 HP and larger panels. 32 | - `Solid`: This adds strength to larger blanks, or very narrow modules where the shell approach wouldn't leave enough 33 | space for components. 34 | - save custom default values for easy recall 35 | - easily edit generated sketches and features to change dimensions, after-the-fact 36 | 37 | ### Currently supported modular synth panel formats 38 | 39 | | Format name | Reference Specification | 40 | | ----------------------------- | ----------------------------------------------------------- | 41 | | 3U Eurorack | [Doepfer - A-100 Construction Details][doepfer-spec] | 42 | | 1U (Intellijel) | [Intellijel - 1U Technical Specifications][intellijel-spec] | 43 | | 1U Tile (Pulp Logic) | [Pulp Logic - About 1U Tiles][pulplogic-spec] | 44 | | <Your favorite format?> | [Contributions welcome!](#contributing) | 45 | 46 | ### Additional Notes 47 | 48 | - I print with PETG using a 0.4mm nozzle and 0.2mm layer height on a Bambu X1C, without issues. 49 | - When using reinforcements, it'll probably be easiest if you print with the panel face down. 😛 50 | - Pulp Logic tiles are meant to be made in multipes of 6 HP but I won't tell anyone if you make odd sizes. 51 | 52 | ## Usage 53 | 54 | | 1️⃣ Open the add-in | 2️⃣ Generate the panel | 55 | | --------------------------- | ------------------------------------ | 56 | | ![Menu](resources/menu.png) | ![PanDialogel](resources/dialog.png) | 57 | 58 | ### Generated panel examples 59 | 60 | | 3U 6HP Panel (no reinforcement) | 3U 2HP Panel (solid reinforcement) | 3U 6HP Panel (shell reinforcement) | 61 | | ------------------------------- | ----------------------------------- | ----------------------------------- | 62 | | ![](resources/3u-6hp-none.png) | ![](resources/3u-2hp-solid-top.png) | ![](resources/3u-4hp-shell-top.png) | 63 | 64 | | 1U 12HP Panel (solid reinforcement) | 3U 2HP Panel (solid reinforcement) | 3U 6HP Panel (shell reinforcement) | 65 | | ------------------------------------ | -------------------------------------- | -------------------------------------- | 66 | | ![](resources/1u-12hp-solid-top.png) | ![](resources/3u-2hp-solid-bottom.png) | ![](resources/3u-4hp-shell-bottom.png) | 67 | 68 | ### Generated sketch examples 69 | 70 | | Example generated sketch | Panel width HP in sketch is editable | 71 | | -------------------------------- | ------------------------------------------- | 72 | | ![](resources/3u-6hp-sketch.png) | ![](resources/3u-6hp-sketch-edit-width.png) | 73 | 74 | ## Requirements 75 | 76 | - Requires parametric modeling (timeline) to be enabled. Does not work with direct modeling. 77 | - Tested with Fusion 2603.1.31 (August, 2025) on Windows 11. Should work on other operating systems, but they are 78 | untested. 79 | 80 | ## Installation 81 | 82 | ### From Github 83 | 84 | #### Step 1: Download 85 | 86 | You have a few options: 87 | 88 | **Option 1:** You just want to use the add-in 89 | 90 | 1. Download the `ModularSynthPanelGenerator-vX.Y.Z.zip` file from the [latest release][latest-release] page. Note that 91 | the `X.Y.Z` part will change based on the release version. 92 | 2. Unzip it. You can unzip anywhere, but the [Installing, Linking, and Removing Scripts and Add-Ins][addins-installing] 93 | documentation page has suggestions. 94 | 95 | **Option 2:** You plan on contributing 96 | 97 | 1. Fork this repo. 98 | 2. Clone your fork. You can clone anywhere, but the [Installing, Linking, and Removing Scripts and 99 | Add-Ins][addins-installing] documentation page has suggestions. 100 | 101 | #### Step 2: Install into Fusion 102 | 103 | 1. In Fusion open the `Scripts and Add-Ins` dialog by pressing `Shift + S` or going to 104 | `Utilities -> Add-Ins -> Scripts and Add-Ins` in the top menu of the Design workspace. 105 | 2. Click the `+` (plus) icon at the top of the `Scripts and Add-Ins` dialog and select `Script or add-in from device`. 106 | 3. Choose the folder created after unzipping / cloning. It will be named something like `ModularSynthPanelGenerator` or 107 | `ModularSynthPanelGenerator-vX.Y.Z` and will contain `lib`, `commands` and `resources` folders, as well as files like 108 | `ModularSynthPanelGenerator.manifest` and `ModularSynthPanelGenerator.py` (you may not be able to see some of the 109 | folder contents in the `+` file dialog). 110 | 4. Verify that you see the `ModularSynthPanelGenerator` add-in in the `Scripts and Add-Ins` dialog list. 111 | 5. Enable the `Run` option for the `ModularSynthPanelGenerator` add-in. 112 | 113 | When done correctly, the Design workspace `Solid -> Create` menu should have a `Eurorack Panel Generator` option. 114 | 115 | ### From the Autodesk App Store 116 | 117 | _(coming soon)_ 118 | 119 | ## Update 120 | 121 | To update this add-in, download the [latest release][latest-release] into the same location and relaunch Fusion. 122 | 123 | ## Contributing 124 | 125 | This project follows the [Contributor Covenant 3.0 Code of Conduct][coc]. 126 | 127 | Useful links: 128 | 129 | - [Fusion API User's Manual](https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-C1545D80-D804-4CF3-886D-9B5C54B2D7A2) 130 | - [Fusion API Reference Manual](https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-7B5A90C8-E94C-48DA-B16B-430729B734DC) 131 | - [Managing Scripts and Add-Ins][addins] 132 | - [Fusion 360 API Interactive Cheat Sheet](https://fusion360-api-cheatsheet.github.io/) 133 | 134 | Development environment notes: 135 | 136 | - Launch the editor ([Visual Studio Code][vscode]) from inside Fusion by opening the `Scripts and Add-Ins` dialog 137 | (`Shift + S`), right-clicking on the `ModularSynthPanelGenerator` and clicking `Edit in code editor`. This allows you 138 | to attach the vscode debugger to the running process as well as reload after you've made changes. 139 | - I've tried to leave the boilerplate files generated by Fusion's `Create script or add-in` relatively untouched, so 140 | that the code can be as modular as possible. Unused boilerplate has been removed where possible. 141 | - There are many seemingly extraneous `cast()` calls throughout the code to help 142 | [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) understand what's going on, because [many classes in the underlying adsk libraries aren't proper Enums](https://forums.autodesk.com/t5/fusion-api-and-scripts-forum/faulty-enum-types/m-p/12066483). 143 | - I've been using a `.vscode/settings.json` file that looks like this, but it's gitignored because it constains local 144 | paths. AFAIK, there's no way to write these paths in a user or operating system agnostic way. Until there's a better 145 | solution, I recommend creating a throwaway Fusion add-in from the `Scripts and Add-Ins` dialog just to grab those 146 | paths, then create your own local `.vscode/settings.json` file in this project using the paths it generates. You'll 147 | probably also want to add in the other stuff from here, to be consistent. 148 | 149 | ```json 150 | { 151 | "python.autoComplete.extraPaths": ["C:/Users/Cowboy/AppData/Roaming/Autodesk/Autodesk Fusion 360/API/Python/defs"], 152 | "python.analysis.extraPaths": ["C:/Users/Cowboy/AppData/Roaming/Autodesk/Autodesk Fusion 360/API/Python/defs"], 153 | "python.defaultInterpreterPath": "C:/Users/Cowboy/AppData/Local/Autodesk/webdeploy/production/7627f627889be835182cfc345110c3c9f5bc9cc3/Python/python.exe", 154 | "files.exclude": { 155 | "**/__pycache__/": true, 156 | "**/*.py[codz]": true, 157 | "**/*$py.class": true 158 | }, 159 | "editor.tabSize": 4, 160 | "editor.defaultFormatter": "charliermarsh.ruff", 161 | "ruff.lineLength": 160, 162 | "python.analysis.typeCheckingMode": "standard", 163 | "python.analysis.diagnosticsSource": "Pylance" 164 | } 165 | ``` 166 | 167 | Files of interest: 168 | 169 | | File | Description | 170 | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | 171 | | [commands/commandDialog](/commands/commandDialog) | Boilerplate command code generated by Fusion. You likely won't be touching these files. | 172 | | [lib/panelUtils/panel_command.py](/lib/panelUtils/panel_command.py) | Most of the command code that would have gone into the boilerplate command `entry.py` file. This is where the main dialog is initialized and updated. | 173 | | [lib/panelUtils/panel_options.py](/lib/panelUtils/panel_options.py) | `PanelOptions` class with panel options and constants, including convenience getters/setters for ui dialog imputs. | 174 | | [lib/panelUtils/panel_generate.py](/lib/panelUtils/panel_generate.py) | Code that actually generates the panel, including the sketch and extrusions. | 175 | | [lib/generalUtils/debug_utils.py](/lib/generalUtils/debug_utils.py) | Debugging utilities | 176 | | [lib/generalUtils/extrude_utils.py](/lib/generalUtils/extrude_utils.py) | Extrusion utilities | 177 | | [lib/generalUtils/persist_utils.py](/lib/generalUtils/persist_utils.py) | `Persistable` class for persisting defaults to disk | 178 | | [lib/generalUtils/sketch_utils.py](/lib/generalUtils/sketch_utils.py) | Sketch utilities | 179 | | [lib/generalUtils/value_utils.py](/lib/generalUtils/value_utils.py) | Value normalization utilities | 180 | 181 | _(More to come, but in the meantime, if you give this a try and have any issues, please let me know)_ 182 | 183 | ## Support the project 184 | 185 | This add-in is free. However, if you want to support the project you can do so by 186 | [buying me a coffee (or synthesizer)](https://buymeacoffee.com/benalman). Thanks! 187 | 188 | ## Credits 189 | 190 | This work was heavily influenced by the 191 | [FusionGridfinityGenerator](https://github.com/Le0Michine/FusionGridfinityGenerator) add-in. I did my best to solve 192 | problems in my own way, but if you see similarities in the code or README, don't be surprised. There's no way I could've 193 | done this without studying that codebase. I honestly didn't even know Fusion add-ins were a thing until I stumbled 194 | across that project. Yay! 195 | 196 | ## License 197 | 198 | This work is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 199 | License][cc-by-nc-sa]. 200 | 201 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-image]][cc-by-nc-sa] 202 | 203 | [fusion]: https://www.autodesk.com/products/fusion-360 204 | [addins]: https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-9701BBA7-EC0E-4016-A9C8-964AA4838954 205 | [addins-installing]: 206 | https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-9701BBA7-EC0E-4016-A9C8-964AA4838954#Installing 207 | [latest-release]: https://github.com/cowboy/ModularSynthPanelGenerator/releases/latest 208 | [cc-by-nc-sa]: http://creativecommons.org/licenses/by-nc-sa/4.0/ 209 | [cc-by-nc-sa-image]: https://licensebuttons.net/l/by-nc-sa/4.0/88x31.png 210 | [cc-by-nc-sa-shield]: https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg 211 | [coc]: https://github.com/cowboy/ModularSynthPanelGenerator/blob/main/CODE_OF_CONDUCT.md 212 | [vscode]: https://code.visualstudio.com/ 213 | [doepfer-spec]: https://www.doepfer.de/a100_man/a100m_e.htm 214 | [pulplogic-spec]: https://pulplogic.com/1u_tiles/ 215 | [intellijel-spec]: https://intellijel.com/support/1u-technical-specifications/ 216 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. 4 | Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons 5 | makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding 6 | its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons 7 | disclaims all liability for damages resulting from their use to the fullest extent possible. 8 | 9 | **Using Creative Commons Public Licenses** 10 | 11 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders 12 | may use to share original works of authorship and other material subject to copyright and certain other rights specified 13 | in the public license below. The following considerations are for informational purposes only, are not exhaustive, and 14 | do not form part of our licenses. 15 | 16 | - **Considerations for licensors:** Our public licenses are intended for use by those authorized to give the public 17 | permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are 18 | irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying 19 | it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the 20 | material as expected. Licensors should clearly mark any material not subject to the license. This includes other 21 | CC-licensed material, or material used under an exception or limitation to copyright. 22 | [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 23 | 24 | - **Considerations for the public:** By using one of our public licenses, a licensor grants the public permission to use 25 | the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any 26 | reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by 27 | the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has 28 | authority to grant. Use of the licensed material may still be restricted for other reasons, including because others 29 | have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes 30 | be marked or described. Although not required by our licenses, you are encouraged to respect those requests where 31 | reasonable. 32 | [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 33 | 34 | ## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 35 | 36 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this 37 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent 38 | this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your 39 | acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the 40 | Licensor receives from making the Licensed Material available under these terms and conditions. 41 | 42 | ### Section 1 – Definitions. 43 | 44 | a. **Adapted Material** means material subject to Copyright and Similar Rights that is derived from or based upon the 45 | Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise 46 | modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of 47 | this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is 48 | always produced where the Licensed Material is synched in timed relation with a moving image. 49 | 50 | b. **Adapter's License** means the license You apply to Your Copyright and Similar Rights in Your contributions to 51 | Adapted Material in accordance with the terms and conditions of this Public License. 52 | 53 | c. **BY-NC-SA Compatible License** means a license listed at 54 | [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as 55 | essentially the equivalent of this Public License. 56 | 57 | d. **Copyright and Similar Rights** means copyright and/or similar rights closely related to copyright including, 58 | without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the 59 | rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are 60 | not Copyright and Similar Rights. 61 | 62 | e. **Effective Technological Measures** means those measures that, in the absence of proper authority, may not be 63 | circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 64 | 1996, and/or similar international agreements. 65 | 66 | f. **Exceptions and Limitations** means fair use, fair dealing, and/or any other exception or limitation to Copyright 67 | and Similar Rights that applies to Your use of the Licensed Material. 68 | 69 | g. **License Elements** means the license attributes listed in the name of a Creative Commons Public License. The 70 | License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. 71 | 72 | h. **Licensed Material** means the artistic or literary work, database, or other material to which the Licensor applied 73 | this Public License. 74 | 75 | i. **Licensed Rights** means the rights granted to You subject to the terms and conditions of this Public License, which 76 | are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor 77 | has authority to license. 78 | 79 | j. **Licensor** means the individual(s) or entity(ies) granting rights under this Public License. 80 | 81 | k. **NonCommercial** means not primarily intended for or directed towards commercial advantage or monetary compensation. 82 | For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and 83 | Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary 84 | compensation in connection with the exchange. 85 | 86 | l. **Share** means to provide material to the public by any means or process that requires permission under the Licensed 87 | Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or 88 | importation, and to make material available to the public including in ways that members of the public may access the 89 | material from a place and at a time individually chosen by them. 90 | 91 | m. **Sui Generis Database Rights** means rights other than copyright resulting from Directive 96/9/EC of the European 92 | Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as 93 | well as other essentially equivalent rights anywhere in the world. 94 | 95 | n. **You** means the individual or entity exercising the Licensed Rights under this Public License. **Your** has a 96 | corresponding meaning. 97 | 98 | ### Section 2 – Scope. 99 | 100 | a. **_License grant._** 101 | 102 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, 103 | royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed 104 | Material to: 105 | 106 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 107 | 108 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 109 | 110 | 2. **Exceptions and Limitations.** For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this 111 | Public License does not apply, and You do not need to comply with its terms and conditions. 112 | 113 | 3. **Term.** The term of this Public License is specified in Section 6(a). 114 | 115 | 4. **Media and formats; technical modifications allowed.** The Licensor authorizes You to exercise the Licensed Rights 116 | in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do 117 | so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical 118 | modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent 119 | Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by 120 | this Section 2(a)(4) never produces Adapted Material. 121 | 122 | 5. **Downstream recipients.** 123 | 124 | A. **Offer from the Licensor – Licensed Material.** Every recipient of the Licensed Material automatically receives 125 | an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 126 | 127 | B. **Additional offer from the Licensor – Adapted Material.** Every recipient of Adapted Material from You 128 | automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the 129 | conditions of the Adapter’s License You apply. 130 | 131 | C. **No downstream restrictions.** You may not offer or impose any additional or different terms or conditions on, 132 | or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the 133 | Licensed Rights by any recipient of the Licensed Material. 134 | 135 | 6. **No endorsement.** Nothing in this Public License constitutes or may be construed as permission to assert or imply 136 | that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted 137 | official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 138 | 139 | b. **_Other rights._** 140 | 141 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, 142 | privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees 143 | not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the 144 | Licensed Rights, but not otherwise. 145 | 146 | 2. Patent and trademark rights are not licensed under this Public License. 147 | 148 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed 149 | Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory 150 | licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including 151 | when the Licensed Material is used other than for NonCommercial purposes. 152 | 153 | ### Section 3 – License Conditions. 154 | 155 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 156 | 157 | a. **_Attribution._** 158 | 159 | 1. If You Share the Licensed Material (including in modified form), You must: 160 | 161 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 162 | 163 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in 164 | any reasonable manner requested by the Licensor (including by pseudonym if designated); 165 | 166 | ii. a copyright notice; 167 | 168 | iii. a notice that refers to this Public License; 169 | 170 | iv. a notice that refers to the disclaimer of warranties; 171 | 172 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 173 | 174 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 175 | 176 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or 177 | hyperlink to, this Public License. 178 | 179 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context 180 | in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a 181 | URI or hyperlink to a resource that includes the required information. 182 | 183 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent 184 | reasonably practicable. 185 | 186 | b. **_ShareAlike._** 187 | 188 | In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also 189 | apply. 190 | 191 | 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or 192 | later, or a BY-NC-SA Compatible License. 193 | 194 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this 195 | condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 196 | 197 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological 198 | Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 199 | 200 | ### Section 4 – Sui Generis Database Rights. 201 | 202 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 203 | 204 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a 205 | substantial portion of the contents of the database for NonCommercial purposes only; 206 | 207 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis 208 | Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is 209 | Adapted Material, including for purposes of Section 3(b); and 210 | 211 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the 212 | database. 213 | 214 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License 215 | where the Licensed Rights include other Copyright and Similar Rights. 216 | 217 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 218 | 219 | a. **Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed 220 | Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed 221 | Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, 222 | merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or 223 | the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed 224 | in full or in part, this disclaimer may not apply to You.** 225 | 226 | b. **To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without 227 | limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, 228 | or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if 229 | the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of 230 | liability is not allowed in full or in part, this limitation may not apply to You.** 231 | 232 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the 233 | extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 234 | 235 | ### Section 6 – Term and Termination. 236 | 237 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to 238 | comply with this Public License, then Your rights under this Public License terminate automatically. 239 | 240 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 241 | 242 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the 243 | violation; or 244 | 245 | 2. upon express reinstatement by the Licensor. 246 | 247 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your 248 | violations of this Public License. 249 | 250 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or 251 | stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 252 | 253 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 254 | 255 | ### Section 7 – Other Terms and Conditions. 256 | 257 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless 258 | expressly agreed. 259 | 260 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from 261 | and independent of the terms and conditions of this Public License. 262 | 263 | ### Section 8 – Interpretation. 264 | 265 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, 266 | or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this 267 | Public License. 268 | 269 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically 270 | reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be 271 | severed from this Public License without affecting the enforceability of the remaining terms and conditions. 272 | 273 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly 274 | agreed to by the Licensor. 275 | 276 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges 277 | and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 278 | 279 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of 280 | > its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the 281 | > limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise 282 | > permitted by the Creative Commons policies published at 283 | > [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of 284 | > the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent 285 | > including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any 286 | > other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, 287 | > this paragraph does not form part of the public licenses. 288 | > 289 | > Creative Commons may be contacted at creativecommons.org 290 | --------------------------------------------------------------------------------