diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66ed5e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/.vscode +/__pycache__ +/defaults.json \ No newline at end of file diff --git a/FingerJoints.manifest b/FingerJoints.manifest new file mode 100644 index 0000000..f6242f6 --- /dev/null +++ b/FingerJoints.manifest @@ -0,0 +1,13 @@ +{ + "autodeskProduct": "Fusion360", + "type": "addin", + "id": "51e13efe-8453-49c7-becd-bfed6571c1cf", + "author": "Florian Pommerening", + "description": { + "": "An add-in for making finger joints." + }, + "version": "1.0.0", + "runOnStartup": true, + "supportedOS": "windows|mac", + "editEnabled": true +} \ No newline at end of file diff --git a/FingerJoints.py b/FingerJoints.py new file mode 100644 index 0000000..502266c --- /dev/null +++ b/FingerJoints.py @@ -0,0 +1,91 @@ +#Author-Florian Pommerening +#Description-An Add-In for making finger joints. + +# Select two overlapping bodies and a direction. The overlap is cut along the +# direction multiple times resulting in the individual fingers/notches. We +# then remove every second finger from the first body and the other fingers +# from the second body. The remaining bodies then do not overlap anymore. + +# Some inspiration was taken from the dogbone add-in developed by Peter +# Ludikar, Gary Singer, Patrick Rainsberry, David Liu, and Casey Rogers. + +import adsk.core +import adsk.fusion + +from . import commands +from . import options +from . import ui +from . import geometry + + +# Global variable to hold the add-in (created in run(), destroyed in stop()) +addin = None + + +class FingerJointCommand(commands.RunningCommandBase): + def __init__(self, args: adsk.core.CommandCreatedEventArgs): + super(FingerJointCommand, self).__init__(args) + self.options = options.FingerJointOptions() + self.ui = ui.FingerJointUI(args.command.commandInputs, self.options) + + def onInputChanged(self, args: adsk.core.InputChangedEventArgs): + self.ui.updateVisibility() + self.ui.focusNextSelectionInput() + + def onValidate(self, args: adsk.core.ValidateInputsEventArgs): + args.areInputsValid = self.ui.areInputsValid() + + def onExecutePreview(self, args: adsk.core.CommandEventArgs): + if self.ui.isPreviewEnabled(): + if self.doExecute(): + args.isValidResult = True + self.ui.setInputErrorMessage('') + else: + self.ui.setInputErrorMessage('Finger joints could not be completed. Try selecting overlapping bodies and double-check the dimensions.') + + def onExecute(self, args: adsk.core.CommandEventArgs): + if not self.doExecute(): + args.executeFailed = True + args.executeFailedMessage = 'Finger joints could not be completed. Try selecting overlapping bodies and double-check the dimensions.' + + def doExecute(self): + self.ui.setRelevantOptions(self.options) + body0 = self.ui.getBody0() + body1 = self.ui.getBody1() + direction = self.ui.getDirection() + return geometry.createFingerJoint(body0, body1, direction, self.options) + + def onDestroy(self, args: adsk.core.CommandEventArgs): + super(FingerJointCommand, self).onDestroy(args) + if args.terminationReason == adsk.core.CommandTerminationReason.CompletedTerminationReason: + self.options.writeDefaults() + + +class FingerJointAddIn(object): + def __init__(self): + self.button = commands.CommandButton('FingerJointBtn', 'SolidModifyPanel', FingerJointCommand) + + def start(self): + self.button.addToUI('Finger Joint', + 'Creates a finger joint from the overlap of two bodies', + 'resources/ui/command_button') + + def stop(self): + self.button.removeFromUI() + + +def run(context): + global addin + try: + if addin is not None: + stop() + addin = FingerJointAddIn() + addin.start() + except: + ui.reportError('Uncaught exception', True) + + +def stop(context): + global addin + addin.stop() + addin = None diff --git a/README.md b/README.md new file mode 100644 index 0000000..99c9b8e --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Finger Joints + +This add-in for Autodesk Fusion 360 can create a finger joint (box joint) from the overlap of two objects. Although, not specifically designed for this, the add-in also works for lap joints and can cut pieces into slices. + +![Finger joint](resources/doc/demo.png) + + +## Installation + +Download the [latest version of the plugin](https://github.com/FlorianPommerening/FingerJoints/archive/master.zip), unpack it to your add-ins directory (see below) and remove the "-master" from the name. To start the add-in inside Fusion use the "ADD-INS" button in the "TOOLS" ribbon, then find the add-in in the "Add-Ins" tab, select it and click "Run". + +Add-ins directory + +* Windows: `%AppData%\Autodesk\Autodesk Fusion 360\API\AddIns` +* Mac OS X: `~/Library/Application Support/Autodesk/Autodesk Fusion 360/API/AddIns` + or ` ~/Library/Containers/com.autodesk.mas.fusion360/Data/Library/Application Support/Autodesk/Autodesk Fusion 360/API/AddIns` + +There are also [more detailed installation instructions](https://tapnair.github.io/installation.html) by Patrick Rainsberry. + + +## Usage + +Select two overlapping bodies and a direction. The direction is required, so the add-in knows in which plane to cut. In most cases, you probably want an edge that runs along the joint as shown in the picture. + +![Selection of body and edges](resources/doc/usage1.png) + +The next setting controls whether the first body has fingers on both ends, notches on both ends, or a finger on one end and a notch on the other. In the next picture, we place three fingers on the first body with different settings, the number of fingers on the second body adjusts accordingly. + +![Finger placement](resources/doc/usage2.png) + +The next settings determine how the size of the overlap is distributed between fingers of the first body and fingers of the second body (as we view everything from the perspective of the first body, we call fingers of the second body "notches"). + +When the number of fingers is fixed, we can specify their size in three ways. In the example above, we chose to have fingers and notches of equal size. Instead, we can chose to fix the size of either fingers or notches (the other size is determined automatically). The next picture shows the effect of fixing the size of fingers or notches to 5mm. + +![Fixed finger size with fixed number of fingers](resources/doc/usage3.png) + +Finally, instead of fixing the number of fingers, we might want to have both fingers and notches of a certain size and place as many fingers as will fit along the joint. Fixing both sizes could lead to cases where one body ends in half a finger, so instead, we only fix either the size of fingers or notches and specify a minimal size for the other. For example, the following picture fixes the size of a finger to 9mm and uses a minimal notch size of 5mm. The height of the joint is 50mm, so using four 9mm fingers and three 5mm notches would exceed the size by 1 mm. Instead, the add-in uses three 9mm fingers and increases the size of the two notches to 11.5mm. Likewise, we could fix the size of notches and make the size of fingers dynamic, or we could require that fingers and notches have the same size. + +![Dynamic finger number with fixed finger size](resources/doc/usage4.png) + + +Of course, the objects you join do not have to be rectangular and can overlap in multiple places. + +![General objects can be joined](resources/doc/usage5.png) +![Resulting objects](resources/doc/usage6.png) + + +## Other Uses + +In addition to creating finger joints, the add-in can be used to create lap joints and cut pieces into slices. + +### Lap Joints + +To create a lap joint, start with two overlapping pieces and create a finger joint with one finger placed asymmertically on the start or end of the first body. + +![Lap joint settings](resources/doc/lapjointssettings.png) +![Lap joint result](resources/doc/lapjointsresults.png) + +### Slicing + +This is not really by design but you can duplicate a body so it perfectly overlaps with itself, then create a finger joint between the +copy and the original. This will slice the body along your chosen axis. + +![Slicing](resources/doc/slicing1.png) +![Slicing](resources/doc/slicing2.png) + + +## Issues + +If you find any issues with the add-in, please report them on [Github][issuetracker] + +## TODO / Help wanted + +If you have ideas how to improve the add-in, you can create an issue on [Github][issuetracker]. Since I am working on this in my free time I cannot promise I will get to it, though. Pull requests for fixes and new features are very welcome. + +The add-in is currently not parametric, so updating the underlying geometry will not update the placement or size of the fingers. I'm not sure this is possible with the current API (there is a [related request on the Fusion 360 IdeaStation][ideastation]). If you have an idea of how to implement this, please get in touch. + +I am not an artist, so the icons are still [Programmer art](https://en.wikipedia.org/wiki/Programmer_art). I'd be happy to replace them with something closer to the style of the built-in commands. + +## License + +[![Creative Commons License][by-nc-sa-logo]][by-nc-sa-link] + +This add-in is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][by-nc-sa-link]. If you are interested in using the add-in under different terms, contact florian.pommerening@unibas.ch. + + +[by-nc-sa-logo]: https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png +[by-nc-sa-link]: http://creativecommons.org/licenses/by-nc-sa/4.0/ +[issuetracker]: https://github.com/FlorianPommerening/FingerJoints/issues +[ideastation]: https://forums.autodesk.com/t5/fusion-360-ideastation/allow-add-ins-to-be-fully-parametric-and-represented-in-the/idi-p/8660436 \ No newline at end of file diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..2a3c62f --- /dev/null +++ b/commands.py @@ -0,0 +1,131 @@ +import adsk.core +import adsk.fusion + +from . import ui + +# Keep track of all currently running commands in a global set, so their +# callback handlers are not GC'd. +running_commands = set() + +class RunningCommandBase(object): + """ + Base class to keep persistent data during the lifetime of a command from + creation to destruction. The constructor of this class automatically adds + the instance to running_commands and the onDestroy event removes it again. + To use this class, inherit from it an override the events. + """ + + def __init__(self, args): + running_commands.add(self) + + cmd = adsk.core.Command.cast(args.command) + + self._inputChangedHandler = makeForwardingHandler( + adsk.core.InputChangedEventHandler, self.onInputChanged) + cmd.inputChanged.add(self._inputChangedHandler) + + self._selectionHandler = makeForwardingHandler( + adsk.core.SelectionEventHandler, self.onSelectionEvent) + cmd.selectionEvent.add(self._selectionHandler) + + self._validateHandler = makeForwardingHandler( + adsk.core.ValidateInputsEventHandler, self.onValidate) + cmd.validateInputs.add(self._validateHandler) + + self._executeHandler = makeForwardingHandler( + adsk.core.CommandEventHandler, self.onExecute) + cmd.execute.add(self._executeHandler) + + self._executePreviewHandler = makeForwardingHandler( + adsk.core.CommandEventHandler, self.onExecutePreview) + cmd.executePreview.add(self._executePreviewHandler) + + self._destroyHandler = makeForwardingHandler( + adsk.core.CommandEventHandler, self.onDestroy) + cmd.destroy.add(self._destroyHandler) + + def onCreated(self, args): + pass + + def onInputChanged(self, args): + pass + + def onSelectionEvent(self, args): + pass + + def onValidate(self, args): + pass + + def onExecute(self, args): + pass + + def onExecutePreview(self, args): + pass + + def onDestroy(self, args): + running_commands.remove(self) + + +class CommandCreatedHandler(adsk.core.CommandCreatedEventHandler): + def __init__(self, runningCommandClass): + super().__init__() + self.runningCommandClass = runningCommandClass + + def notify(self, args): + try: + running_command = self.runningCommandClass(args) + running_command.onCreated(args) + except: + ui.reportError('Command creation callback method failed', True) + + +def makeForwardingHandler(handler_cls, callback): + class ForwardingHandler(handler_cls): + def __init__(self, callback): + super().__init__() + self.callback = callback + + def notify(self, args): + try: + self.callback(args) + except: + ui.reportError('Callback method failed', True) + return ForwardingHandler(callback) + + +class CommandButton(object): + def __init__(self, commandID, panelName, commandDataClass): + self.commandID = commandID + self.panelName = panelName + + fusion = adsk.core.Application.get() + self.fusionUI = fusion.userInterface + self.creationHandler = CommandCreatedHandler(commandDataClass) + + def addToUI(self, name, tooltip='', resourceFolder=''): + # If there are existing instances of the button, clean them up first. + try: + self.removeFromUI() + except: + pass + + # Create a command definition and button for it. + commandDefinition = self.fusionUI.commandDefinitions.addButtonDefinition( + self.commandID, name, tooltip, resourceFolder) + commandDefinition.commandCreated.add(self.creationHandler) + + panel = self.fusionUI.allToolbarPanels.itemById(self.panelName) + buttonControl = panel.controls.addCommand(commandDefinition) + + # Make the button available in the panel. + buttonControl.isPromotedByDefault = True + buttonControl.isPromoted = True + + def removeFromUI(self): + commandDefinition = self.fusionUI.commandDefinitions.itemById(self.commandID) + if commandDefinition: + commandDefinition.deleteMe() + panel = self.fusionUI.allToolbarPanels.itemById(self.panelName) + buttonControl = panel.controls.itemById(self.commandID) + if buttonControl: + buttonControl.deleteMe() diff --git a/geometry.py b/geometry.py new file mode 100644 index 0000000..c7e712a --- /dev/null +++ b/geometry.py @@ -0,0 +1,246 @@ +import adsk.core +import adsk.fusion + +from .options import DynamicSizeType, PlacementType + + +def findOrthogonalUnitVectors(z): + v = adsk.core.Vector3D.create(1, 0, 0) + if v.isParallelTo(z): + v = adsk.core.Vector3D.create(0, 1, 0) + x = z.crossProduct(v) + x.normalize() + y = z.crossProduct(x) + y.normalize() + return x, y + + +class CoordinateSystem(object): + def __init__(self, direction): + if isinstance(direction, adsk.fusion.BRepEdge): + self.origin = direction.startVertex.geometry + self.direction = self.origin.vectorTo(direction.endVertex.geometry) + else: + assert(isinstance(direction, adsk.fusion.SketchLine)) + self.origin = direction.startSketchPoint.worldGeometry + self.direction = self.origin.vectorTo(direction.endSketchPoint.worldGeometry) + self.direction.normalize() + + xAxis, yAxis = findOrthogonalUnitVectors(self.direction) + self.transform = adsk.core.Matrix3D.create() + self.transform.setWithCoordinateSystem( + self.origin, + xAxis, + yAxis, + self.direction) + + self.inverseTransform = self.transform.copy() + self.inverseTransform.invert() + + def transformToLocalCoordinates(self, body): + temporaryBRepManager = adsk.fusion.TemporaryBRepManager.get() + temporaryBRepManager.transform(body, self.inverseTransform) + + def transformToGlobalCoordinates(self, body): + temporaryBRepManager = adsk.fusion.TemporaryBRepManager.get() + temporaryBRepManager.transform(body, self.transform) + + +def createBox(x, y, z, length, width, height): + centerPoint = adsk.core.Point3D.create(x, y, z) + lengthDirection = adsk.core.Vector3D.create(1, 0, 0) + widthDirection = adsk.core.Vector3D.create(0, 1, 0) + return adsk.core.OrientedBoundingBox3D.create(centerPoint, lengthDirection, widthDirection, length, width, height) + + +def createSlicesBody(body, slices, debug=False): + bb = body.boundingBox + minx, miny, minz = bb.minPoint.asArray() + maxx, maxy, maxz = bb.maxPoint.asArray() + cx = (minx + maxx) / 2 + cy = (miny + maxy) / 2 + # To avoid issues with rounding, we add 1cm of slack. + slack = 1 + length = maxx - minx + slack + width = maxy - miny + slack + + temporaryBRepManager = adsk.fusion.TemporaryBRepManager.get() + targetBody = None + for (sliceCenterStart, sliceThickness) in slices: + box = createBox(cx, cy, minz + sliceCenterStart + sliceThickness/2, length, width, sliceThickness) + sliceBody = temporaryBRepManager.createBox(box) + if targetBody is None: + targetBody = sliceBody + else: + temporaryBRepManager.booleanOperation(targetBody, sliceBody, adsk.fusion.BooleanTypes.UnionBooleanType) + + if debug: + app = adsk.core.Application.get() + root = app.activeProduct.rootComponent + feature = root.features.baseFeatures.add() + feature.startEdit() + root.bRepBodies.add(targetBody, feature) + feature.finishEdit() + feature = root.features.baseFeatures.add() + feature.startEdit() + root.bRepBodies.add(body, feature) + feature.finishEdit() + + temporaryBRepManager.booleanOperation(targetBody, body, adsk.fusion.BooleanTypes.IntersectionBooleanType) + return targetBody + + +def createBodyFromOverlap(body0, body1): + temporaryBRepManager = adsk.fusion.TemporaryBRepManager.get() + overlapBody = temporaryBRepManager.copy(body0) + temporaryBRepManager.booleanOperation(overlapBody, body1, adsk.fusion.BooleanTypes.IntersectionBooleanType) + return overlapBody + +def cutFingersIntoBodies(body0, body1, fingers): + app = adsk.core.Application.get() + root = app.activeProduct.rootComponent + combineFeatures = root.features.combineFeatures + + # Add the fingers to the document so they can interact with the other bodies in the document. + feature = root.features.baseFeatures.add() + feature.startEdit() + root.bRepBodies.add(fingers, feature) + feature.finishEdit() + + # Cut the fingers out of body1. + fingerCollection = adsk.core.ObjectCollection.create() + for i in range(feature.bodies.count): + fingerCollection.add(feature.bodies.item(i)) + cut1Input = combineFeatures.createInput(body1, fingerCollection) + cut1Input.operation = adsk.fusion.FeatureOperations.CutFeatureOperation + combineFeature = combineFeatures.add(cut1Input) + + # Cut body0 with body1 (since we removed the fingers from body1, this leaves them on body0). + body1Collection = adsk.core.ObjectCollection.create() + for i in range(combineFeature.bodies.count): + body1Collection.add(combineFeature.bodies.item(i)) + cut0Input = combineFeatures.createInput(body0, body1Collection) + cut0Input.operation = adsk.fusion.FeatureOperations.CutFeatureOperation + cut0Input.isKeepToolBodies = True + combineFeatures.add(cut0Input) + + +def createFingerJoint(body0, body1, direction, options): + coordinateSystem = CoordinateSystem(direction) + overlap = createBodyFromOverlap(body0, body1) + coordinateSystem.transformToLocalCoordinates(overlap) + # TODO: look at MeasureManager.getOrientedBoundingBox to see if this can be simplified, probably with direction.geometry/worldGeometry + + bb = overlap.boundingBox + height = bb.maxPoint.z - bb.minPoint.z + if height <= 0: + return True + slices = defineSlices(height, options) + if slices is None: + return False + + fingers = createSlicesBody(overlap, slices) + coordinateSystem.transformToGlobalCoordinates(fingers) + cutFingersIntoBodies(body0, body1, fingers) + return True + + +def defineSlices(size, options): + if options.isNumberOfFingersFixed: + # The number of fingers is given, the number of notches depends on their placement. + numFingers = options.fixedNumFingers + if options.placementType == PlacementType.FINGERS_OUTSIDE: + numNotches = numFingers - 1 + elif options.placementType == PlacementType.NOTCHES_OUTSIDE: + numNotches = numFingers + 1 + else: + numNotches = numFingers + # Once the number of fingers and notches is fixed, their size can be determined. + if options.dynamicSizeType == DynamicSizeType.EQUAL_NOTCH_AND_FINGER_SIZE: + fingerSize = size / (numFingers + numNotches) + notchSize = fingerSize + elif options.dynamicSizeType == DynamicSizeType.FIXED_NOTCH_SIZE: + notchSize = options.fixedNotchSize + fingerSize = (size - numNotches * notchSize) / numFingers + elif options.dynamicSizeType == DynamicSizeType.FIXED_FINGER_SIZE: + fingerSize = options.fixedFingerSize + notchSize = (size - numFingers * fingerSize) / numNotches + else: # Both fingers and notches are dynamically sized. + + # If fingers and notches have the same size, this size depends only on their placement and the minimal length. + if options.dynamicSizeType == DynamicSizeType.EQUAL_NOTCH_AND_FINGER_SIZE: + maxNumFingersAndNotches = int(size / options.minFingerSize) + # If there are the same number of fingers and notches the number needs to be even, otherwise odd. + # We treat the number as even (rounding down) and correct when the number was odd. + numFingers = numNotches = int(maxNumFingersAndNotches / 2) + if options.placementType == PlacementType.FINGERS_OUTSIDE: + if maxNumFingersAndNotches % 2 == 1: + numFingers += 1 + else: + numNotches -= 1 + elif options.placementType == PlacementType.NOTCHES_OUTSIDE: + if maxNumFingersAndNotches % 2 == 1: + numNotches += 1 + else: + numFingers -= 1 + + if numFingers + numNotches == 0: + return None + # Once the number of fingers and notches is known, we can compute their size. + fingerSize = size / (numFingers + numNotches) + notchSize = fingerSize + + # Notches have a fixed size, only fingers are dynamically sized. + elif options.dynamicSizeType == DynamicSizeType.FIXED_NOTCH_SIZE: + notchSize = options.fixedNotchSize + # Depending on the placement, we either need an additional notch or an additional finger + # (one less notch). We pretend the part is longer or shorter by the size of one notch in + # this case. This way, the virtual part always contains the same number of fingers as notches. + extraNotch = 0 + if options.placementType == PlacementType.FINGERS_OUTSIDE: + extraNotch = -1 + elif options.placementType == PlacementType.NOTCHES_OUTSIDE: + extraNotch = 1 + virtualSize = size - extraNotch * notchSize + numFingers = int(virtualSize / (notchSize + options.minFingerSize)) + numNotches = numFingers + extraNotch + if numFingers == 0: + return None + fingerSize = (size - numNotches * notchSize) / numFingers + + # Fingers have a fixed size, only notches are dynamically sized. + elif options.dynamicSizeType == DynamicSizeType.FIXED_FINGER_SIZE: + fingerSize = options.fixedFingerSize + # Depending on the placement, we either need an additional finger or an additional notch + # (one less finger). We pretend the part is longer or shorter by the size of one finger in + # this case. This way, the virtual part always contains the same number of fingers as notches. + extraFinger = 0 + if options.placementType == PlacementType.FINGERS_OUTSIDE: + extraFinger = 1 + elif options.placementType == PlacementType.NOTCHES_OUTSIDE: + extraFinger = -1 + virtualSize = size - extraFinger * fingerSize + numNotches = int(virtualSize / (fingerSize + options.minNotchSize)) + numFingers = numNotches + extraFinger + if numNotches == 0: + return None + notchSize = (size - numFingers * fingerSize) / numNotches + + # Now that number and size of fingers and notches are defined, we set the position of the first finger. + if options.placementType in [PlacementType.FINGERS_OUTSIDE, PlacementType.SAME_NUMBER_START_FINGER]: + fingerStart = 0 + else: + fingerStart = notchSize + + # Sanity-check the dimensions before passing them along. + epsilon = 0.00001 # avoid rounding issues with floats + if (fingerSize <= epsilon + or notchSize <= epsilon + or numFingers < 0 + or numNotches < 0 + or numFingers + numNotches == 1 + or fingerSize * numFingers + notchSize * numNotches - epsilon > size): + return None + + fingerDistance = fingerSize + notchSize + return [(fingerStart + i*fingerDistance, fingerSize) for i in range(numFingers)] diff --git a/options.py b/options.py new file mode 100644 index 0000000..8198717 --- /dev/null +++ b/options.py @@ -0,0 +1,71 @@ +import json +import os + +import adsk.core + +from . import ui + +APP_PATH = os.path.dirname(os.path.abspath(__file__)) + +class DynamicSizeType: + FIXED_NOTCH_SIZE = 'fixed notch size' + FIXED_FINGER_SIZE = 'fixed finger size' + EQUAL_NOTCH_AND_FINGER_SIZE = 'equal notch and finger size' + +class PlacementType: + FINGERS_OUTSIDE = 'fingers outside' + NOTCHES_OUTSIDE = 'notches outside' + SAME_NUMBER_START_FINGER = 'same number of fingers and notches (start with finger)' + SAME_NUMBER_START_NOTCH = 'same number of fingers and notches (start with notch)' + + +class FingerJointOptions(object): + DEFAULTS_FILENAME = os.path.join(APP_PATH, 'defaults.json') + DEFAULTS_DATA = {} + + def __init__(self): + self.dynamicSizeType = DynamicSizeType.EQUAL_NOTCH_AND_FINGER_SIZE + self.placementType = PlacementType.FINGERS_OUTSIDE + self.isNumberOfFingersFixed = False + self.fixedFingerSize = 2 + self.fixedNotchSize = 2 + self.minFingerSize = 2 + self.minNotchSize = 2 + self.fixedNumFingers = 3 + self.isPreviewEnabled = True + self.readDefaults() + + def writeDefaults(self): + defaultData = { + 'dynamicSizeType': self.dynamicSizeType, + 'placementType': self.placementType, + 'isNumberOfFingersFixed': self.isNumberOfFingersFixed, + 'fixedFingerSize': self.fixedFingerSize, + 'fixedNotchSize': self.fixedNotchSize, + 'minFingerSize': self.minFingerSize, + 'minNotchSize': self.minNotchSize, + 'fixedNumFingers': self.fixedNumFingers, + 'isPreviewEnabled': self.isPreviewEnabled, + } + with open(self.DEFAULTS_FILENAME, 'w', encoding='UTF-8') as json_file: + json.dump(defaultData, json_file, ensure_ascii=False) + + def readDefaults(self): + if not os.path.isfile(self.DEFAULTS_FILENAME): + return + with open(self.DEFAULTS_FILENAME, 'r', encoding='UTF-8') as json_file: + try: + defaultData = json.load(json_file) + except ValueError: + ui.reportError('Cannot read default options. Invalid JSON in "%s":' % self.DEFAULTS_FILENAME) + + self.dynamicSizeType = defaultData.get('dynamicSizeType', self.dynamicSizeType) + self.placementType = defaultData.get('placementType', self.placementType) + self.isNumberOfFingersFixed = defaultData.get('isNumberOfFingersFixed', self.isNumberOfFingersFixed) + self.fixedFingerSize = defaultData.get('fixedFingerSize', self.fixedFingerSize) + self.fixedNotchSize = defaultData.get('fixedNotchSize', self.fixedNotchSize) + self.minFingerSize = defaultData.get('minFingerSize', self.minFingerSize) + self.minNotchSize = defaultData.get('minNotchSize', self.minNotchSize) + self.fixedNumFingers = defaultData.get('fixedNumFingers', self.fixedNumFingers) + self.isPreviewEnabled = defaultData.get('isPreviewEnabled', self.isPreviewEnabled) + diff --git a/resources/doc/demo.png b/resources/doc/demo.png new file mode 100644 index 0000000..6b2cea5 Binary files /dev/null and b/resources/doc/demo.png differ diff --git a/resources/doc/lapjointsresults.png b/resources/doc/lapjointsresults.png new file mode 100644 index 0000000..fd118f7 Binary files /dev/null and b/resources/doc/lapjointsresults.png differ diff --git a/resources/doc/lapjointssettings.png b/resources/doc/lapjointssettings.png new file mode 100644 index 0000000..5ef8db8 Binary files /dev/null and b/resources/doc/lapjointssettings.png differ diff --git a/resources/doc/logo.png b/resources/doc/logo.png new file mode 100644 index 0000000..3b0151a Binary files /dev/null and b/resources/doc/logo.png differ diff --git a/resources/doc/slicing1.png b/resources/doc/slicing1.png new file mode 100644 index 0000000..1478976 Binary files /dev/null and b/resources/doc/slicing1.png differ diff --git a/resources/doc/slicing2.png b/resources/doc/slicing2.png new file mode 100644 index 0000000..d04e042 Binary files /dev/null and b/resources/doc/slicing2.png differ diff --git a/resources/doc/usage1.png b/resources/doc/usage1.png new file mode 100644 index 0000000..0f9c1ad Binary files /dev/null and b/resources/doc/usage1.png differ diff --git a/resources/doc/usage2.png b/resources/doc/usage2.png new file mode 100644 index 0000000..fac92bb Binary files /dev/null and b/resources/doc/usage2.png differ diff --git a/resources/doc/usage3.png b/resources/doc/usage3.png new file mode 100644 index 0000000..9e86084 Binary files /dev/null and b/resources/doc/usage3.png differ diff --git a/resources/doc/usage4.png b/resources/doc/usage4.png new file mode 100644 index 0000000..b847369 Binary files /dev/null and b/resources/doc/usage4.png differ diff --git a/resources/doc/usage5.png b/resources/doc/usage5.png new file mode 100644 index 0000000..03188e5 Binary files /dev/null and b/resources/doc/usage5.png differ diff --git a/resources/doc/usage6.png b/resources/doc/usage6.png new file mode 100644 index 0000000..314f82e Binary files /dev/null and b/resources/doc/usage6.png differ diff --git a/resources/ui/command_button/16x16-dark.png b/resources/ui/command_button/16x16-dark.png new file mode 100644 index 0000000..286c370 Binary files /dev/null and b/resources/ui/command_button/16x16-dark.png differ diff --git a/resources/ui/command_button/16x16-disabled.png b/resources/ui/command_button/16x16-disabled.png new file mode 100644 index 0000000..75e834b Binary files /dev/null and b/resources/ui/command_button/16x16-disabled.png differ diff --git a/resources/ui/command_button/16x16.png b/resources/ui/command_button/16x16.png new file mode 100644 index 0000000..286c370 Binary files /dev/null and b/resources/ui/command_button/16x16.png differ diff --git a/resources/ui/command_button/32x32-dark.png b/resources/ui/command_button/32x32-dark.png new file mode 100644 index 0000000..86c532c Binary files /dev/null and b/resources/ui/command_button/32x32-dark.png differ diff --git a/resources/ui/command_button/32x32-disabled.png b/resources/ui/command_button/32x32-disabled.png new file mode 100644 index 0000000..20d0d18 Binary files /dev/null and b/resources/ui/command_button/32x32-disabled.png differ diff --git a/resources/ui/command_button/32x32.png b/resources/ui/command_button/32x32.png new file mode 100644 index 0000000..86c532c Binary files /dev/null and b/resources/ui/command_button/32x32.png differ diff --git a/resources/ui/command_button/32x32@2x-dark.png b/resources/ui/command_button/32x32@2x-dark.png new file mode 100644 index 0000000..86c532c Binary files /dev/null and b/resources/ui/command_button/32x32@2x-dark.png differ diff --git a/resources/ui/command_button/32x32@2x.png b/resources/ui/command_button/32x32@2x.png new file mode 100644 index 0000000..86c532c Binary files /dev/null and b/resources/ui/command_button/32x32@2x.png differ diff --git a/resources/ui/dynamic_type_equal_notch_and_finger/16x16-disabled.png b/resources/ui/dynamic_type_equal_notch_and_finger/16x16-disabled.png new file mode 100644 index 0000000..a4db921 Binary files /dev/null and b/resources/ui/dynamic_type_equal_notch_and_finger/16x16-disabled.png differ diff --git a/resources/ui/dynamic_type_equal_notch_and_finger/16x16-normal.png b/resources/ui/dynamic_type_equal_notch_and_finger/16x16-normal.png new file mode 100644 index 0000000..cfd7b73 Binary files /dev/null and b/resources/ui/dynamic_type_equal_notch_and_finger/16x16-normal.png differ diff --git a/resources/ui/dynamic_type_equal_notch_and_finger/32x32-disabled.png b/resources/ui/dynamic_type_equal_notch_and_finger/32x32-disabled.png new file mode 100644 index 0000000..4e02072 Binary files /dev/null and b/resources/ui/dynamic_type_equal_notch_and_finger/32x32-disabled.png differ diff --git a/resources/ui/dynamic_type_equal_notch_and_finger/32x32-normal.png b/resources/ui/dynamic_type_equal_notch_and_finger/32x32-normal.png new file mode 100644 index 0000000..895c3ae Binary files /dev/null and b/resources/ui/dynamic_type_equal_notch_and_finger/32x32-normal.png differ diff --git a/resources/ui/dynamic_type_fixed_finger/16x16-disabled.png b/resources/ui/dynamic_type_fixed_finger/16x16-disabled.png new file mode 100644 index 0000000..1746b01 Binary files /dev/null and b/resources/ui/dynamic_type_fixed_finger/16x16-disabled.png differ diff --git a/resources/ui/dynamic_type_fixed_finger/16x16-normal.png b/resources/ui/dynamic_type_fixed_finger/16x16-normal.png new file mode 100644 index 0000000..0e7661c Binary files /dev/null and b/resources/ui/dynamic_type_fixed_finger/16x16-normal.png differ diff --git a/resources/ui/dynamic_type_fixed_finger/32x32-disabled.png b/resources/ui/dynamic_type_fixed_finger/32x32-disabled.png new file mode 100644 index 0000000..8709929 Binary files /dev/null and b/resources/ui/dynamic_type_fixed_finger/32x32-disabled.png differ diff --git a/resources/ui/dynamic_type_fixed_finger/32x32-normal.png b/resources/ui/dynamic_type_fixed_finger/32x32-normal.png new file mode 100644 index 0000000..04a2ceb Binary files /dev/null and b/resources/ui/dynamic_type_fixed_finger/32x32-normal.png differ diff --git a/resources/ui/dynamic_type_fixed_notch/16x16-disabled.png b/resources/ui/dynamic_type_fixed_notch/16x16-disabled.png new file mode 100644 index 0000000..825b719 Binary files /dev/null and b/resources/ui/dynamic_type_fixed_notch/16x16-disabled.png differ diff --git a/resources/ui/dynamic_type_fixed_notch/16x16-normal.png b/resources/ui/dynamic_type_fixed_notch/16x16-normal.png new file mode 100644 index 0000000..8b15d81 Binary files /dev/null and b/resources/ui/dynamic_type_fixed_notch/16x16-normal.png differ diff --git a/resources/ui/dynamic_type_fixed_notch/32x32-disabled.png b/resources/ui/dynamic_type_fixed_notch/32x32-disabled.png new file mode 100644 index 0000000..ff162ce Binary files /dev/null and b/resources/ui/dynamic_type_fixed_notch/32x32-disabled.png differ diff --git a/resources/ui/dynamic_type_fixed_notch/32x32-normal.png b/resources/ui/dynamic_type_fixed_notch/32x32-normal.png new file mode 100644 index 0000000..9784253 Binary files /dev/null and b/resources/ui/dynamic_type_fixed_notch/32x32-normal.png differ diff --git a/resources/ui/finger_number_dynamic/16x16-disabled.png b/resources/ui/finger_number_dynamic/16x16-disabled.png new file mode 100644 index 0000000..090d963 Binary files /dev/null and b/resources/ui/finger_number_dynamic/16x16-disabled.png differ diff --git a/resources/ui/finger_number_dynamic/16x16-normal.png b/resources/ui/finger_number_dynamic/16x16-normal.png new file mode 100644 index 0000000..090d963 Binary files /dev/null and b/resources/ui/finger_number_dynamic/16x16-normal.png differ diff --git a/resources/ui/finger_number_dynamic/32x32-disabled.png b/resources/ui/finger_number_dynamic/32x32-disabled.png new file mode 100644 index 0000000..75e8c09 Binary files /dev/null and b/resources/ui/finger_number_dynamic/32x32-disabled.png differ diff --git a/resources/ui/finger_number_dynamic/32x32-normal.png b/resources/ui/finger_number_dynamic/32x32-normal.png new file mode 100644 index 0000000..92e95c2 Binary files /dev/null and b/resources/ui/finger_number_dynamic/32x32-normal.png differ diff --git a/resources/ui/finger_number_fixed/16x16-disabled.png b/resources/ui/finger_number_fixed/16x16-disabled.png new file mode 100644 index 0000000..aba1c00 Binary files /dev/null and b/resources/ui/finger_number_fixed/16x16-disabled.png differ diff --git a/resources/ui/finger_number_fixed/16x16-normal.png b/resources/ui/finger_number_fixed/16x16-normal.png new file mode 100644 index 0000000..aba1c00 Binary files /dev/null and b/resources/ui/finger_number_fixed/16x16-normal.png differ diff --git a/resources/ui/finger_number_fixed/32x32-disabled.png b/resources/ui/finger_number_fixed/32x32-disabled.png new file mode 100644 index 0000000..5596025 Binary files /dev/null and b/resources/ui/finger_number_fixed/32x32-disabled.png differ diff --git a/resources/ui/finger_number_fixed/32x32-normal.png b/resources/ui/finger_number_fixed/32x32-normal.png new file mode 100644 index 0000000..5596025 Binary files /dev/null and b/resources/ui/finger_number_fixed/32x32-normal.png differ diff --git a/resources/ui/placement_fingers_outside/16x16-disabled.png b/resources/ui/placement_fingers_outside/16x16-disabled.png new file mode 100644 index 0000000..4e1685f Binary files /dev/null and b/resources/ui/placement_fingers_outside/16x16-disabled.png differ diff --git a/resources/ui/placement_fingers_outside/16x16-normal.png b/resources/ui/placement_fingers_outside/16x16-normal.png new file mode 100644 index 0000000..1d5e6e1 Binary files /dev/null and b/resources/ui/placement_fingers_outside/16x16-normal.png differ diff --git a/resources/ui/placement_fingers_outside/32x32-disabled.png b/resources/ui/placement_fingers_outside/32x32-disabled.png new file mode 100644 index 0000000..3bfdcb7 Binary files /dev/null and b/resources/ui/placement_fingers_outside/32x32-disabled.png differ diff --git a/resources/ui/placement_fingers_outside/32x32-normal.png b/resources/ui/placement_fingers_outside/32x32-normal.png new file mode 100644 index 0000000..6dd26f1 Binary files /dev/null and b/resources/ui/placement_fingers_outside/32x32-normal.png differ diff --git a/resources/ui/placement_notches_outside/16x16-disabled.png b/resources/ui/placement_notches_outside/16x16-disabled.png new file mode 100644 index 0000000..f5364d1 Binary files /dev/null and b/resources/ui/placement_notches_outside/16x16-disabled.png differ diff --git a/resources/ui/placement_notches_outside/16x16-normal.png b/resources/ui/placement_notches_outside/16x16-normal.png new file mode 100644 index 0000000..ce303c9 Binary files /dev/null and b/resources/ui/placement_notches_outside/16x16-normal.png differ diff --git a/resources/ui/placement_notches_outside/32x32-disabled.png b/resources/ui/placement_notches_outside/32x32-disabled.png new file mode 100644 index 0000000..b136035 Binary files /dev/null and b/resources/ui/placement_notches_outside/32x32-disabled.png differ diff --git a/resources/ui/placement_notches_outside/32x32-normal.png b/resources/ui/placement_notches_outside/32x32-normal.png new file mode 100644 index 0000000..debdd6b Binary files /dev/null and b/resources/ui/placement_notches_outside/32x32-normal.png differ diff --git a/resources/ui/placement_same_number_start_finger/16x16-disabled.png b/resources/ui/placement_same_number_start_finger/16x16-disabled.png new file mode 100644 index 0000000..1cfbe6d Binary files /dev/null and b/resources/ui/placement_same_number_start_finger/16x16-disabled.png differ diff --git a/resources/ui/placement_same_number_start_finger/16x16-normal.png b/resources/ui/placement_same_number_start_finger/16x16-normal.png new file mode 100644 index 0000000..6dfc04d Binary files /dev/null and b/resources/ui/placement_same_number_start_finger/16x16-normal.png differ diff --git a/resources/ui/placement_same_number_start_finger/32x32-disabled.png b/resources/ui/placement_same_number_start_finger/32x32-disabled.png new file mode 100644 index 0000000..1c6b275 Binary files /dev/null and b/resources/ui/placement_same_number_start_finger/32x32-disabled.png differ diff --git a/resources/ui/placement_same_number_start_finger/32x32-normal.png b/resources/ui/placement_same_number_start_finger/32x32-normal.png new file mode 100644 index 0000000..bf3eb55 Binary files /dev/null and b/resources/ui/placement_same_number_start_finger/32x32-normal.png differ diff --git a/resources/ui/placement_same_number_start_notch/16x16-disabled.png b/resources/ui/placement_same_number_start_notch/16x16-disabled.png new file mode 100644 index 0000000..f9a12c6 Binary files /dev/null and b/resources/ui/placement_same_number_start_notch/16x16-disabled.png differ diff --git a/resources/ui/placement_same_number_start_notch/16x16-normal.png b/resources/ui/placement_same_number_start_notch/16x16-normal.png new file mode 100644 index 0000000..70ae992 Binary files /dev/null and b/resources/ui/placement_same_number_start_notch/16x16-normal.png differ diff --git a/resources/ui/placement_same_number_start_notch/32x32-disabled.png b/resources/ui/placement_same_number_start_notch/32x32-disabled.png new file mode 100644 index 0000000..f8027eb Binary files /dev/null and b/resources/ui/placement_same_number_start_notch/32x32-disabled.png differ diff --git a/resources/ui/placement_same_number_start_notch/32x32-normal.png b/resources/ui/placement_same_number_start_notch/32x32-normal.png new file mode 100644 index 0000000..e891d15 Binary files /dev/null and b/resources/ui/placement_same_number_start_notch/32x32-normal.png differ diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..5890c42 --- /dev/null +++ b/ui.py @@ -0,0 +1,204 @@ +import traceback + +import adsk.core + +from .options import PlacementType, DynamicSizeType + +class FingerJointUI(object): + def __init__(self, inputs, defaults): + app = adsk.core.Application.get() + unitsManager = app.activeProduct.unitsManager + defaultUnit = unitsManager.defaultLengthUnits + + self._inputs = inputs + + self._inputBody0 = inputs.addSelectionInput( + 'inputBody0', 'First Body', + 'Select the body that should contain the first finger.') + self._inputBody0.tooltip ='Select the body that contains the fingers.' + self._inputBody0.addSelectionFilter('SolidBodies') + # TODO allow to select more than one body here and below and do the cuts for all combinations? + self._inputBody0.setSelectionLimits(1,1) + + self._inputBody1 = inputs.addSelectionInput( + 'inputBody1', 'Second Body', + 'Select the body that should contain the first notch.') + self._inputBody1.tooltip ='Select the body that contains the notches.' + self._inputBody1.addSelectionFilter('SolidBodies') + self._inputBody1.setSelectionLimits(1,1) + + self._inputDirection = inputs.addSelectionInput( + 'inputDirection', 'Direction of the Joint', + 'Select edge along the direction of the joint.') + self._inputDirection.tooltip = ('Select an edge or sketch line along the direction of the joint. Normally, this should be defined ' + 'by an edge where the two bodies intersect. The planes separating fingers from notches will be perpendicular to this direction.') + self._inputDirection.addSelectionFilter('LinearEdges') + self._inputDirection.addSelectionFilter('SketchLines') + self._inputDirection.setSelectionLimits(1,1) + + self._inputPlacementType = inputs.addButtonRowCommandInput('inputPlacementType', 'Finger Placement', False) + self._inputPlacementType.listItems.add('Fingers outside', defaults.placementType == PlacementType.FINGERS_OUTSIDE, 'resources/ui/placement_fingers_outside' ) + self._inputPlacementType.listItems.add('Notches outside', defaults.placementType == PlacementType.NOTCHES_OUTSIDE, 'resources/ui/placement_notches_outside' ) + self._inputPlacementType.listItems.add('Start with finger, end with notch', defaults.placementType == PlacementType.SAME_NUMBER_START_FINGER, 'resources/ui/placement_same_number_start_finger' ) + self._inputPlacementType.listItems.add('Start with notch, end with finger', defaults.placementType == PlacementType.SAME_NUMBER_START_NOTCH, 'resources/ui/placement_same_number_start_notch' ) + self._inputPlacementType.tooltipDescription = "Should the first selected body start/end with a finger or a notch?" + self._placementTypesByIndex = [ + PlacementType.FINGERS_OUTSIDE, + PlacementType.NOTCHES_OUTSIDE, + PlacementType.SAME_NUMBER_START_FINGER, + PlacementType.SAME_NUMBER_START_NOTCH + ] + + self._inputIsNumberOfFingersFixed = inputs.addDropDownCommandInput('inputIsNumberOfFingersFixed', 'Number', adsk.core.DropDownStyles.LabeledIconDropDownStyle) + self._inputIsNumberOfFingersFixed.tooltipDescription = "Should the number of fingers be a fixed number or determined from the size of fingers/notches and the size of the overlap?" + dropdownItems = self._inputIsNumberOfFingersFixed.listItems + dropdownItems.add('Fixed', defaults.isNumberOfFingersFixed, 'resources/ui/finger_number_fixed') + dropdownItems.add('Dynamic', not defaults.isNumberOfFingersFixed, 'resources/ui/finger_number_dynamic') + self._numberOfFingersFixedByIndex = [True, False] + + # Looks like a spinner has to have a maximum but 100000 seems reasonable. Creating that much fingers will likely run into memory or performance issues anyway. + self._inputFixedNumFingers = inputs.addIntegerSpinnerCommandInput('inputFixedNumFingers', 'Number of Fingers', 1 , 100000 , 1, defaults.fixedNumFingers) + + self._inputDynamicSizeType = inputs.addButtonRowCommandInput('inputDynamicSizeType', 'Size', False) + self._inputDynamicSizeType.listItems.add('Fixed Notch Size', defaults.dynamicSizeType == DynamicSizeType.FIXED_NOTCH_SIZE, 'resources/ui/dynamic_type_fixed_notch' ) + self._inputDynamicSizeType.listItems.add('Fixed Finger Size', defaults.dynamicSizeType == DynamicSizeType.FIXED_FINGER_SIZE, 'resources/ui/dynamic_type_fixed_finger' ) + self._inputDynamicSizeType.listItems.add('Equal Notch and Finger Size', defaults.dynamicSizeType == DynamicSizeType.EQUAL_NOTCH_AND_FINGER_SIZE, 'resources/ui/dynamic_type_equal_notch_and_finger' ) + self._inputDynamicSizeType.tooltipDescription = "Should notches or fingers have a fixed size or should their size be equally distributed over the length of the overlap?" + self._dynamicSizeTypesByIndex = [ + DynamicSizeType.FIXED_NOTCH_SIZE, + DynamicSizeType.FIXED_FINGER_SIZE, + DynamicSizeType.EQUAL_NOTCH_AND_FINGER_SIZE, + ] + + defaultNotchSize = adsk.core.ValueInput.createByReal(defaults.fixedNotchSize) + self._inputFixedNotchSize = inputs.addValueInput('inputFixedNotchSize', 'Notch Size', defaultUnit, defaultNotchSize) + + defaultFingerSize = adsk.core.ValueInput.createByReal(defaults.fixedFingerSize) + self._inputFixedFingerSize = inputs.addValueInput('inputFixedFingerSize', 'Finger Size', defaultUnit, defaultFingerSize) + + defaultMinNotchSize = adsk.core.ValueInput.createByReal(defaults.minNotchSize) + self._inputMinNotchSize = inputs.addValueInput('inputMinNotchSize', 'Minimal Notch Size', defaultUnit, defaultMinNotchSize) + + defaultMinFingerSize = adsk.core.ValueInput.createByReal(defaults.minFingerSize) + self._inputMinFingerSize = inputs.addValueInput('inputMinFingerSize', 'Minimal Finger Size', defaultUnit, defaultMinFingerSize) + + self._inputIsPreviewEnabled = inputs.addBoolValueInput('inputIsPreviewEnabled', 'Show Preview', True, '', defaults.isPreviewEnabled) + + self._inputErrorMessage = inputs.addTextBoxCommandInput('inputErrorMessage', '', '', 3, True) + self._inputErrorMessage.isFullWidth = True + + self.updateVisibility() + self.focusNextSelectionInput() + + def _getDistanceInputValue(self, input): + if input.isVisible and input.isValidExpression: + return input.value + + def updateVisibility(self): + dynamicSizeType = self.getDynamicSizeType() + numberOfFingersFixed = self.isNumberOfFingersFixed() + self._inputFixedNotchSize.isVisible = dynamicSizeType == DynamicSizeType.FIXED_NOTCH_SIZE + self._inputFixedFingerSize.isVisible = dynamicSizeType == DynamicSizeType.FIXED_FINGER_SIZE + self._inputFixedNumFingers.isVisible = numberOfFingersFixed + self._inputMinNotchSize.isVisible = not numberOfFingersFixed and dynamicSizeType == DynamicSizeType.FIXED_FINGER_SIZE + self._inputMinFingerSize.isVisible = not numberOfFingersFixed and dynamicSizeType != DynamicSizeType.FIXED_FINGER_SIZE + + def getBody0(self): + if self._inputBody0.selectionCount > 0: + return self._inputBody0.selection(0).entity + + def getBody1(self): + if self._inputBody1.selectionCount > 0: + return self._inputBody1.selection(0).entity + + def getDirection(self): + if self._inputDirection.selectionCount > 0: + return self._inputDirection.selection(0).entity + + def getPlacementType(self): + return self._placementTypesByIndex[self._inputPlacementType.selectedItem.index] + + def isNumberOfFingersFixed(self): + return self._numberOfFingersFixedByIndex[self._inputIsNumberOfFingersFixed.selectedItem.index] + + def getFixedNumFingers(self): + if self._inputFixedNumFingers.isVisible: + return self._inputFixedNumFingers.value + + def getDynamicSizeType(self): + return self._dynamicSizeTypesByIndex[self._inputDynamicSizeType.selectedItem.index] + + def getFixedNotchSize(self): + return self._getDistanceInputValue(self._inputFixedNotchSize) + + def getFixedFingerSize(self): + return self._getDistanceInputValue(self._inputFixedFingerSize) + + def getMinNotchSize(self): + return self._getDistanceInputValue(self._inputMinNotchSize) + + def getMinFingerSize(self): + return self._getDistanceInputValue(self._inputMinFingerSize) + + def isPreviewEnabled(self): + return self._inputIsPreviewEnabled.value + + def setInputErrorMessage(self, msg): + if msg: + formattedText = '

