├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------