forked from DVE2000/Dogbone
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDogbone.py
347 lines (273 loc) · 13.8 KB
/
Dogbone.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#Author-Casey Rogers and Patrick Rainsberry and David Liu
#Description-An Add-In for making dog-bone fillets.
# Select edges interior to 90 degree angles. Specify a tool diameter and a radial offset.
# The add-in will then create a dogbone with diamater equal to the tool diameter plus
# twice the offset (as the offset is applied to the radius) at each selected edge.
# Alternatively, select an entire body and the add-in will automatically apply a dog-bone to all interior vertical edges
from collections import defaultdict
import adsk.core, adsk.fusion
import math
import traceback
import time
from . import utils
class DogboneCommand(object):
COMMAND_ID = "dogboneBtn"
def __init__(self):
self.app = adsk.core.Application.get()
self.ui = self.app.userInterface
self.offStr = "0"
self.offVal = None
self.circStr = "cutter"
self.circVal = None
self.yUp = False
self.outputUnconstrainedGeometry = True
self.edges = []
self.benchmark = False
self.handlers = utils.HandlerHelper()
def addButton(self):
# clean up any crashed instances of the button if existing
try:
self.removeButton()
except:
pass
# add add-in to UI
buttonDogbone = self.ui.commandDefinitions.addButtonDefinition(
self.COMMAND_ID, 'Dogbone', 'Creates a dogbone at the corner of two lines/edges', 'Resources')
buttonDogbone.commandCreated.add(self.handlers.make_handler(adsk.core.CommandCreatedEventHandler,
self.onCreate))
createPanel = self.ui.allToolbarPanels.itemById('SolidCreatePanel')
buttonControl = createPanel.controls.addCommand(buttonDogbone, 'dogboneBtn')
# Make the button available in the panel.
buttonControl.isPromotedByDefault = True
buttonControl.isPromoted = True
def removeButton(self):
cmdDef = self.ui.commandDefinitions.itemById(self.COMMAND_ID)
if cmdDef:
cmdDef.deleteMe()
createPanel = self.ui.allToolbarPanels.itemById('SolidCreatePanel')
cntrl = createPanel.controls.itemById(self.COMMAND_ID)
if cntrl:
cntrl.deleteMe()
def onCreate(self, args):
inputs = args.command.commandInputs
selInput0 = inputs.addSelectionInput(
'select', 'Interior Edges or Solid Bodies',
'Select the edge interior to each corner, or a body to apply to all internal edges')
selInput0.addSelectionFilter('LinearEdges')
selInput0.addSelectionFilter('SolidBodies')
selInput0.setSelectionLimits(1,0)
inp = inputs.addValueInput(
'circDiameter', 'Tool Diameter', self.design.unitsManager.defaultLengthUnits,
adsk.core.ValueInput.createByString(self.circStr))
inp.tooltip = "Size of the tool with which you'll cut the dogbone."
inp = inputs.addValueInput(
'offset', 'Additional Offset', self.design.unitsManager.defaultLengthUnits,
adsk.core.ValueInput.createByString(self.offStr))
inp.tooltip = "Additional increase to the radius of the dogbone."
inp = inputs.addBoolValueInput("yUp", "Y-Up", True, "", self.yUp)
inp.tooltip = "Controls which direction is vertical (parallel to cutter). " \
"Check this box to use Y, otherwise Z."
inp = inputs.addBoolValueInput("outputUnconstrainedGeometry",
"Output unconstrained geometry",
True, "", self.outputUnconstrainedGeometry)
inp.tooltip = "~5x faster, but non-parametric. " \
"If enabled, you'll have to delete and re-generate dogbones if geometry " \
"preceding dogbones is updated."
inputs.addBoolValueInput("benchmark", "Benchmark running time", True, "", self.benchmark)
# Add handlers to this command.
args.command.execute.add(self.handlers.make_handler(adsk.core.CommandEventHandler, self.onExecute))
args.command.validateInputs.add(
self.handlers.make_handler(adsk.core.ValidateInputsEventHandler, self.onValidate))
def parseInputs(self, inputs):
inputs = {inp.id: inp for inp in inputs}
self.circStr = inputs['circDiameter'].expression
self.circVal = inputs['circDiameter'].value
self.offStr = inputs['offset'].expression
self.offVal = inputs['offset'].value
self.outputUnconstrainedGeometry = inputs['outputUnconstrainedGeometry'].value
self.yUp = inputs['yUp'].value
self.benchmark = inputs['benchmark'].value
self.edges = []
bodies = []
for i in range(inputs['select'].selectionCount):
entity = inputs['select'].selection(i).entity
if entity.objectType == adsk.fusion.BRepBody.classType():
bodies.append(entity)
elif entity.objectType == adsk.fusion.BRepEdge.classType():
self.edges.append(entity)
for body in bodies:
for bodyEdge in body.edges:
if bodyEdge.geometry.objectType == adsk.core.Line3D.classType():
if utils.isVertical(bodyEdge, self.yUp):
# Check if its an internal edge
if utils.getAngleBetweenFaces(bodyEdge) < math.pi:
# Add edge to the selection
self.edges.append(bodyEdge)
def onExecute(self, args):
start = time.time()
self.parseInputs(args.firingEvent.sender.commandInputs)
self.createConsolidatedDogbones()
if self.benchmark:
utils.messageBox("Benchmark: {:.02f} sec processing {} edges".format(
time.time() - start, len(self.edges)))
def onValidate(self, args):
cmd = args.firingEvent.sender
for input in cmd.commandInputs:
if input.id == 'select':
if input.selectionCount < 1:
args.areInputsValid = False
elif input.id == 'circDiameter':
if input.value <= 0:
args.areInputsValid = False
@property
def design(self):
return self.app.activeProduct
@property
def rootComp(self):
return self.design.rootComponent
@property
def originPlane(self):
return self.rootComp.xZConstructionPlane if self.yUp else self.rootComp.xYConstructionPlane
# The main algorithm
def createConsolidatedDogbones(self):
if not self.design:
raise RuntimeError('No active Fusion design')
sketches = self.rootComp.sketches
planes = self.rootComp.constructionPlanes
extrudes = self.rootComp.features.extrudeFeatures
startIndex = self.design.timeline.markerPosition
progressDialog = self.ui.createProgressDialog()
progressDialog.cancelButtonText = 'Cancel'
progressDialog.isBackgroundTranslucent = False
progressDialog.isCancelButtonShown = True
progressDialog.show('Create Dogbones', "Computing edge groups (%m edges)", 0, len(self.edges))
adsk.doEvents()
progressMsg = '[%p%] %v / %m dogbones created'
skipped = 0
for (h0, h1), edges in self.groupEdgesByVExtent(self.edges).items():
# Edges with the same vertical extent will be dogboned using one sketch + extrude-cut operation.
progressDialog.message = "{}\nOperating on {} edges with extent {:.03f},{:.03f}".format(
progressMsg, len(edges), h0, h1)
adsk.doEvents()
planeInput = planes.createInput()
planeInput.setByOffset(self.originPlane, adsk.core.ValueInput.createByReal(h0))
h0Plane = planes.add(planeInput)
sketch = sketches.add(h0Plane)
# Deferring sketch computation only works when using unconstrained geometry.
# Otherwise, projected lines in the sketch won't be computed.
sketch.isComputeDeferred = self.outputUnconstrainedGeometry
for edge, (cornerEdge0, cornerEdge1) in edges:
if progressDialog.wasCancelled:
return
if not utils.isVertical(edge, self.yUp):
progressDialog.progressValue += 1
skipped += 1
continue
self.addDogboneCircle(cornerEdge0, cornerEdge1, sketch)
progressDialog.progressValue += 1
adsk.doEvents()
progressDialog.message += "\nExtruding"
adsk.doEvents()
# Extrude-cut the dogbones
sketch.isComputeDeferred = False
profileColl = adsk.core.ObjectCollection.create()
for prof in sketch.profiles:
profileColl.add(prof)
exInput = extrudes.createInput(profileColl, adsk.fusion.FeatureOperations.CutFeatureOperation)
exInput.setDistanceExtent(False, adsk.core.ValueInput.createByReal(h1 - h0))
extrudes.add(exInput)
progressDialog.message = "All done. Grouping timeline operations."
adsk.doEvents()
# group all the features we added
endIndex = self.design.timeline.markerPosition - 1
# if endIndex > startIndex: # at least two items to group
# utils.messageBox("{} - {}".format(startIndex, endIndex))
# self.design.timeline.timelineGroups.add(startIndex, endIndex)
progressDialog.hide()
if skipped:
utils.messageBox("Skipped {} non-vertical edges".format(skipped))
def addDogboneCircle(self, cornerEdge0, cornerEdge1, sketch):
if self.outputUnconstrainedGeometry:
# OPTIMIZATION: Directly compute where circle should go.
# Don't use projected geometry, because it's slow.
# Don't use sketch constraints, because it's slow.
# Corner is defined by points c-a-b, a is where the edges meet.
a, b, c = [sketch.modelToSketchSpace(p.geometry)
for p in utils.findPoints(cornerEdge0, cornerEdge1)]
a.z = b.z = c.z = 0
ab = a.vectorTo(b)
ab.normalize()
ac = a.vectorTo(c)
ac.normalize()
ad = ab.copy()
ad.add(ac)
ad.normalize()
radius = self.circVal / 2 + self.offVal
ad.scaleBy(radius)
d = a.copy()
d.translateBy(ad)
sketch.sketchCurves.sketchCircles.addByCenterRadius(d, radius)
else:
# project the dogbone's corner onto the sketch
line1 = sketch.project(cornerEdge0).item(0)
line2 = sketch.project(cornerEdge1).item(0)
# Corner is defined by points c-a-b, a is where the edges meet.
a, b, c = utils.findPoints(line1, line2)
# This is a temporary point for our Dogbone sketch's centerline to end at
d = adsk.core.Point3D.create((b.geometry.x + c.geometry.x) / 2,
(b.geometry.y + c.geometry.y) / 2, 0)
line0 = sketch.sketchCurves.sketchLines.addByTwoPoints(a, d)
line0.isConstruction = True
line1.isConstruction = True
line2.isConstruction = True
# line0 should form line a-d that bisects angle c-a-b.
sketch.geometricConstraints.addSymmetry(line1, line2, line0)
# Constrain the length of the centerline to the radius of the desired dogbone
length = sketch.sketchDimensions.addDistanceDimension(
line0.startSketchPoint, line0.endSketchPoint,
adsk.fusion.DimensionOrientations.AlignedDimensionOrientation,
utils.findMidPoint(line0))
length.parameter.expression = self.circStr + "/ 2 + " + self.offStr
# Create the dogbone's profile
circle = sketch.sketchCurves.sketchCircles.addByCenterRadius(line0.endSketchPoint, self.circVal / 2)
sketch.geometricConstraints.addCoincident(a, circle)
def groupEdgesByVExtent(self, edges):
"""Group edges by their vertical extent, returning a dict where the keys are vertical extents
(h0, h1 where h0 < h1), and the value is a list of edges that span that extent."""
edgesByExtent = defaultdict(list)
for edge in edges:
approxExtent = self.normalizeVExtent(
self.getH(edge.startVertex),
self.getH(edge.endVertex))
edgesByExtent[approxExtent].append((edge, utils.findCorner(edge)))
# Now compute the true (unrounded) extent as the min and max of all extents in each group.
edgesByTrueExtent = {}
for approxExtent, edges in edgesByExtent.items():
h0, h1 = 1e20, -1e20
for e, corner in edges:
h0 = min(h0, self.getH(e.startVertex), self.getH(e.endVertex))
h1 = max(h1, self.getH(e.startVertex), self.getH(e.endVertex))
edgesByTrueExtent[(h0, h1)] = edges
return edgesByTrueExtent
def getH(self, point):
return point.geometry.y if self.yUp else point.geometry.z
@staticmethod
def normalizeVExtent(h0, h1):
"""Given a vertical extent (h0, h1), round the extent values and make sure they are ordered correctly.
This makes them suitable for a hash key, as extents that are functionally identical (but different due to
machine precision or reversed direction) will have the same key."""
if h0 > h1:
return round(h1, 5), round(h0, 5)
else:
return round(h0, 5), round(h1, 5)
dog = DogboneCommand()
def run(context):
try:
dog.addButton()
except:
utils.messageBox(traceback.format_exc())
def stop(context):
try:
dog.removeButton()
except:
utils.messageBox(traceback.format_exc())