├── 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]
4 | [![CC BY-NC-SA 4.0][cc-by-nc-sa-shield]][cc-by-nc-sa]
5 | [][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 | |  |  |
57 |
58 | ### Generated panel examples
59 |
60 | | 3U 6HP Panel (no reinforcement) | 3U 2HP Panel (solid reinforcement) | 3U 6HP Panel (shell reinforcement) |
61 | | ------------------------------- | ----------------------------------- | ----------------------------------- |
62 | |  |  |  |
63 |
64 | | 1U 12HP Panel (solid reinforcement) | 3U 2HP Panel (solid reinforcement) | 3U 6HP Panel (shell reinforcement) |
65 | | ------------------------------------ | -------------------------------------- | -------------------------------------- |
66 | |  |  |  |
67 |
68 | ### Generated sketch examples
69 |
70 | | Example generated sketch | Panel width HP in sketch is editable |
71 | | -------------------------------- | ------------------------------------------- |
72 | |  |  |
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 |
--------------------------------------------------------------------------------