/
Dogbone.py
executable file
·1148 lines (946 loc) · 59 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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#Author-Peter Ludikar, Gary Singer
#Description-An Add-In for making dog-bone fillets.
# Peter completely revamped the dogbone add-in by Casey Rogers and Patrick Rainsberry and David Liu
# Some of the original utilities have remained, but a lot of the other functionality has changed.
# The original add-in was based on creating sketch points and extruding - Peter found using sketches and extrusion to be very heavy
# on processing resources, so this version has been designed to create dogbones directly by using a hole tool. So far the
# the performance of this approach is day and night compared to the original version.
# Select the face you want the dogbones to drop from. 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.
import logging
from collections import defaultdict
import adsk.core, adsk.fusion
import math
import traceback
import os
import json
import time
from . import dbutils as dbUtils
from math import sqrt as sqrt
#constants - to keep attribute group and names consistent
DOGBONEGROUP = 'dogBoneGroup'
FACE_ID = 'faceID'
REV_ID = 'revId'
ID = 'id'
DEBUGLEVEL = logging.NOTSET
# Generate an edgeId or faceId from object
calcId = lambda x: str(x.tempId) + ':' + x.assemblyContext.name.split(':')[-1] if x.assemblyContext else str(x.tempId) + ':' + x.body.name
makeNative = lambda x: x.nativeObject if x.nativeObject else x
reValidateFace = lambda comp, x: comp.findBRepUsingPoint(x, adsk.fusion.BRepEntityTypes.BRepFaceEntityType,-1.0 ,False ).item(0)
class SelectedEdge:
def __init__(self, edge, edgeId, activeEdgeName, tempId, selectedFace):
self.edge = edge
self.edgeId = edgeId
self.activeEdgeName = activeEdgeName
self.tempId = tempId
self.selected = True
self.selectedFace = selectedFace
def select(self, selection = True):
self.selected = selection
class SelectedFace:
def __init__(self, dog, face, faceId, tempId, occurrenceName, refPoint, commandInputsEdgeSelect):
self.dog = dog
self.face = face # BrepFace
self.faceId = faceId
self.tempId = tempId
self.occurrenceName = occurrenceName
self.refPoint = refPoint
self.commandInputsEdgeSelect = commandInputsEdgeSelect
self.selected = True
self.selectedEdges = {} # Keyed with edge
self.brepEdges = [] # used for quick checking if an edge is already included (below)
#==============================================================================
# this is where inside corner edges, dropping down from the face are processed
#==============================================================================
faceNormal = dbUtils.getFaceNormal(face)
for edge in self.face.body.edges:
if edge.isDegenerate:
continue
if edge in self.brepEdges:
continue
try:
if edge.geometry.curveType != adsk.core.Curve3DTypes.Line3DCurveType:
continue
vector = edge.startVertex.geometry.vectorTo(edge.endVertex.geometry)
if vector.isPerpendicularTo(faceNormal):
continue
if edge.faces.item(0).geometry.objectType != adsk.core.Plane.classType():
continue
if edge.faces.item(1).geometry.objectType != adsk.core.Plane.classType():
continue
if edge.startVertex not in face.vertices:
if edge.endVertex not in face.vertices:
continue
else:
vector = edge.endVertex.geometry.vectorTo(edge.startVertex.geometry)
if vector.dotProduct(faceNormal) >= 0:
continue
if dbUtils.getAngleBetweenFaces(edge) > math.pi:
continue
activeEdgeName = edge.assemblyContext.name.split(':')[-1] if edge.assemblyContext else edge.body.name
edgeId = str(edge.tempId)+':'+ activeEdgeName
self.selectedEdges[edgeId] = SelectedEdge(edge, edgeId, activeEdgeName, edge.tempId, self)
self.brepEdges.append(edge)
dog.addingEdges = True
self.commandInputsEdgeSelect.addSelection(edge)
dog.addingEdges = False
dog.selectedEdges[edgeId] = self.selectedEdges[edgeId] # can be used for reverse lookup of edge to face
except:
dbUtils.messageBox('Failed at edge:\n{}'.format(traceback.format_exc()))
def selectAll(self, selection = True):
self.selected = selection
dog.addingEdges = True
for edgeId, selectedEdge in self.selectedEdges.items():
selectedEdge.select(selection)
if selection:
#commandInputsEdgeSelect.addSelection(edge.edge) # Not working for re-adding.
dog.ui.activeSelections.add(selectedEdge.edge)
else:
dog.ui.activeSelections.removeByEntity(selectedEdge.edge)
dog.addingEdges = False
class DogboneCommand(object):
COMMAND_ID = "dogboneBtn"
faceAssociations = {}
defaultData = {}
def __init__(self):
self.app = adsk.core.Application.get()
self.ui = self.app.userInterface
self.offStr = "0"
self.offVal = None
self.circStr = "0.25 in"
self.circVal = None
self.edges = []
self.benchmark = False
self.errorCount = 0
# self.boneDirection = "top"
self.dbType = 'Normal Dogbone'
self.longside = True
self.minimalPercent = 10.0
self.faceSelections = adsk.core.ObjectCollection.create()
self.fromTop = False
self.addingEdges = 0
self.parametric = True
self.logging = 0
self.loggingLevels = {'Notset':0,'Debug':10,'Info':20,'Warning':30,'Error':40}
self.expandModeGroup = True
self.expandSettingsGroup = False
# self.loggingLevelsLookUp = {self.loggingLevels[k]:k for k in self.loggingLevels}
self.levels = {}
self.handlers = dbUtils.HandlerHelper()
self.appPath = os.path.dirname(os.path.abspath(__file__))
def writeDefaults(self):
self.logger.info('config file write')
self.defaultData['offStr'] = self.offStr
self.defaultData['offVal'] = self.offVal
self.defaultData['circStr'] = self.circStr
self.defaultData['circVal'] = self.circVal
#self.defaultData['!outputUnconstrainedGeometry:' = str(self.outputUnconstrainedGeometry))
self.defaultData['benchmark'] = self.benchmark
# self.defaultData['boneDirection'] = self.boneDirection
self.defaultData['dbType'] = self.dbType
self.defaultData['minimalPercent'] = self.minimalPercent
self.defaultData['fromTop'] = self.fromTop
self.defaultData['parametric'] = self.parametric
self.defaultData['logging'] = self.logging
self.defaultData['mortiseType'] = self.longside
self.defaultData['expandModeGroup'] = self.expandModeGroup
self.defaultData['expandSettingsGroup'] = self.expandSettingsGroup
json_file = open(os.path.join(self.appPath, 'defaults.dat'), 'w', encoding='UTF-8')
json.dump(self.defaultData, json_file, ensure_ascii=False)
json_file.close()
#file.write('!limitParticipation:' = str(self.limitParticipation))
#file.write('!minimumAngle:' = str(self.minimumAngle))
#file.write('!maximumAngle:' = str(self.maximumAngle))
def readDefaults(self):
# self.logger.info('config file read')
if not os.path.isfile(os.path.join(self.appPath, 'defaults.dat')):
return
json_file = open(os.path.join(self.appPath, 'defaults.dat'), 'r', encoding='UTF-8')
try:
self.defaultData = json.load(json_file)
except ValueError:
self.logger.error('default.dat error')
json_file.close()
json_file = open(os.path.join(self.appPath, 'defaults.dat'), 'w', encoding='UTF-8')
json.dump(self.defaultData, json_file, ensure_ascii=False)
return
json_file.close()
try:
self.offStr = self.defaultData['offStr']
self.offVal = self.defaultData['offVal']
self.circStr = self.defaultData['circStr']
self.circVal = self.defaultData['circVal']
#elif var == 'outputUnconstrainedGeometry': self.outputUnconstrainedGeometry = val == 'True'
self.benchmark = self.defaultData['benchmark']
# self.boneDirection = self.defaultData['boneDirection']
self.dbType = self.defaultData['dbType']
self.minimalPercent = self.defaultData['minimalPercent']
self.fromTop = self.defaultData['fromTop']
self.parametric = self.defaultData['parametric']
self.logging = self.defaultData['logging']
self.longside = self.defaultData['mortiseType']
self.expandModeGroup = self.defaultData['expandModeGroup']
self.expandSettingsGroup = self.defaultData['expandSettingsGroup']
except KeyError:
# self.logger.error('Key error on read config file')
#if there's a keyError - means file is corrupted - so, rewrite it with known existing defaultData - it will result in a valid dict,
# but contents may have extra, superfluous data
json_file = open(os.path.join(self.appPath, 'defaults.dat'), 'w', encoding='UTF-8')
json.dump(self.defaultData, json_file, ensure_ascii=False)
json_file.close()
return
def debugFace(self, face):
if self.logger.level < logging.DEBUG:
return
for edge in face.edges:
self.logger.debug('edge {}; startVertex: {}; endVertex: {}'.format(edge.tempId, edge.startVertex.geometry.asArray(), edge.endVertex.geometry.asArray()))
return
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 dogbones at all inside corners of a face', '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:adsk.core.CommandCreatedEventArgs):
"""
important persistent variables:
self.selectedOccurrences - Lookup dictionary
key: activeOccurrenceName
value: list of selectedFaces
provides a quick lookup relationship between each occurrence and in particular which faces have been selected.
The 1st selected face in the list is always the primary face
self.selectedFaces - Lookup dictionary
key: faceId = str(face tempId:occurrenceNumber)
value: [BrepFace, objectCollection of edges, reference point on nativeObject Face]
provides fast method of getting Brep entities associated with a faceId
self.selectedEdges - reverse lookup
key: edgeId = str(edgeId:occurrenceNumber)
value: str(face tempId:occurrenceNumber)
provides fast method of finding face that owns an edge
"""
inputs = adsk.core.CommandCreatedEventArgs.cast(args)
self.faces = []
self.errorCount = 0
self.faceSelections.clear()
self.selectedOccurrences = {}
self.selectedFaces = {}
self.selectedEdges = {}
argsCmd = adsk.core.Command.cast(args)
self.readDefaults()
inputs = adsk.core.CommandInputs.cast(inputs.command.commandInputs)
selInput0 = inputs.addSelectionInput(
'select', 'Face',
'Select a face to apply dogbones to all internal corner edges')
selInput0.tooltip ='Select a face to apply dogbones to all internal corner edges\n*** Select faces by clicking on them. DO NOT DRAG SELECT! ***'
# selInput0.addSelectionFilter('LinearEdges')
selInput0.addSelectionFilter('PlanarFaces')
selInput0.setSelectionLimits(1,0)
selInput1 = inputs.addSelectionInput(
'edgeSelect', 'DogBone Edges',
'Select or de-select any internal edges dropping down from a selected face (to apply dogbones to')
# selInput0.addSelectionFilter('LinearEdges')
selInput1.tooltip ='Select or de-select any internal edges dropping down from a selected face (to apply dogbones to)'
selInput1.addSelectionFilter('LinearEdges')
selInput1.setSelectionLimits(1,0)
selInput1.isVisible = False
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."
offsetInp = inputs.addValueInput(
'offset', 'Tool diameter offset', self.design.unitsManager.defaultLengthUnits,
adsk.core.ValueInput.createByString(self.offStr))
offsetInp.tooltip = "Increases the tool diameter"
offsetInp.tooltipDescription = "Use this to create an oversized dogbone.\n"\
"Normally set to 0. \n"\
"A value of .010 would increase the dogbone diameter by .010 \n"\
"Used when you want to keep the tool diameter and oversize value separate"
modeGroup = adsk.core.GroupCommandInput.cast(inputs.addGroupCommandInput('modeGroup', 'Mode'))
modeGroup.isExpanded = self.expandModeGroup
modeGroupChildInputs = modeGroup.children
modeRowInput = adsk.core.ButtonRowCommandInput.cast(modeGroupChildInputs.addButtonRowCommandInput('modeRow', 'Mode', False))
modeRowInput.listItems.add('Static', not self.parametric, 'resources/staticMode' )
modeRowInput.listItems.add('Parametric', self.parametric, 'resources/parametricMode' )
modeRowInput.tooltipDescription = "Static dogbones do not move with the underlying component geometry. \n" \
"\nParametric dogbones will automatically adjust position with parametric changes to underlying geometry. " \
"Geometry changes must be made via the parametric dialog.\nFusion has more issues/bugs with these!"
typeRowInput = adsk.core.ButtonRowCommandInput.cast(modeGroupChildInputs.addButtonRowCommandInput('dogboneType', 'Type', False))
typeRowInput.listItems.add('Normal Dogbone', self.dbType == 'Normal Dogbone', 'resources/normal' )
typeRowInput.listItems.add('Minimal Dogbone', self.dbType == 'Minimal Dogbone', 'resources/minimal' )
typeRowInput.listItems.add('Mortise Dogbone', self.dbType == 'Mortise Dogbone', 'resources/hidden' )
typeRowInput.tooltipDescription = "Minimal dogbones creates visually less prominent dogbones, but results in an interference fit " \
"that, for example, will require a larger force to insert a tenon into a mortise.\n" \
"\nMortise dogbones create dogbones on the shortest sides, or the longest sides.\n" \
"A piece with a tenon can be used to hide them if they're not cut all the way through the workpiece."
mortiseRowInput = adsk.core.ButtonRowCommandInput.cast(modeGroupChildInputs.addButtonRowCommandInput('mortiseType', 'Mortise Type', False))
mortiseRowInput.listItems.add('On Long Side', self.longside, 'resources/hidden/longside' )
mortiseRowInput.listItems.add('On Short Side', not self.longside, 'resources/hidden/shortside' )
mortiseRowInput.tooltipDescription = "Along Long Side will have the dogbones cut into the longer sides." \
"\nAlong Short Side will have the dogbones cut into the shorter sides."
mortiseRowInput.isVisible = self.dbType == 'Mortise Dogbone'
minPercentInp = modeGroupChildInputs.addValueInput(
'minimalPercent', 'Percentage Reduction', '',
adsk.core.ValueInput.createByReal(self.minimalPercent))
minPercentInp.tooltip = "Percentage of tool radius added to dogBone offset."
minPercentInp.tooltipDescription = "This should typically be left at 10%, but if the fit is too tight, it should be reduced"
minPercentInp.isVisible = self.dbType == 'Minimal Dogbone'
depthRowInput = adsk.core.ButtonRowCommandInput.cast(modeGroupChildInputs.addButtonRowCommandInput('depthExtent', 'Depth Extent', False))
depthRowInput.listItems.add('From Selected Face', not self.fromTop, 'resources/fromFace' )
depthRowInput.listItems.add('From Top Face', self.fromTop, 'resources/fromTop' )
depthRowInput.tooltipDescription = "When \"From Top Face\" is selected, all dogbones will be extended to the top most face\n"\
"\nThis is typically chosen when you don't want to, or can't do, double sided machining."
settingGroup = adsk.core.GroupCommandInput.cast(inputs.addGroupCommandInput('settingsGroup', 'Settings'))
settingGroup.isExpanded = self.expandSettingsGroup
settingGroupChildInputs = settingGroup.children
benchMark = settingGroupChildInputs.addBoolValueInput("benchmark", "Benchmark time", True, "", self.benchmark)
benchMark.tooltip = "Enables benchmarking"
benchMark.tooltipDescription = "When enabled, shows overall time taken to process all selected dogbones."
logDropDownInp = adsk.core.DropDownCommandInput.cast(settingGroupChildInputs.addDropDownCommandInput("logging", "Logging level", adsk.core.DropDownStyles.TextListDropDownStyle))
logDropDownInp.tooltip = "Enables logging"
logDropDownInp.tooltipDescription = "Creates a dogbone.log file. \n" \
"Location: " + os.path.join(self.appPath, 'dogBone.log')
logDropDownInp.listItems.add('Notset', self.logging == 0)
logDropDownInp.listItems.add('Debug', self.logging == 10)
logDropDownInp.listItems.add('Info', self.logging == 20)
cmd = adsk.core.Command.cast(args.command)
# Add handlers to this command.
cmd.execute.add(self.handlers.make_handler(adsk.core.CommandEventHandler, self.onExecute))
cmd.selectionEvent.add(self.handlers.make_handler(adsk.core.SelectionEventHandler, self.onFaceSelect))
cmd.validateInputs.add(
self.handlers.make_handler(adsk.core.ValidateInputsEventHandler, self.onValidate))
cmd.inputChanged.add(
self.handlers.make_handler(adsk.core.InputChangedEventHandler, self.onChange))
#==============================================================================
# routine to process any changed selections
# this is where selection and deselection management takes place
# also where eligible edges are determined
#==============================================================================
def onChange(self, args:adsk.core.InputChangedEventArgs):
changedInput = adsk.core.CommandInput.cast(args.input)
# self.logger.debug('input changed- {}'.format(changedInput.id))
if changedInput.id == 'dogboneType':
changedInput.commandInputs.itemById('minimalPercent').isVisible = (changedInput.commandInputs.itemById('dogboneType').selectedItem.name == 'Minimal Dogbone')
changedInput.commandInputs.itemById('mortiseType').isVisible = (changedInput.commandInputs.itemById('dogboneType').selectedItem.name == 'Mortise Dogbone')
if changedInput.id != 'select' and changedInput.id != 'edgeSelect':
return
# self.logger.debug('input changed- {}'.format(changedInput.id))
if changedInput.id == 'select':
#==============================================================================
# processing changes to face selections
#==============================================================================
numSelectedFaces = sum(1 for face in self.selectedFaces.values() if face.selected)
if numSelectedFaces > changedInput.selectionCount:
# a face has been removed
# If all faces are removed, just iterate through all
if changedInput.selectionCount == 0:
for face in self.selectedFaces.values():
if face.selected:
face.selectAll(False)
changedInput.commandInputs.itemById('edgeSelect').clearSelection()
changedInput.commandInputs.itemById('select').hasFocus = True
changedInput.commandInputs.itemById('edgeSelect').isVisible = False
return
# Else find the missing face in selection
selectionList = [changedInput.selection(i).entity.tempId for i in range(changedInput.selectionCount)]
missingFace = {k for k, v in self.selectedFaces.items() if v.selected and v.tempId not in selectionList}.pop()
changedInput.commandInputs.itemById('edgeSelect').hasFocus = True
self.selectedFaces[missingFace].selectAll(False)
changedInput.commandInputs.itemById('select').hasFocus = True
return
#==============================================================================
# Face has been added - assume that the last selection entity is the one added
#==============================================================================
face = adsk.fusion.BRepFace.cast(changedInput.selection(changedInput.selectionCount -1).entity)
changedInput.commandInputs.itemById('edgeSelect').isVisible = True
changedEntity = face #changedInput.selection(changedInput.selectionCount-1).entity
if changedEntity.assemblyContext:
activeOccurrenceName = changedEntity.assemblyContext.name
else:
activeOccurrenceName = changedEntity.body.name
if changedInput.selection(changedInput.selectionCount-1).entity.assemblyContext:
changedEntityName = changedInput.selection(changedInput.selectionCount-1).entity.assemblyContext.name.split(':')[-1]
else:
changedEntityName = changedEntity.body.name
faceId = str(changedEntity.tempId) + ":" + changedEntityName
if faceId in self.selectedFaces :
changedInput.commandInputs.itemById('edgeSelect').hasFocus = True
self.selectedFaces[faceId].selectAll(True)
changedInput.commandInputs.itemById('select').hasFocus = True
return
newSelectedFace = SelectedFace(
self,
face,
faceId,
changedEntity.tempId,
changedEntityName,
face.nativeObject.pointOnFace if face.assemblyContext else face.pointOnFace,
changedInput.commandInputs.itemById('edgeSelect')
) # creates a collecton (of edges) associated with a faceId
faces = []
faces = self.selectedOccurrences.get(activeOccurrenceName, faces)
faces.append(newSelectedFace)
self.selectedOccurrences[activeOccurrenceName] = faces # adds a face to a list of faces associated with this occurrence
self.selectedFaces[faceId] = newSelectedFace
#end of processing faces
#==============================================================================
# Processing changed edge selection
#==============================================================================
if changedInput.id != 'edgeSelect':
return
if sum(1 for edge in self.selectedEdges.values() if edge.selected) > changedInput.selectionCount:
#==============================================================================
# an edge has been removed
#==============================================================================
changedSelectionList = [changedInput.selection(i).entity for i in range(changedInput.selectionCount)]
changedEdgeIdSet = set(map(calcId, changedSelectionList)) # converts list of edges to a list of their edgeIds
missingEdges = (set(self.selectedEdges.keys()) - changedEdgeIdSet)
for missingEdge in missingEdges:
self.selectedEdges[missingEdge].select(False)
# Note - let the user manually unselect the face if they want to choose a different face
return
# End of processing removed edge
else:
#==============================================================================
# Start of adding a selected edge
# Edge has been added - assume that the last selection entity is the one added
#==============================================================================
edge = adsk.fusion.BRepEdge.cast(changedInput.selection(changedInput.selectionCount - 1).entity)
self.selectedEdges[calcId(edge)].select() # Get selectedFace then get selectedEdge, then call function
def parseInputs(self, inputs):
'''==============================================================================
put the selections into variables that can be accessed by the main routine
==============================================================================
'''
inputs = {inp.id: inp for inp in inputs}
self.logging = self.loggingLevels[inputs['logging'].selectedItem.name]
self.logHandler.setLevel(self.logging)
self.logger.debug('Parsing inputs')
self.circStr = inputs['circDiameter'].expression
self.circVal = inputs['circDiameter'].value
self.offStr = inputs['offset'].expression
self.offVal = inputs['offset'].value
self.benchmark = inputs['benchmark'].value
self.dbType = inputs['dogboneType'].selectedItem.name
self.minimalPercent = inputs['minimalPercent'].value
self.fromTop = (inputs['depthExtent'].selectedItem.name == 'From Top Face')
self.parametric = (inputs['modeRow'].selectedItem.name == 'Parametric')
self.longside = (inputs['mortiseType'].selectedItem.name == 'On Long Side')
self.expandModeGroup = (inputs['modeGroup']).isExpanded
self.expandSettingsGroup = (inputs['settingsGroup']).isExpanded
self.logger.debug('self.fromTop = {}'.format(self.fromTop))
self.logger.debug('self.dbType = {}'.format(self.dbType))
self.logger.debug('self.parametric = {}'.format(self.parametric))
self.logger.debug('self.circStr = {}'.format(self.circStr))
self.logger.debug('self.circDiameter = {}'.format(self.circVal))
self.logger.debug('self.offStr = {}'.format(self.offStr))
self.logger.debug('self.offVal = {}'.format(self.offVal))
self.logger.debug('self.benchmark = {}'.format(self.benchmark))
self.logger.debug('self.mortiseType = {}'.format(self.longside))
self.logger.debug('self.expandModeGroup = {}'.format(self.expandModeGroup))
self.logger.debug('self.expandSettingsGroup = {}'.format(self.expandSettingsGroup))
self.edges = []
self.faces = []
for i in range(inputs['edgeSelect'].selectionCount):
entity = inputs['edgeSelect'].selection(i).entity
if entity.objectType == adsk.fusion.BRepEdge.classType():
self.edges.append(entity)
for i in range(inputs['select'].selectionCount):
entity = inputs['select'].selection(i).entity
if entity.objectType == adsk.fusion.BRepFace.classType():
self.faces.append(entity)
def initLogger(self):
self.logger = logging.getLogger(__name__)
self.formatter = logging.Formatter('%(asctime)s ; %(name)s ; %(levelname)s ; %(lineno)d; %(message)s')
# if not os.path.isfile(os.path.join(self.appPath, 'dogBone.log')):
# return
self.logHandler = logging.FileHandler(os.path.join(self.appPath, 'dogbone.log'), mode='w')
self.logHandler.setFormatter(self.formatter)
self.logHandler.flush()
self.logger.addHandler(self.logHandler)
def closeLogger(self):
for handler in self.logger.handlers:
handler.flush()
handler.close()
def onExecute(self, args):
start = time.time()
self.initLogger()
self.logger.log(0, 'logging Level = %(levelname)')
self.parseInputs(args.firingEvent.sender.commandInputs)
self.logHandler.setLevel(self.logging)
self.logger.setLevel(self.logging)
self.writeDefaults()
if self.parametric:
userParams = adsk.fusion.UserParameters.cast(self.design.userParameters)
#set up parameters, so that changes can be easily made after dogbones have been inserted
if not userParams.itemByName('dbToolDia'):
dValIn = adsk.core.ValueInput.createByString(self.circStr)
dParameter = userParams.add('dbToolDia', dValIn, self.design.unitsManager.defaultLengthUnits, '')
dParameter.isFavorite = True
else:
uParam = userParams.itemByName('dbToolDia')
uParam.expression = self.circStr
uParam.isFavorite = True
if not userParams.itemByName('dbOffset'):
rValIn = adsk.core.ValueInput.createByString(self.offStr)
rParameter = userParams.add('dbOffset',rValIn, self.design.unitsManager.defaultLengthUnits, 'Do NOT change formula')
else:
uParam = userParams.itemByName('dbOffset')
uParam.expression = self.offStr
uParam.comment = 'Do NOT change formula'
if not userParams.itemByName('dbRadius'):
rValIn = adsk.core.ValueInput.createByString('(dbToolDia + dbOffset)/2')
rParameter = userParams.add('dbRadius',rValIn, self.design.unitsManager.defaultLengthUnits, 'Do NOT change formula')
else:
uParam = userParams.itemByName('dbRadius')
uParam.expression = '(dbToolDia + dbOffset)/2'
uParam.comment = 'Do NOT change formula'
if not userParams.itemByName('dbMinPercent'):
rValIn = adsk.core.ValueInput.createByReal(self.minimalPercent)
rParameter = userParams.add('dbMinPercent',rValIn, '', '')
rParameter.isFavorite = True
else:
uParam = userParams.itemByName('dbMinPercent')
uParam.value = self.minimalPercent
uParam.comment = ''
uParam.isFavorite = True
if not userParams.itemByName('dbHoleOffset'):
oValIn = adsk.core.ValueInput.createByString('dbRadius / sqrt(2)' + (' * (1 + dbMinPercent/100)') if self.dbType == 'Minimal Dogbone' else 'dbRadius' if self.dbType == 'Mortise Dogbone' else 'dbRadius / sqrt(2)')
oParameter = userParams.add('dbHoleOffset', oValIn, self.design.unitsManager.defaultLengthUnits, 'Do NOT change formula')
else:
uParam = userParams.itemByName('dbHoleOffset')
uParam.expression = 'dbRadius / sqrt(2)' + (' * (1 + dbMinPercent/100)') if self.dbType == 'Minimal Dogbone' else 'dbRadius' if self.dbType == 'Mortise Dogbone' else 'dbRadius / sqrt(2)'
uParam.comment = 'Do NOT change formula'
self.radius = userParams.itemByName('dbRadius').value
self.offset = adsk.core.ValueInput.createByString('dbOffset')
self.offset = adsk.core.ValueInput.createByReal(userParams.itemByName('dbHoleOffset').value)
self.createParametricDogbones()
else: #Static dogbones
self.radius = (self.circVal + self.offVal) / 2
self.offset = self.radius / sqrt(2) * (1 + self.minimalPercent/100) if self.dbType == 'Minimal Dogbone' else self.radius if self.dbType == 'Mortise Dogbone' else self.radius / sqrt(2)
self.createStaticDogbones()
self.logger.info('all dogbones complete\n-------------------------------------------\n')
self.closeLogger()
if self.benchmark:
dbUtils.messageBox("Benchmark: {:.02f} sec processing {} edges".format(
time.time() - start, len(self.edges)))
################################################################################
def onValidate(self, args):
cmd = adsk.core.ValidateInputsEventArgs.cast(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
def onFaceSelect(self, args):
'''==============================================================================
Routine gets called with every mouse movement, if a commandInput select is active
==============================================================================
'''
eventArgs = adsk.core.SelectionEventArgs.cast(args)
# Check which selection input the event is firing for.
activeIn = eventArgs.firingEvent.activeInput
if activeIn.id != 'select' and activeIn.id != 'edgeSelect':
return # jump out if not dealing with either of the two selection boxes
if activeIn.id == 'select':
#==============================================================================
# processing activities when faces are being selected
# selection filter is limited to planar faces
# makes sure only valid occurrences and components are selectable
#==============================================================================
if not len( self.selectedOccurrences ): #get out if the face selection list is empty
eventArgs.isSelectable = True
return
if not eventArgs.selection.entity.assemblyContext:
# dealing with a root component body
activeBodyName = eventArgs.selection.entity.body.name
try:
faces = self.selectedOccurrences[activeBodyName]
for face in faces:
if face.selected:
primaryFace = face
break
else:
eventArgs.isSelectable = True
return
except (KeyError, IndexError) as e:
return
primaryFaceNormal = dbUtils.getFaceNormal(primaryFace.face)
if primaryFaceNormal.isParallelTo(dbUtils.getFaceNormal(eventArgs.selection.entity)):
eventArgs.isSelectable = True
return
eventArgs.isSelectable = False
return
# End of root component face processing
#==============================================================================
# Start of occurrence face processing
#==============================================================================
activeOccurrence = eventArgs.selection.entity.assemblyContext
activeOccurrenceName = activeOccurrence.name
activeComponent = activeOccurrence.component
# we got here because the face is either not in root or is on the existing selected list
# at this point only need to check for duplicate component selection - Only one component allowed, to save on conflict checking
try:
selectedComponentList = [x[0].face.assemblyContext.component for x in self.selectedOccurrences.values() if x[0].face.assemblyContext]
except KeyError:
eventArgs.isSelectable = True
return
if activeComponent not in selectedComponentList:
eventArgs.isSelectable = True
return
if activeOccurrenceName not in self.selectedOccurrences: #check if mouse is over a face that is not already selected
eventArgs.isSelectable = False
return
try:
faces = self.selectedOccurrences[activeOccurrenceName]
for face in faces:
if face.selected:
primaryFace = face
break
else:
eventArgs.isSelectable = True
return
except KeyError:
return
primaryFaceNormal = dbUtils.getFaceNormal(primaryFace.face)
if primaryFaceNormal.isParallelTo(dbUtils.getFaceNormal(eventArgs.selection.entity)):
eventArgs.isSelectable = True
return
eventArgs.isSelectable = False
return
# end selecting faces
else:
#==============================================================================
# processing edges associated with face - edges selection has focus
#==============================================================================
if self.addingEdges:
return
selected = eventArgs.selection
currentEdge = adsk.fusion.BRepEdge.cast(selected.entity)
activeOccurrence = eventArgs.selection.entity.assemblyContext
if eventArgs.selection.entity.assemblyContext:
activeOccurrenceName = activeOccurrence.name
else:
activeOccurrenceName = eventArgs.selection.entity.body.name
occurrenceNumber = activeOccurrenceName.split(':')[-1]
edgeId = str(currentEdge.tempId) + ':' + occurrenceNumber
if (edgeId in self.selectedEdges and self.selectedEdges[edgeId].selectedFace.selected):
eventArgs.isSelectable = True
else:
eventArgs.isSelectable = False
return
@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 for parametric dogbones
def createParametricDogbones(self):
self.logger.info('Creating parametric dogbones')
self.errorCount = 0
if not self.design:
raise RuntimeError('No active Fusion design')
holeInput = adsk.fusion.HoleFeatureInput.cast(None)
offsetByStr = adsk.core.ValueInput.createByString('dbHoleOffset')
centreDistance = self.radius*(1+self.minimalPercent/100 if self.dbType=='Minimal Dogbone' else 1)
for occurrenceFace in self.selectedOccurrences.values():
startTlMarker = self.design.timeline.markerPosition
if occurrenceFace[0].face.assemblyContext:
comp = occurrenceFace[0].face.assemblyContext.component
occ = occurrenceFace[0].face.assemblyContext
self.logger.debug('processing component = {}'.format(comp.name))
self.logger.debug('processing occurrence = {}'.format(occ.name))
#entityName = occ.name.split(':')[-1]
else:
comp = self.rootComp
occ = None
self.logger.debug('processing Rootcomponent')
if self.fromTop:
(topFace, topFaceRefPoint) = dbUtils.getTopFace(makeNative(occurrenceFace[0].face))
self.logger.info('Processing holes from top face - {}'.format(topFace.body.name))
for selectedFace in occurrenceFace:
if len(selectedFace.selectedEdges.values()) <1:
self.logger.debug('Face has no edges')
face = makeNative(selectedFace.face)
comp = adsk.fusion.Component.cast(comp)
if not face.isValid:
self.logger.debug('revalidating Face')
face = reValidateFace(comp, selectedFace.refPoint)
self.logger.debug('Processing Face = {}'.format(face.tempId))
#faceNormal = dbUtils.getFaceNormal(face.nativeObject)
if self.fromTop:
self.logger.debug('topFace type {}'.format(type(topFace)))
if not topFace.isValid:
self.logger.debug('revalidating topFace')
topFace = reValidateFace(comp, topFaceRefPoint)
topFace = makeNative(topFace)
self.logger.debug('topFace isValid = {}'.format(topFace.isValid))
transformVector = dbUtils.getTranslateVectorBetweenFaces(face, topFace)
self.logger.debug('creating transformVector to topFace = ({},{},{}) length = {}'.format(transformVector.x, transformVector.y, transformVector.z, transformVector.length))
for selectedEdge in selectedFace.selectedEdges.values():
self.logger.debug('Processing edge - {}'.format(selectedEdge.edge.tempId))
if not selectedEdge.selected:
self.logger.debug(' Not selected. Skipping...')
continue
if not face.isValid:
self.logger.debug('Revalidating face')
face = reValidateFace(comp, selectedFace.refPoint)
if not selectedEdge.edge.isValid:
continue # edges that have been processed already will not be valid any more - at the moment this is easier than removing the
# affected edge from self.edges after having been processed
edge = makeNative(selectedEdge.edge)
try:
if not dbUtils.isEdgeAssociatedWithFace(face, edge):
continue # skip if edge is not associated with the face currently being processed
except:
pass
startVertex = adsk.fusion.BRepVertex.cast(dbUtils.getVertexAtFace(face, edge))
extentToEntity = dbUtils.findExtent(face, edge)
extentToEntity = makeNative(extentToEntity)
self.logger.debug('extentToEntity - {}'.format(extentToEntity.isValid))
if not extentToEntity.isValid:
self.logger.debug('To face invalid')
try:
(edge1, edge2) = dbUtils.getCornerEdgesAtFace(face, edge)
except:
self.logger.exception('Failed at findAdjecentFaceEdges')
dbUtils.messageBox('Failed at findAdjecentFaceEdges:\n{}'.format(traceback.format_exc()))
centrePoint = makeNative(startVertex).geometry.copy()
selectedEdgeFaces = makeNative(selectedEdge.edge).faces
dirVect = adsk.core.Vector3D.cast(dbUtils.getFaceNormal(selectedEdgeFaces[0]).copy())
dirVect.add(dbUtils.getFaceNormal(selectedEdgeFaces[1]))
dirVect.normalize()
dirVect.scaleBy(centreDistance) #ideally radius should be linked to parameters,
if self.dbType == 'Mortise Dogbone':
direction0 = dbUtils.correctedEdgeVector(edge1,startVertex)
direction1 = dbUtils.correctedEdgeVector(edge2,startVertex)
if self.longside:
if (edge1.length > edge2.length):
dirVect = direction0
edge1OffsetByStr = adsk.core.ValueInput.createByReal(0)
edge2OffsetByStr = offsetByStr
else:
dirVect = direction1
edge2OffsetByStr = adsk.core.ValueInput.createByReal(0)
edge1OffsetByStr = offsetByStr
else:
if (edge1.length > edge2.length):
dirVect = direction1
edge2OffsetByStr = adsk.core.ValueInput.createByReal(0)
edge1OffsetByStr = offsetByStr
else:
dirVect = direction0
edge1OffsetByStr = adsk.core.ValueInput.createByReal(0)
edge2OffsetByStr = offsetByStr
else:
dirVect = adsk.core.Vector3D.cast(dbUtils.getFaceNormal(makeNative(selectedEdgeFaces[0])).copy())
dirVect.add(dbUtils.getFaceNormal(makeNative(selectedEdgeFaces[1])))
edge1OffsetByStr = offsetByStr
edge2OffsetByStr = offsetByStr
centrePoint.translateBy(dirVect)
self.logger.debug('centrePoint = ({},{},{})'.format(centrePoint.x, centrePoint.y, centrePoint.z))
if self.fromTop:
centrePoint.translateBy(transformVector)
self.logger.debug('centrePoint at topFace = {}'.format(centrePoint.asArray()))
holePlane = topFace if self.fromTop else face
if not holePlane.isValid:
holePlane = reValidateFace(comp, topFaceRefPoint)
else:
holePlane = makeNative(face)
holes = comp.features.holeFeatures
holeInput = holes.createSimpleInput(adsk.core.ValueInput.createByString('dbRadius*2'))
# holeInput.creationOccurrence = occ #This needs to be uncommented once AD fixes component copy issue!!
holeInput.isDefaultDirection = True
holeInput.tipAngle = adsk.core.ValueInput.createByString('180 deg')
# holeInput.participantBodies = [face.nativeObject.body if occ else face.body] #Restore this once AD fixes occurrence bugs
holeInput.participantBodies = [makeNative(face.body)]
self.logger.debug('extentToEntity before setPositionByPlaneAndOffsets - {}'.format(extentToEntity.isValid))
holeInput.setPositionByPlaneAndOffsets(holePlane, centrePoint, edge1, edge1OffsetByStr, edge2, edge2OffsetByStr)
self.logger.debug('extentToEntity after setPositionByPlaneAndOffsets - {}'.format(extentToEntity.isValid))
holeInput.setOneSideToExtent(extentToEntity, False)
self.logger.info('hole added to list - {}'.format(centrePoint.asArray()))
holeFeature = holes.add(holeInput)
holeFeature.name = 'dogbone'
holeFeature.isSuppressed = True
for hole in holes:
if hole.name[:7] != 'dogbone':
break
hole.isSuppressed = False
endTlMarker = self.design.timeline.markerPosition-1
if endTlMarker - startTlMarker >0:
timelineGroup = self.design.timeline.timelineGroups.add(startTlMarker,endTlMarker)
timelineGroup.name = 'dogbone'
# self.logger.debug('doEvents - allowing display to refresh')
# adsk.doEvents()
if self.errorCount >0:
dbUtils.messageBox('Reported errors:{}\nYou may not need to do anything, \nbut check holes have been created'.format(self.errorCount))
def createStaticDogbones(self):
self.logger.info('Creating static dogbones')
self.errorCount = 0
if not self.design:
raise RuntimeError('No active Fusion design')
holeInput = adsk.fusion.HoleFeatureInput.cast(None)
centreDistance = self.radius*(1+self.minimalPercent/100 if self.dbType == 'Minimal Dogbone' else 1)
for occurrenceFace in self.selectedOccurrences.values():
startTlMarker = self.design.timeline.markerPosition
if occurrenceFace[0].face.assemblyContext:
comp = occurrenceFace[0].face.assemblyContext.component
occ = occurrenceFace[0].face.assemblyContext
self.logger.info('processing component = {}'.format(comp.name))
self.logger.info('processing occurrence = {}'.format(occ.name))
#entityName = occ.name.split(':')[-1]
else:
comp = self.rootComp
occ = None
self.logger.info('processing Rootcomponent')
if self.fromTop:
(topFace, topFaceRefPoint) = dbUtils.getTopFace(makeNative(occurrenceFace[0].face))
self.logger.debug('topFace ref point: {}'.format(topFaceRefPoint.asArray()))
self.logger.info('Processing holes from top face - {}'.format(topFace.tempId))
self.debugFace(topFace)
sketch = adsk.fusion.Sketch.cast(comp.sketches.add(topFace)) #used for fault finding
sketch.name = 'dogbone'
sketch.isComputeDeferred = True