-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathImage2Surface.py
More file actions
316 lines (264 loc) · 13.3 KB
/
Copy pathImage2Surface.py
File metadata and controls
316 lines (264 loc) · 13.3 KB
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
#Author-Hans Kellner
#Description-This is an Autodesk Fusion script that's used for generating surfaces from images.
#Copyright (C) 2015-2026 Hans Kellner: https://github.com/hanskellner/Fusion360Image2Surface
#MIT License: See https://github.com/hanskellner/Fusion360Image2Surface/LICENSE.md
import adsk.core, adsk.fusion, adsk.cam, traceback
import json, tempfile, platform, os
# global set of event handlers to keep them referenced for the duration of the command
handlers = []
_app = adsk.core.Application.cast(None)
_ui = adsk.core.UserInterface.cast(None)
# Return the active design's default length unit as a string ('mm', 'cm', 'm',
# 'in', 'ft'). The palette UI uses this to label and scale its controls so the
# user works in the document's units rather than always millimeters.
def documentLengthUnit():
try:
design = adsk.fusion.Design.cast(_app.activeProduct)
if design:
return design.unitsManager.defaultLengthUnits
except:
pass
return 'mm'
# Map a unit string to the matching MeshUnits enum used when importing the OBJ.
# The OBJ vertices are authored numerically in the document's units (see the
# HTML/JS side), so the mesh must be imported with the corresponding unit.
def meshUnitFor(unitStr):
unitMap = {
'mm': adsk.fusion.MeshUnits.MillimeterMeshUnit,
'cm': adsk.fusion.MeshUnits.CentimeterMeshUnit,
'm': adsk.fusion.MeshUnits.MeterMeshUnit,
'in': adsk.fusion.MeshUnits.InchMeshUnit,
'ft': adsk.fusion.MeshUnits.FootMeshUnit
}
# Fusion's internal database length unit is centimeters - a safe fallback.
return unitMap.get(unitStr, adsk.fusion.MeshUnits.CentimeterMeshUnit)
# Push the current document length unit to the palette's HTML/JS so it can
# relabel and rescale its controls. Mirrors the Voronoi add-in's push model
# (returnData from incomingFromHTML round-trips as a JS Promise, so it is not
# used here).
def sendUnitsToPalette(palette):
try:
unitJson = json.dumps({ 'unit': documentLengthUnit() })
if palette:
palette.sendInfoToHTML('units', unitJson) # async push (received by fusionJavaScriptHandler)
return unitJson
except:
return json.dumps({ 'unit': 'mm' })
# Event handler for the commandExecuted event.
class ShowPaletteCommandExecuteHandler(adsk.core.CommandEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
global _app, _ui
# Verify that in parametric design mode
design = _app.activeProduct
if design.designType != adsk.fusion.DesignTypes.ParametricDesignType:
_ui.messageBox('The "Image2Surface" command must be run in parametric modeling mode.\n\nPlease enable "Capture design history" for your document.')
return
# Create and display the palette.
palette = _ui.palettes.itemById('Image2SurfacePalette')
if not palette:
# NOTE: do NOT add a ?v= query to this local HTML path - Fusion
# treats it as a literal filename and fails to load (blank page).
# The ?v= cache-busting lives only on the <script>/<link> tags.
palette = _ui.palettes.add('Image2SurfacePalette', 'Image2Surface', 'image2surface.html', True, True, True, 1200, 800)
# Float the palette.
palette.dockingState = adsk.core.PaletteDockingStates.PaletteDockStateFloating
# Add handler to HTMLEvent of the palette.
onHTMLEvent = MyHTMLEventHandler()
palette.incomingFromHTML.add(onHTMLEvent)
handlers.append(onHTMLEvent)
# Add handler to CloseEvent of the palette.
onClosed = MyCloseEventHandler()
palette.closed.add(onClosed)
handlers.append(onClosed)
else:
palette.isVisible = True
# The page is already loaded (it won't re-fire its 'ready'
# handshake), so push the current document units now in case the
# active document - and thus its units - changed since last show.
sendUnitsToPalette(palette)
except:
_ui.messageBox('Command executed failed: {}'.format(traceback.format_exc()))
# Event handler for the commandCreated event.
class ShowPaletteCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
command = args.command
onExecute = ShowPaletteCommandExecuteHandler()
command.execute.add(onExecute)
handlers.append(onExecute)
except:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
# Event handler for the commandExecuted event.
class SendInfoCommandExecuteHandler(adsk.core.CommandEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
# Send information to the palette. This will trigger an event in the javascript
# within the html so that it can be handled.
palette = _ui.palettes.itemById('Image2SurfacePalette')
if palette:
palette.sendInfoToHTML('send', 'This is a message sent to the palette from Fusion.')
except:
_ui.messageBox('Command executed failed: {}'.format(traceback.format_exc()))
# Event handler for the commandCreated event.
class SendInfoCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
command = args.command
onExecute = SendInfoCommandExecuteHandler()
command.execute.add(onExecute)
handlers.append(onExecute)
except:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
# Event handler for the palette close event.
class MyCloseEventHandler(adsk.core.UserInterfaceGeneralEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
# _ui.messageBox('Close button is clicked.')
# Do nothing
pass
except:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
# Event handler for the palette HTML event.
class MyHTMLEventHandler(adsk.core.HTMLEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
objFilePath = None
try:
htmlArgs = adsk.core.HTMLEventArgs.cast(args)
data = json.loads(htmlArgs.data) if htmlArgs.data else {}
global _app
# The page sends exactly two kinds of message: the load/units
# handshake (no 'obj' key) and a generate (carrying 'obj'). We key
# off the payload SHAPE rather than the action string, because the
# action is not surfaced reliably through
# neutronJavaScriptObject.executeQuery (it can arrive empty), which
# otherwise made the handshake look like a failed generate and
# popped a spurious "Failed to generate mesh OBJ" dialog at startup.
#
# Handshake: reply with the document units and return. The units are
# delivered BOTH ways (mirrors the working Voronoi add-in): pushed
# async via sendInfoToHTML, and returned synchronously here as
# returnData (the JS captures the send's return value too).
if 'obj' not in data:
htmlArgs.returnData = sendUnitsToPalette(_ui.palettes.itemById('Image2SurfacePalette'))
return
objStr = data.get('obj') or ''
objStrLen = len(objStr)
if objStrLen <= 0:
_ui.messageBox('Failed to generate mesh OBJ')
return
# Get the current document, otherwise create a new one.
doc = _app.activeDocument
if not doc:
doc = _app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
# 4.4: Make sure we actually have a valid, parametric design to
# import into. The user may have closed all documents between
# clicking "Generate Surface" and this callback firing.
design = adsk.fusion.Design.cast(_app.activeProduct)
if not design:
_ui.messageBox('No active Fusion design was found.\n\nPlease open or create a design, then try again.')
return
if design.designType != adsk.fusion.DesignTypes.ParametricDesignType:
_ui.messageBox('The "Image2Surface" command must be run in parametric modeling mode.\n\nPlease enable "Capture design history" for your document.')
return
# Write the OBJ out to a temp file for import.
fp = tempfile.NamedTemporaryFile(mode='w', suffix='.obj', delete=False)
fp.writelines(objStr)
fp.close()
objFilePath = fp.name
print ("Generated OBJ File: " + objFilePath)
# Get the root component of the active design.
rootComp = design.rootComponent
# Need to place the mesh in a BaseFeature (non-parametric)
baseFeats = rootComp.features.baseFeatures
baseFeat = baseFeats.add()
baseFeat.startEdit()
# Add a mesh body by importing this data (OBJ) file. The OBJ is
# authored numerically in the document's units (the JS uses the unit
# we pushed via 'units'), so import with the matching MeshUnits.
meshList = rootComp.meshBodies.add(objFilePath, meshUnitFor(documentLengthUnit()), baseFeat)
# Need to finish the base feature edit
baseFeat.finishEdit()
if meshList.count > 0:
# Success - close palette
palette = _ui.palettes.itemById('Image2SurfacePalette')
if palette:
palette.isVisible = False
# HACK: bug causes mesh to be placed away from origin
# therefore zoom to fit so mesh appears to user
vp = _app.activeViewport
vp.fit()
htmlArgs.returnData = 'OK'
else:
_ui.messageBox('Failed to generate mesh body from file: {}'.format(objFilePath))
except:
# 4.2: Surface the failure in the Fusion UI (e.g. malformed OBJ or a
# face-count limit), not just to stderr where the user never sees it.
if _ui:
_ui.messageBox('Failed to import the generated surface:\n{}'.format(traceback.format_exc()))
finally:
# 4.3: Always clean up the temp OBJ file, whether the import
# succeeded or raised.
if objFilePath:
try:
os.remove(objFilePath)
except OSError:
pass
def run(context):
try:
global _ui, _app
_app = adsk.core.Application.get()
_ui = _app.userInterface
# Add a command that displays the panel.
showPaletteCmdDef = _ui.commandDefinitions.itemById('showImage2SurfacePalette')
if not showPaletteCmdDef:
showPaletteCmdDef = _ui.commandDefinitions.addButtonDefinition('showImage2SurfacePalette', 'Show Image 2 Surface', '', './/Resources//image2surface')
showPaletteCmdDef.toolClipFilename = './/Resources//image2surface//image2surface-tooltip.png'
# Connect to Command Created event.
onCommandCreated = ShowPaletteCommandCreatedHandler()
showPaletteCmdDef.commandCreated.add(onCommandCreated)
handlers.append(onCommandCreated)
# Get the CREATE panel in the MODEL workspace.
createPanel = _ui.allToolbarPanels.itemById("SolidCreatePanel")
# Add button to the panel
btnControl = createPanel.controls.itemById('showImage2SurfacePalette')
if not btnControl:
btnControl = createPanel.controls.addCommand(showPaletteCmdDef)
# Make the button available in the panel.
btnControl.isPromotedByDefault = True
btnControl.isPromoted = True
if context['IsApplicationStartup'] is False:
_ui.messageBox('The "Image2Surface" command has been added\nto the SOLID->CREATE panel dropdown of the DESIGN workspace.\n\nTo run the command, select the SOLID->CREATE dropdown\nthen select "Show Image 2 Surface".')
except Exception:
#pass
if _ui:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
def stop(context):
try:
# Delete the palette created by this add-in.
palette = _ui.palettes.itemById('Image2SurfacePalette')
if palette:
palette.deleteMe()
# Delete controls and associated command definitions created by this add-ins
panel = _ui.allToolbarPanels.itemById('SolidScriptsAddinsPanel')
cmd = panel.controls.itemById('showImage2SurfacePalette')
if cmd:
cmd.deleteMe()
cmdDef = _ui.commandDefinitions.itemById('showImage2SurfacePalette')
if cmdDef:
cmdDef.deleteMe()
except:
if _ui:
_ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))