{}

'.format(msg) + else: + formattedText = '' + # We guard this statement to prevent an infinite loop of setting + # the value, validating the input because an input changed, computing + # the preview because the validation was successfull, and setting the + # value to '' there. + if self._inputErrorMessage.formattedText != formattedText: + self._inputErrorMessage.formattedText = formattedText + + def focusNextSelectionInput(self): + for input in self._inputs: + if isinstance(input, adsk.core.SelectionCommandInput) and input.selectionCount == 0: + input.hasFocus = True + break + + def setRelevantOptions(self, options): + options.dynamicSizeType = self.getDynamicSizeType() + options.placementType = self.getPlacementType() + options.isNumberOfFingersFixed = self.isNumberOfFingersFixed() + # Only update the options that are relevant for the current operation. + if self.getFixedNumFingers() is not None: + options.fixedNumFingers = self.getFixedNumFingers() + if self.getFixedNotchSize() is not None: + options.fixedNotchSize = self.getFixedNotchSize() + if self.getFixedFingerSize() is not None: + options.fixedFingerSize = self.getFixedFingerSize() + if self.getMinNotchSize() is not None: + options.minNotchSize = self.getMinNotchSize() + if self.getMinFingerSize() is not None: + options.minFingerSize = self.getMinFingerSize() + options.isPreviewEnabled = self.isPreviewEnabled() + + def areInputsValid(self): + valid = True + errorMessage = '' + if self.getPlacementType() == PlacementType.FINGERS_OUTSIDE and self.isNumberOfFingersFixed() and self.getFixedNumFingers() < 2: + valid = False + errorMessage = 'When using one finger on each edge, there have to be at least two fingers.' + if self._inputFixedNotchSize.isVisible and (self.getFixedNotchSize() is None or self.getFixedNotchSize() <= 0): + valid = False + if self._inputFixedFingerSize.isVisible and (self.getFixedFingerSize() is None or self.getFixedFingerSize() <= 0): + valid = False + if self._inputMinNotchSize.isVisible and (self.getMinNotchSize() is None or self.getMinNotchSize() <= 0): + valid = False + if self._inputMinFingerSize.isVisible and (self.getMinFingerSize() is None or self.getMinFingerSize() <= 0): + valid = False + self.setInputErrorMessage(errorMessage) + return valid + + +def reportError(message, includeStacktrace=False): + fusion = adsk.core.Application.get() + fusionUI = fusion.userInterface + if includeStacktrace: + message = '{}\n\nStack trace:\n{}'.format(message, traceback.format_exc()) + fusionUI.messageBox(message)