├── commands ├── counterboreBridgingDialog │ ├── __init__.py │ ├── resources │ │ ├── 16x16.png │ │ ├── 32x32.png │ │ └── 64x64.png │ ├── geometryUtil.py │ └── entry.py └── __init__.py ├── .gitignore ├── lib └── fusion360utils │ ├── __init__.py │ ├── general_utils.py │ └── event_utils.py ├── media ├── addin_input.png ├── addin_output.png └── slicer_output.png ├── .vscode └── launch.json ├── CounterboreBridging.manifest ├── CounterboreBridging.py ├── README_zh.md ├── config.py └── README.md /commands/counterboreBridgingDialog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode/settings.json 3 | .env 4 | -------------------------------------------------------------------------------- /lib/fusion360utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .general_utils import * 2 | from .event_utils import * 3 | -------------------------------------------------------------------------------- /media/addin_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finn2708/CounterboreBridging/HEAD/media/addin_input.png -------------------------------------------------------------------------------- /media/addin_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finn2708/CounterboreBridging/HEAD/media/addin_output.png -------------------------------------------------------------------------------- /media/slicer_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finn2708/CounterboreBridging/HEAD/media/slicer_output.png -------------------------------------------------------------------------------- /commands/counterboreBridgingDialog/resources/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finn2708/CounterboreBridging/HEAD/commands/counterboreBridgingDialog/resources/16x16.png -------------------------------------------------------------------------------- /commands/counterboreBridgingDialog/resources/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finn2708/CounterboreBridging/HEAD/commands/counterboreBridgingDialog/resources/32x32.png -------------------------------------------------------------------------------- /commands/counterboreBridgingDialog/resources/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finn2708/CounterboreBridging/HEAD/commands/counterboreBridgingDialog/resources/64x64.png -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /CounterboreBridging.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "autodeskProduct": "Fusion360", 3 | "type": "addin", 4 | "id": "c33cffa2-cd57-4350-9a68-f708c9284e5f", 5 | "author": "Finn Weithoff", 6 | "description": { 7 | "": "A Fusion 360 Add-in Command for optimizing counterbores for 3D printing", 8 | "2052": "一款用于优化3D打印沉头孔的Fusion 360插件", 9 | "1028": "一款用於優化3D列印沉頭孔的Fusion 360外掛程式" 10 | }, 11 | "version": "0.1", 12 | "runOnStartup": true, 13 | "supportedOS": "windows|mac", 14 | "editEnabled": true 15 | } -------------------------------------------------------------------------------- /CounterboreBridging.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 fusion360utils 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') -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 沉孔搭桥 2 | 3 | 这是一个适用于 Autodesk Fusion360 的插件,用于为 FDM 3D 打印优化沉头孔结构。 4 | 5 | 6 | 7 | ## 安装方法 8 | 9 | 1. 克隆本仓库到 `%appdata%\Autodesk\Autodesk Fusion 360\API\AddIns` 目录下(例如:`C:\Users\Finn\AppData\Roaming\Autodesk\Autodesk Fusion 360\API\AddIns`,Mac 用户为 `~/Library/Application\ Support/Autodesk/Autodesk\ Fusion\ 360/API`)。 10 | 请确保文件夹名称为 `CounterboreBridging`(如果你是以 zip 文件方式下载的仓库)。 11 | 12 | 2. 重启 Fusion。 13 | 3. 插件将在 `设计工作区 - 实体选项卡 - 修改分组` 中显示,在 `平移/复制(Move/Copy)` 命令旁边。 14 | 15 | 如果未自动激活,你可以手动在 `设计工作区 - 工具选项卡 - 插件分组 - 脚本与插件(Scripts and Addins)` 中启动插件。 16 | 17 | ## 使用方法 18 | 先创建一个带有圆柱形沉头孔的零件(例如使用 `孔(Hole)` 命令或普通拉伸)。然后: 19 | 1. 启用该命令 20 | 2. 选择一个或多个沉头孔底面 21 | 3. 选择一个主要桥接应平行于的边 22 | 23 | ![](media/addin_input.png) 24 | -------------------------------------------------------------------------------- /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 = 'ACME' 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 .counterboreBridgingDialog import entry as counterboreBridgingDialog 7 | 8 | # TODO add your imported modules to this list. 9 | # Fusion will automatically call the start() and stop() functions. 10 | commands = [ 11 | counterboreBridgingDialog, 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() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Counterbore Bridging 2 | 3 | This is an addin for Autodesk Fusion360 that provides easy optimization of counterbores for FDM 3d printing. 4 | 5 | 6 | 7 | [中文文档](README_zh.md) 8 | 9 | ## Installation 10 | 11 | 1. Clone this repository to `%appdata%\Autodesk\Autodesk Fusion 360\API\AddIns` (for me, the specific path is `C:\Users\Finn\AppData\Roaming\Autodesk\Autodesk Fusion 360\API\AddIns` and Mac: `~/Library/Application\ Support/Autodesk/Autodesk\ Fusion\ 360/API`). 12 | Make sure, the folder is named `CounterboreBridging` (i.e. if you downloaded the repo as .zip). 13 | 14 | 2. Restart Fusion. 15 | 3. The addin will show up in `Design Workspace - Solid Tab - Modify Group` next to the `Move/Copy` command. 16 | 17 | You may need to activate it manually in `Design Workspace - Utilities Tab - Addins Group - Scripts and Addins`. 18 | 19 | ## Usage 20 | Create a part with a cylindrical counterbore (e.g. using the `Hole` command or generic extrusions). Then: 21 | 1. Activate the command 22 | 2. Select one or more counterbore faces 23 | 3. Select an edge the primary bridges should be parallel to 24 | 25 | ![](media/addin_input.png) 26 | -------------------------------------------------------------------------------- /lib/fusion360utils/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 | -------------------------------------------------------------------------------- /lib/fusion360utils/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 | -------------------------------------------------------------------------------- /commands/counterboreBridgingDialog/geometryUtil.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import adsk.core 4 | import adsk.fusion 5 | 6 | 7 | def rotateVector180(vect): 8 | """ 9 | quickly rotate a vector by 180 degrees 10 | """ 11 | vect.scaleBy(-1) 12 | 13 | 14 | def rotateVector(vect: adsk.core.Vector3D, axis, angle_degrees): 15 | """ 16 | rotates a vector by a set degree along the passed axis. 17 | 18 | axis: [z|x|y] 19 | 20 | """ 21 | angle_radians = math.radians(angle_degrees) 22 | 23 | # Create a rotation matrix 24 | rotation_matrix = adsk.core.Matrix3D.create() 25 | rotation_matrix.setToIdentity() 26 | 27 | origin = adsk.core.Point3D.create(0, 0, 0) 28 | 29 | # Set rotation based on axis 30 | if axis == "z": 31 | rotation_matrix.setToRotation( 32 | angle_radians, adsk.core.Vector3D.create(0, 0, 1), origin 33 | ) 34 | elif axis == "y": 35 | rotation_matrix.setToRotation( 36 | angle_radians, adsk.core.Vector3D.create(0, 1, 0), origin 37 | ) 38 | elif axis == "x": 39 | rotation_matrix.setToRotation( 40 | angle_radians, adsk.core.Vector3D.create(1, 0, 0), origin 41 | ) 42 | else: 43 | raise ValueError("The axis must be 'x', 'y', or 'z'.") 44 | 45 | # Apply rotation 46 | vect.transformBy(rotation_matrix) 47 | 48 | 49 | def createVectorFrom2Points(p1: adsk.core.Point3D, p2: adsk.core.Point3D): 50 | return adsk.core.Vector3D.create( 51 | p1.x - p2.x, 52 | p1.y - p2.y, 53 | p1.z - p2.z, 54 | ) 55 | 56 | 57 | def movePointTo(currentPoint: adsk.fusion.SketchPoint, newPoint: adsk.core.Point3D): 58 | """ 59 | allows you to move a SketchPoint (point of a line, curve) to a specified point 60 | """ 61 | translationVector = adsk.core.Vector3D.create( 62 | newPoint.x - currentPoint.geometry.x, 63 | newPoint.y - currentPoint.geometry.y, 64 | newPoint.z - currentPoint.geometry.z, 65 | ) 66 | currentPoint.move(translationVector) 67 | 68 | 69 | """def extendLineTo(line : adsk.fusion.SketchLine,newPosition,start=True): 70 | 71 | if( start): 72 | movePointTo(line.startSketchPoint,newPosition) 73 | else: 74 | movePointTo(line.endSketchPoint,newPosition) 75 | 76 | 77 | def extendLineDim(line : adsk.fusion.SketchLine,dimension,start=True): 78 | 79 | lineGeom :adsk.core.Line3D= line.geometry 80 | 81 | 82 | extensionVector = createVectorFrom2Points(lineGeom.endPoint,lineGeom.startPoint) 83 | 84 | extensionVector.normalize() 85 | extensionVector.scaleBy(dimension) 86 | if start: 87 | extensionVector.scaleBy(-1) #se allungo l'inizio devo invertire la direzione del vettore 88 | 89 | 90 | # Crea un nuovo punto finale per la linea estesa 91 | if( start): 92 | newEndPoint:adsk.core.Point3D = lineGeom.startPoint.copy() 93 | newEndPoint.translateBy(extensionVector) 94 | movePointTo(line.startSketchPoint,newEndPoint) 95 | else: 96 | newEndPoint:adsk.core.Point3D = lineGeom.endPoint.copy() 97 | newEndPoint.translateBy(extensionVector) 98 | movePointTo(line.endSketchPoint,newEndPoint) 99 | def extendToAnotherCurve(line : adsk.fusion.SketchLine,curveArray,start=True): 100 | #estendo lo start 101 | extendLineDim(line,100,start) 102 | intersections =[] 103 | #scorro tutte le linee e trovo le intersezioni 104 | for l in curveArray: 105 | intersectionsPoint = line.geometry.intersectWithCurve(l.geometry) 106 | for point in intersectionsPoint: 107 | intersections.append(point) 108 | 109 | if len(intersections)== 0: 110 | return False 111 | 112 | sketchPoint = line.startSketchPoint if start==True else line.endSketchPoint 113 | 114 | intersections.sort(key=lambda point: sketchPoint.geometry.distanceTo(point),reverse=True) 115 | movePointTo(sketchPoint,intersections[0]) 116 | return True 117 | """ 118 | 119 | 120 | def midpoint(line: adsk.fusion.SketchLine): 121 | start_point = line.startSketchPoint.geometry 122 | end_point = line.endSketchPoint.geometry 123 | 124 | mid_x = (start_point.x + end_point.x) / 2 125 | mid_y = (start_point.y + end_point.y) / 2 126 | mid_z = (start_point.z + end_point.z) / 2 127 | 128 | return adsk.core.Point3D.create(mid_x, mid_y, mid_z) 129 | 130 | 131 | def getExtendedIntersectionPoints(line: adsk.fusion.SketchLine, curveArray): 132 | """ 133 | given a SketchLine, it is extended and the intersection points with "curves" present in the "curveArray" are returned 134 | 135 | """ 136 | startIntersection = line.startSketchPoint.geometry 137 | endIntersection = line.endSketchPoint.geometry 138 | 139 | # extend the line to infinity 140 | copiedLine = line.geometry.copy() 141 | infLine = copiedLine.asInfiniteLine() 142 | 143 | intersections = [] 144 | intersectionsWithLine = [] 145 | # scroll through all the lines and find the intersections 146 | for l in curveArray: # adsk.fusion.SketchLine 147 | intersectionsPoint = infLine.intersectWithCurve(l.geometry) 148 | for point in intersectionsPoint: 149 | intersections.append(point) 150 | intersectionsWithLine.append({"point": point, "line": l}) 151 | 152 | middle = midpoint(line) 153 | 154 | # I find the direction in degrees of the direction of the line 155 | startDirection = round(getAngleFromTwoPoints(startIntersection, endIntersection), 0) 156 | 157 | # endDirection should be startDirection-180... 158 | endDirection = (startDirection + 180) % 360 159 | # endDirection= round(getAngleFromTwoPoints(endIntersection,startIntersection),0) 160 | 161 | # I find the intersections that touch the "start" / "end" side of the line 162 | intersectionsWithLineStart = [ 163 | i 164 | for i in intersectionsWithLine 165 | if round(getAngleFromTwoPoints(i["point"], middle), 0) % 360 == startDirection 166 | ] 167 | intersectionsWithLineEnd = [ 168 | i 169 | for i in intersectionsWithLine 170 | if round(getAngleFromTwoPoints(i["point"], middle), 0) % 360 == endDirection 171 | ] 172 | 173 | # I find the nearest intersections 174 | if len(intersectionsWithLineStart) > 0: 175 | intersectionsWithLineStart.sort(key=lambda i: middle.distanceTo(i["point"])) 176 | startIntersection = intersectionsWithLineStart[0]["point"] 177 | startInteractionWith = intersectionsWithLineStart[0]["line"] 178 | 179 | if len(intersectionsWithLineEnd) > 0: 180 | intersectionsWithLineEnd.sort(key=lambda i: middle.distanceTo(i["point"])) 181 | endIntersection = intersectionsWithLineEnd[0]["point"] 182 | endInteractionWith = intersectionsWithLineEnd[0]["line"] 183 | 184 | # return the intersections and with which lines 185 | return startIntersection, endIntersection, startInteractionWith, endInteractionWith 186 | 187 | 188 | def getAngleFromTwoPoints(point1: adsk.core.Point3D, point2: adsk.core.Point3D): 189 | dx = point2.x - point1.x 190 | dy = point2.y - point1.y 191 | angolo_radianti = math.atan2(dy, dx) 192 | deg = (math.degrees(angolo_radianti) + 360) % 360 193 | return deg 194 | 195 | 196 | def moveLine(line: adsk.fusion.SketchLine, vector: adsk.core.Vector3D): 197 | """ 198 | BEWARE OF THE CONSTRAINTS! if the line is parallel you will end up moving it twice as much (moving the first point also moves the second) 199 | """ 200 | line.endSketchPoint.move(vector) 201 | line.startSketchPoint.move(vector) 202 | 203 | 204 | def get_curves_from_sketch(sketch: adsk.fusion.Sketch): 205 | """ 206 | Gets all curves and lines from a sketch. 207 | 208 | Args: 209 | sketch (adsk.fusion.Sketch): The sketch from which to obtain the curves. 210 | 211 | Returns: 212 | list: A list of curves and lines from the sketch. 213 | """ 214 | curves = [] 215 | 216 | # Get all curves in the sketch 217 | sketchCurves = sketch.sketchCurves 218 | 219 | for line in sketchCurves.sketchLines: 220 | curves.append(line) 221 | 222 | for circle in sketchCurves.sketchCircles: 223 | curves.append(circle) 224 | 225 | for ellipse in sketchCurves.sketchEllipses: 226 | curves.append(ellipse) 227 | 228 | for entity in sketchCurves.sketchArcs: 229 | curves.append(entity) 230 | 231 | for entity in sketchCurves.sketchConicCurves: 232 | curves.append(entity) 233 | 234 | for entity in sketchCurves.sketchEllipticalArcs: 235 | curves.append(entity) 236 | for entity in sketchCurves.sketchFittedSplines: 237 | curves.append(entity) 238 | for entity in sketchCurves.sketchFixedSplines: 239 | curves.append(entity) 240 | for entity in sketchCurves.sketchControlPointSplines: 241 | curves.append(entity) 242 | 243 | return curves 244 | 245 | 246 | """def isPointInside(profile :adsk.fusion.Profile,point:adsk.core.Point3D): 247 | 248 | bounding_box=profile.boundingBox 249 | if (bounding_box.minPoint.x <= point.x <= bounding_box.maxPoint.x and 250 | bounding_box.minPoint.y <= point.y <= bounding_box.maxPoint.y): 251 | return True 252 | return False 253 | """ 254 | 255 | 256 | def profileHasLine(profile: adsk.fusion.Profile, line: adsk.core.Line3D): 257 | """ 258 | identifies whether a given line is inside the passed profile; the vertices are checked to be equal with a small tolerance 259 | """ 260 | for profileLine in profile.profileLoops: 261 | for c in profileLine.profileCurves: 262 | if isinstance(c.geometry, adsk.core.Line3D): 263 | g: adsk.core.Line3D = c.geometry 264 | 265 | if g.startPoint.isEqualToByTolerance(line.startPoint, 0.000000001): 266 | if g.endPoint.isEqualToByTolerance(line.endPoint, 0.000000001): 267 | return True 268 | 269 | elif g.startPoint.isEqualToByTolerance(line.endPoint, 0.000000001): 270 | if g.endPoint.isEqualToByTolerance(line.startPoint, 0.000000001): 271 | return True 272 | 273 | return False 274 | -------------------------------------------------------------------------------- /commands/counterboreBridgingDialog/entry.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | 4 | import adsk.core 5 | import adsk.fusion 6 | 7 | from ... import config 8 | from ...lib import fusion360utils as futil 9 | 10 | from .geometryUtil import ( 11 | rotateVector180, 12 | rotateVector, 13 | movePointTo, 14 | profileHasLine, 15 | getExtendedIntersectionPoints, 16 | getAngleFromTwoPoints, 17 | get_curves_from_sketch, 18 | createVectorFrom2Points, 19 | ) 20 | 21 | 22 | app = adsk.core.Application.get() 23 | ui = app.userInterface 24 | userLanguage = app.preferences.generalPreferences.userLanguage 25 | 26 | # i18n 27 | # ChinesePRCLanguage = 0 28 | # ChineseTaiwanLanguage = 1 29 | # CzechLanguage = 2 30 | # EnglishLanguage = 3 31 | # FrenchLanguage = 4 32 | # GermanLanguage = 5 33 | # HungarianLanguage = 6 34 | # ItalianLanguage = 7 35 | # JapaneseLanguage = 8 36 | # KoreanLanguage = 9 37 | # PolishLanguage = 10 38 | # PortugueseBrazilianLanguage = 11 39 | # RussianLanguage = 12 40 | # SpanishLanguage = 13 41 | # TurkishLanguage = 14 42 | i18n = { 43 | 0: { 44 | "A Fusion 360 Add-in Command for optimizing counterbores for 3D printing": "一款用于优化3D打印沉头孔的Fusion 360插件", 45 | "Counterbore Bridging": "沉孔搭桥", 46 | "Counterbore Face": "沉头孔面", 47 | "Select the counterbore bottom face.": "请选择沉头孔底面。", 48 | "Angle degree": "角度", 49 | "Layer height": "层高", 50 | "Number of cut": "切割次数", 51 | "Invalid shape, cant compute the inner profile": "无效的形状,无法计算内部轮廓", 52 | "Cannot find inner circle": "找不到内圆", 53 | }, 54 | 1: { 55 | "A Fusion 360 Add-in Command for optimizing counterbores for 3D printing": "一款用於優化3D列印沉頭孔的Fusion 360外掛程式", 56 | "Counterbore Bridging": "沉頭孔搭橋", 57 | "Counterbore Face": "沉頭孔面", 58 | "Select the counterbore bottom face.": "請選擇沉頭孔底面。", 59 | "Angle degree": "角度", 60 | "Layer height": "層高", 61 | "Number of cut": "切割次數", 62 | "Invalid shape, cant compute the inner profile": "無效的形狀,無法計算內部輪廓", 63 | "Cannot find inner circle": "找不到內圓", 64 | }, 65 | 3: { 66 | "A Fusion 360 Add-in Command for optimizing counterbores for 3D printing": "A Fusion 360 Add-in Command for optimizing counterbores for 3D printing", 67 | "Counterbore Bridging": "Counterbore Bridging", 68 | "Counterbore Face": "Counterbore Face", 69 | "Select the counterbore bottom face.": "Select the counterbore bottom face.", 70 | "Angle degree": "Angle degree", 71 | "Layer height": "Layer height", 72 | "Number of cut": "Number of cut", 73 | "Invalid shape, cant compute the inner profile": "Invalid shape, cant compute the inner profile", 74 | "Cannot find inner circle": "Cannot find inner circle", 75 | }, 76 | } 77 | 78 | def _(text, lang=3): 79 | """ 80 | i18n localization function: returns the localized string for `text` in `lang`. 81 | Defaults to English if translation not found. 82 | """ 83 | return i18n.get(lang, {}).get(text, i18n[3].get(text, text)) 84 | 85 | 86 | # TODO *** Specify the command identity information. *** 87 | CMD_ID = f"{config.COMPANY_NAME}_{config.ADDIN_NAME}_cmdDialog" 88 | CMD_NAME = _("Counterbore Bridging", userLanguage) 89 | CMD_Description = ( 90 | _("A Fusion 360 Add-in Command for optimizing counterbores for 3D printing", userLanguage) 91 | ) 92 | 93 | # Specify that the command will be promoted to the panel. 94 | IS_PROMOTED = True 95 | 96 | # TODO *** Define the location where the command button will be created. *** 97 | # This is done by specifying the workspace, the tab, and the panel, and the 98 | # command it will be inserted beside. Not providing the command to position it 99 | # will insert it at the end. 100 | WORKSPACE_ID = "FusionSolidEnvironment" 101 | PANEL_ID = "SolidModifyPanel" 102 | COMMAND_BESIDE_ID = "FusionMoveCommand" 103 | 104 | # Resource location for command icons, here we assume a sub folder in this directory named "resources". 105 | ICON_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "") 106 | 107 | # Local list of event handlers used to maintain a reference so 108 | # they are not released and garbage collected. 109 | local_handlers = [] 110 | 111 | 112 | # Executed when add-in is run. 113 | def start(): 114 | # Create a command Definition. 115 | cmd_def = ui.commandDefinitions.addButtonDefinition( 116 | CMD_ID, CMD_NAME, CMD_Description, ICON_FOLDER 117 | ) 118 | 119 | # Define an event handler for the command created event. It will be called when the button is clicked. 120 | futil.add_handler(cmd_def.commandCreated, command_created) 121 | 122 | # ******** Add a button into the UI so the user can run the command. ******** 123 | # Get the target workspace the button will be created in. 124 | workspace = ui.workspaces.itemById(WORKSPACE_ID) 125 | 126 | # Get the panel the button will be created in. 127 | panel = workspace.toolbarPanels.itemById(PANEL_ID) 128 | 129 | # Create the button command control in the UI after the specified existing command. 130 | control = panel.controls.addCommand(cmd_def, COMMAND_BESIDE_ID, False) 131 | 132 | # Specify if the command is promoted to the main toolbar. 133 | control.isPromoted = IS_PROMOTED 134 | 135 | 136 | # Executed when add-in is stopped. 137 | def stop(): 138 | # Get the various UI elements for this command 139 | workspace = ui.workspaces.itemById(WORKSPACE_ID) 140 | panel = workspace.toolbarPanels.itemById(PANEL_ID) 141 | command_control = panel.controls.itemById(CMD_ID) 142 | command_definition = ui.commandDefinitions.itemById(CMD_ID) 143 | 144 | # Delete the button command control 145 | if command_control: 146 | command_control.deleteMe() 147 | 148 | # Delete the command definition 149 | if command_definition: 150 | command_definition.deleteMe() 151 | 152 | 153 | def cutOneFace( 154 | face, 155 | layer_height_input: adsk.core.ValueCommandInput, 156 | angleStep=0, 157 | gap=0.0001, 158 | oldGuideLine=None, 159 | ): 160 | """ 161 | performs a cut with the specified parameters 162 | a "gap" is left between the diameter and the line to make it easier to cut the patterns 163 | """ 164 | app = adsk.core.Application.get() 165 | design = adsk.fusion.Design.cast(app.activeProduct) 166 | 167 | # Create sketch on face 168 | sks = design.activeComponent.sketches 169 | sk: adsk.fusion.Sketch = sks.add(face) 170 | 171 | if oldGuideLine: 172 | # copy it into the current sketch 173 | # take the size of the angle 174 | 175 | proj = sk.project(oldGuideLine) 176 | oldGuideLine_proj: adsk.fusion.SketchLine = proj.item(0) 177 | oldGuideLine_proj.isConstruction = True 178 | 179 | # old fixed method 180 | # leg_x = oldGuideLine_proj.endSketchPoint.geometry.x - oldGuideLine_proj.startSketchPoint.geometry.x 181 | # leg_y = oldGuideLine_proj.endSketchPoint.geometry.y - oldGuideLine_proj.startSketchPoint.geometry.y 182 | # angle = math.atan2(leg_y, leg_x) * 180 /math.pi 183 | 184 | angleOldGuideLine = getAngleFromTwoPoints( 185 | oldGuideLine_proj.startSketchPoint.geometry, 186 | oldGuideLine_proj.endSketchPoint.geometry, 187 | ) 188 | newAngle = angleOldGuideLine + angleStep 189 | else: 190 | newAngle = angleStep 191 | 192 | # Get inner circle 193 | cs = sk.sketchCurves.sketchCircles 194 | circles = [cs.item(i) for i in range(cs.count)] 195 | if len(circles) == 0: 196 | # it could be an arc... 197 | cs = sk.sketchCurves.sketchArcs 198 | circles = [cs.item(i) for i in range(cs.count)] 199 | if len(circles) == 0: 200 | ui.messageBox(_("Cannot find inner circle", userLanguage)) 201 | return 202 | 203 | circles.sort(key=lambda c: c.radius, reverse=False) 204 | inner = circles[0] 205 | inner.isConstruction = True 206 | 207 | xi = inner.centerSketchPoint.geometry.x 208 | yi = inner.centerSketchPoint.geometry.y 209 | zi = inner.centerSketchPoint.geometry.z 210 | 211 | # I take all the existing lines in the sketch (before adding any more) and remove the inner circle 212 | lines = get_curves_from_sketch(sk) 213 | lines.remove(inner) 214 | 215 | # Calculate the coordinates of the end point ( create a very short line since it will only serve as a reference for orientation ) 216 | angle_radians = math.radians(newAngle) 217 | x_end = math.cos(angle_radians) * 0.1 218 | y_end = math.sin(angle_radians) * 0.1 219 | start_point = adsk.core.Point3D.create(xi, yi, zi) 220 | end_point = adsk.core.Point3D.create( 221 | start_point.x + x_end, start_point.y + y_end, start_point.z 222 | ) 223 | angleGuideLine = sk.sketchCurves.sketchLines.addByTwoPoints(start_point, end_point) 224 | angleGuideLine.isConstruction = True 225 | sk.geometricConstraints.addCoincident( 226 | angleGuideLine.startSketchPoint, inner.centerSketchPoint 227 | ) 228 | if oldGuideLine: 229 | ad = sk.sketchDimensions.addAngularDimension( 230 | oldGuideLine_proj, angleGuideLine, oldGuideLine_proj.geometry.startPoint 231 | ) 232 | ad.parameter.value = math.radians(angleStep) 233 | else: 234 | angleGuideLine.isFixed = True 235 | 236 | # create the first line with the related constraints 237 | # (I use a vect to move the vertex points of the lines by certain dimensions) 238 | line1 = sk.sketchCurves.sketchLines.addByTwoPoints( 239 | angleGuideLine.startSketchPoint.geometry, angleGuideLine.endSketchPoint.geometry 240 | ) 241 | sk.geometricConstraints.addParallel(line1, angleGuideLine) 242 | vect = createVectorFrom2Points( 243 | angleGuideLine.startSketchPoint.geometry, angleGuideLine.endSketchPoint.geometry 244 | ) 245 | rotateVector(vect, "z", 90) 246 | vect.normalize() 247 | vect.scaleBy(inner.radius + gap) 248 | line1.endSketchPoint.move(vect) 249 | 250 | # create the second line with the related constraints 251 | line2 = sk.sketchCurves.sketchLines.addByTwoPoints( 252 | angleGuideLine.startSketchPoint.geometry, angleGuideLine.endSketchPoint.geometry 253 | ) 254 | sk.geometricConstraints.addParallel(line2, angleGuideLine) 255 | rotateVector180(vect) 256 | line2.endSketchPoint.move(vect) 257 | 258 | # create construction lines to set the distance between the line and the internal diameter 259 | # for line1 260 | distanceLine = sk.sketchCurves.sketchLines.addByTwoPoints( 261 | inner.centerSketchPoint.geometry, line1.endSketchPoint.geometry 262 | ) 263 | distanceLine.isConstruction = True 264 | sk.geometricConstraints.addPerpendicular(distanceLine, line1) 265 | sk.geometricConstraints.addCoincident( 266 | inner.centerSketchPoint, distanceLine.startSketchPoint 267 | ) 268 | sk.geometricConstraints.addCoincident(distanceLine.endSketchPoint, line1) 269 | dim = sk.sketchDimensions.addDistanceDimension( 270 | distanceLine.startSketchPoint, 271 | distanceLine.endSketchPoint, 272 | adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 273 | distanceLine.startSketchPoint.geometry, 274 | ) 275 | dim.parameter.value = inner.radius + gap 276 | 277 | # for line2 278 | distanceLine = sk.sketchCurves.sketchLines.addByTwoPoints( 279 | inner.centerSketchPoint.geometry, line2.endSketchPoint.geometry 280 | ) 281 | distanceLine.isConstruction = True 282 | sk.geometricConstraints.addPerpendicular(distanceLine, line2) 283 | sk.geometricConstraints.addCoincident( 284 | inner.centerSketchPoint, distanceLine.startSketchPoint 285 | ) 286 | sk.geometricConstraints.addCoincident(distanceLine.endSketchPoint, line2) 287 | dim = sk.sketchDimensions.addDistanceDimension( 288 | distanceLine.startSketchPoint, 289 | distanceLine.endSketchPoint, 290 | adsk.fusion.DimensionOrientations.AlignedDimensionOrientation, 291 | distanceLine.startSketchPoint.geometry, 292 | ) 293 | dim.parameter.value = inner.radius + gap 294 | 295 | # retrieve the intersections of the 2 lines with the existing profile 296 | startPoint1, endPoint1, interLineStart1, interLineEnd1 = ( 297 | getExtendedIntersectionPoints(line1, lines) 298 | ) 299 | startPoint2, endPoint2, interLineStart2, interLineEnd2 = ( 300 | getExtendedIntersectionPoints(line2, lines) 301 | ) 302 | 303 | # move the lines as close to the intersection points as possible 304 | movePointTo(line1.startSketchPoint, startPoint1) 305 | movePointTo(line1.endSketchPoint, endPoint1) 306 | 307 | movePointTo(line2.startSketchPoint, startPoint2) 308 | movePointTo(line2.endSketchPoint, endPoint2) 309 | 310 | # add the coincidence constraint (if it is not placed, it can cause problems on the splines) 311 | sk.geometricConstraints.addCoincident(line1.startSketchPoint, interLineStart1) 312 | sk.geometricConstraints.addCoincident(line1.endSketchPoint, interLineEnd1) 313 | 314 | sk.geometricConstraints.addCoincident(line2.startSketchPoint, interLineStart2) 315 | sk.geometricConstraints.addCoincident(line2.endSketchPoint, interLineEnd2) 316 | 317 | # Take the center profile 318 | 319 | # search all profiles for those that contain both line1 and line2 320 | # if a profile contains both it means it is the central one 321 | profiles = [sk.profiles.item(i) for i in range(sk.profiles.count)] 322 | 323 | candidateProfiles = [] 324 | for p in profiles: 325 | if profileHasLine(p, line1.geometry) and profileHasLine(p, line2.geometry): 326 | candidateProfiles.append(p) 327 | 328 | # if found more "valid" profiles... exceptional case... 329 | if len(candidateProfiles) != 1: 330 | ui.messageBox(_("Invalid shape, cant compute the inner profile", userLanguage)) 331 | return 332 | 333 | centerProfile = adsk.core.ObjectCollection.create() 334 | centerProfile.add(candidateProfiles[0]) 335 | 336 | # make the cut 337 | one_lh = adsk.core.ValueInput.createByReal(-layer_height_input.value) 338 | 339 | extrudes = design.activeComponent.features.extrudeFeatures 340 | ex1 = extrudes.addSimple( 341 | centerProfile, one_lh, adsk.fusion.FeatureOperations.CutFeatureOperation 342 | ) 343 | 344 | # If a user parameter is used as input, link extrude extent to that parameter 345 | if design.userParameters.itemByName(layer_height_input.expression) is not None: 346 | ex1_def = adsk.fusion.DistanceExtentDefinition.cast(ex1.extentOne) 347 | ex1_def.distance.expression = f"-{layer_height_input.expression}" 348 | 349 | # return the new face (for the next cut) and the guide line for orientation 350 | return ex1.endFaces[0], angleGuideLine 351 | 352 | 353 | # Function that is called when a user clicks the corresponding button in the UI. 354 | # This defines the contents of the command dialog and connects to the command related events. 355 | def command_created(args: adsk.core.CommandCreatedEventArgs): 356 | # General logging for debug. 357 | futil.log(f"{CMD_NAME} Command Created Event") 358 | 359 | # https://help.autodesk.com/view/fusion360/ENU/?contextId=CommandInputs 360 | inputs = args.command.commandInputs 361 | 362 | f_in = inputs.addSelectionInput( 363 | "face_input", 364 | _("Counterbore Face", userLanguage), 365 | _("Select the counterbore bottom face.", userLanguage), 366 | ) 367 | f_in.addSelectionFilter(adsk.core.SelectionCommandInput.SolidFaces) 368 | f_in.setSelectionLimits(1) 369 | 370 | inputs.addIntegerSpinnerCommandInput( 371 | "angle_degree_input", _("Angle degree", userLanguage), 0, 359, 1, 0 372 | ) 373 | inputs.addValueInput( 374 | "layer_height_input", 375 | _("Layer height", userLanguage), 376 | app.activeProduct.unitsManager.defaultLengthUnits, 377 | adsk.core.ValueInput.createByString("0.2 mm"), 378 | ) 379 | inputs.addIntegerSpinnerCommandInput( 380 | "number_of_cut", _("Number of cut", userLanguage), 1, 5, 1, 2 381 | ) 382 | 383 | futil.add_handler( 384 | args.command.execute, command_execute, local_handlers=local_handlers 385 | ) 386 | futil.add_handler( 387 | args.command.inputChanged, command_input_changed, local_handlers=local_handlers 388 | ) 389 | futil.add_handler( 390 | args.command.executePreview, command_preview, local_handlers=local_handlers 391 | ) 392 | futil.add_handler( 393 | args.command.validateInputs, 394 | command_validate_input, 395 | local_handlers=local_handlers, 396 | ) 397 | futil.add_handler( 398 | args.command.destroy, command_destroy, local_handlers=local_handlers 399 | ) 400 | 401 | 402 | # This event handler is called when the user clicks the OK button in the command dialog or 403 | # is immediately called after the created event not command inputs were created for the dialog. 404 | def command_execute(args: adsk.core.CommandEventArgs): 405 | # General logging for debug. 406 | futil.log(f"{CMD_NAME} Command Execute Event") 407 | 408 | # Get a reference to your command's inputs. 409 | inputs = args.command.commandInputs 410 | face_input: adsk.core.SelectionCommandInput = inputs.itemById("face_input") 411 | angle_degree_input = inputs.itemById("angle_degree_input") 412 | layer_height_input: adsk.core.ValueCommandInput = inputs.itemById( 413 | "layer_height_input" 414 | ) 415 | number_of_cut_input = inputs.itemById("number_of_cut") 416 | 417 | # app = adsk.core.Application.get() 418 | # design = adsk.fusion.Design.cast(app.activeProduct) 419 | 420 | # Read inputs 421 | faces = [face_input.selection(i) for i in range(face_input.selectionCount)] 422 | # layer_heigth = layer_height_input.value 423 | 424 | angleStep = 180.0 / number_of_cut_input.value 425 | 426 | for face in faces: 427 | currentFace = face.entity 428 | currentAngle = ( 429 | angle_degree_input.value 430 | ) # la prima volta vale come l'angolo impostato, poi step 431 | oldGuideLine = None 432 | for i in range(number_of_cut_input.value): 433 | currentFace, oldGuideLine = cutOneFace( 434 | currentFace, layer_height_input, currentAngle, oldGuideLine=oldGuideLine 435 | ) 436 | currentAngle = angleStep 437 | # app.activeViewport.refresh() 438 | 439 | 440 | # This event handler is called when the command needs to compute a new preview in the graphics window. 441 | def command_preview(args: adsk.core.CommandEventArgs): 442 | # General logging for debug. 443 | futil.log(f"{CMD_NAME} Command Preview Event") 444 | 445 | command_execute(args) 446 | 447 | # inputs = args.command.commandInputs 448 | 449 | 450 | # This event handler is called when the user changes anything in the command dialog 451 | # allowing you to modify values of other inputs based on that change. 452 | def command_input_changed(args: adsk.core.InputChangedEventArgs): 453 | changed_input = args.input 454 | # inputs = args.inputs 455 | 456 | # General logging for debug. 457 | futil.log( 458 | f"{CMD_NAME} Input Changed Event fired from a change to {changed_input.id}" 459 | ) 460 | 461 | 462 | # This event handler is called when the user interacts with any of the inputs in the dialog 463 | # which allows you to verify that all of the inputs are valid and enables the OK button. 464 | def command_validate_input(args: adsk.core.ValidateInputsEventArgs): 465 | # General logging for debug. 466 | futil.log(f"{CMD_NAME} Validate Input Event") 467 | 468 | inputs = args.inputs 469 | 470 | # Verify the validity of the input values. This controls if the OK button is enabled or not. 471 | layer_height_input = inputs.itemById("layer_height_input") 472 | if layer_height_input.value >= 0: 473 | args.areInputsValid = True 474 | else: 475 | args.areInputsValid = False 476 | 477 | 478 | # This event handler is called when the command terminates. 479 | def command_destroy(args: adsk.core.CommandEventArgs): 480 | # General logging for debug. 481 | futil.log(f"{CMD_NAME} Command Destroy Event") 482 | 483 | global local_handlers 484 | local_handlers = [] 485 | --------------------------------------------------------------------------------