Workaround to insert high quality 3D view in TechDraw

I don’t know where to put this, but I created a Macro that inserts the 3D view in TechDraw without looking funny or requiring that the 3D window is active. This makes the designs easier to understand for the technicians at the workshop where I work. References (hard to look for in the forums because “Active View” and “TechDraw” are too common terms):

Looking funny: https://devtalk.freecad.org/t/why-does-the-active-view-render-in-techdraw-look-funny/58541/4
Active window requirement: https://devtalk.freecad.org/t/techdraw-how-to-add-rendered-image/40337/1
Before this Macro, I also tried taking a snasphot and inserting it as a bitmap, but not only that makes the background not transparent as it is inserted as a link with absolute path, so it is lost once you move or share the file. Also, changing the scale is cumbersome when it is inserted as a bitmap due to the crop feature.

Usage:

  1. set the temporary directory in the text editor. In linux, /dev/shm/ is a temp dir on RAM, so it avoids disk IO
  2. set the maximum dimension of the screenshot. Default is 100mm. The smaller the 3D objects and the larger this number, the better the picture quality but the larger the memory footprint during processing
  3. hide all objects you don’t want in the picture
  4. select all TechDraw pages where you want to insert the picture
  5. run the macro

For now, it ignores anything that you selected which is not a TechDraw page. I tried to make it more like the regular TechDraw view usage, where you select a set of objects and a page and it inserts just that set, but I could not figure out how to determine which other objects must be hidden or must not be hidden when there are many links and linkgroups in the object tree.

Observations:

  1. the function saveImage(), which takes the snapshot of the Active View, necessarily writes to a file pointer, not to an object in memory like BytesIO. So the only way I could find to avoid unnecessary disk IO was to write to a file in a RAM disk, which is easy to do in Linux and I have no clue of how to do in Windows. The subsequent images resulting from post-processing steps are all within the python scope, no disk IO
  2. it is possible to include metatags in the SVG container, so information like the camera setting, the labels of the objects which need to be visible in order to generate this snapshot and hashes to identify if these objects changed can be included. This could be a way to make the pictures self-update when needed

Here is the code:

#### xraf32_view3DTD: take a snapshot of the 3D view and embed it in one or more
#### TechDraw pages as a SVG symbol
#### Author: xraf32
#### usage: hide all unwanted 3D objects, set the desired camera angle, select all the 
#### TechDraw pages where a copy of the snapshot is desired and run the Macro

## temporary directory: use '/dev/shm' to write to a RAM file or '/tmp' to write to a disk file. 
## most modern linux systems have both options. The first avoids unnecessary disk IO
tmpDir = "/dev/shm/"
#tmpDir = "/tmp/"

MaxDim = 100 # maximum dimension of the image in mm

from PySide import QtGui
import os

def checkTmpDir():
	global tmpDir
	tmpFileLabel = App.ActiveDocument.Uid + '_3Dsnap'
	tmpFilePath = tmpDir + tmpFileLabel + '.png'
	try:
		f = open(tmpFilePath, 'w')
		f.close()
		os.remove(tmpFilePath)
		return (tmpFileLabel, '.png')
	except Exception as e:
		return (tmpFileLabel, str(e))

def filterSelectionTD():
	sel = Gui.Selection.getSelection()
	selTD = []
	for i in sel:
		if type(i).__name__ == 'DrawPage':
			selTD.append(i)
	return (sel, selTD)

def activateView3D(): # if there is no available View3DInventor, creates one and returns true
	mw=Gui.getMainWindow()
	views=mw.findChildren(QtGui.QMainWindow)
	closeActiveView = False

	view3D = None
	for i in views:
		if i.metaObject().className() == 'Gui::View3DInventor':
			view3D = i
			break
	if view3D is None:
		Gui.runCommand("Std_ViewCreate")
		mw=Gui.getMainWindow()
		views=mw.findChildren(QtGui.QMainWindow)
		view3D = views[-1]
		closeActiveView = True

	view3D.setFocus()
	return closeActiveView

def error(id=0, msg=''):
	errStr = 'ERROR {:3}: '.format(id)
	match id:
		case 0: id = 0
		case 1: print(errStr + "cannot create temporary file at the directory: " + msg)
		case 2: print(errStr + "no TechDraw pages were selected")
		case 3: print(errStr + "no objects which appear in the 3D view were selected")
	return id

def cropTransparent(imgPath):
	## taken from: https://gist.github.com/odyniec/3470977
	from PIL import Image
	img = Image.open(imgPath) # reopen the image as PIL Image object
	bbox = img.getbbox() # get the bounding box of the non-transparent pixels
	img = img.crop(bbox) # crop according to that bounding box
	(width, height) = img.size # get cropped image dimensions
	img2 = Image.new("RGBA", (width, height), (0,0,0,0)) # create new image with these dimensions
	img2.paste(img, (0, 0)) # paste the cropped image in this new image
	# if the non-TechDraw selected objects are NOT geometry, then the 3D view will be empty
	# and then the snapshot will be fully transparent. So, we get the extreme values of all 4 channels
	# and check if all values are zero
	extrema = img2.getextrema()
	extremasum = 0
	for i in extrema:
		extremasum += i[0] + i[1]
	if extremasum == 0:
		return (0, 0, None)
	## save binary image in memory
	## taken from: https://jdhao.github.io/2019/07/06/python_opencv_pil_image_to_bytes/
	from io import BytesIO
	buf = BytesIO()
	img2.save(buf, format='PNG')
	binimg = buf.getvalue()
	# print(str(width) + " ; " + str(height))
	return (width, height, binimg)

def wrapImageInSVG(_width, _height, _binimg):
	## taken from: https://softwarerecs.stackexchange.com/questions/76954/how-can-i-convert-an-svg-with-linked-images-to-embed-those-images-inside-the-svg
	from base64 import b64encode
	widthmm = MaxDim
	heightmm = MaxDim
	if _width > _height:
		heightmm = MaxDim * 1.0 / _width * _height
	elif _height > _width:
		widthmm = MaxDim * 1.0 / _height * _width

	strw = str(widthmm)
	strh = str(heightmm)
	encoded_string = 'data:image/png;base64,' + str(b64encode(_binimg), 'utf-8')

	SVGdata = '<?xml version="1.0" encoding="UTF-8"?>\n' + \
		'<svg version="1.2" width="' + strw + 'mm" height="' + strh + 'mm"\n' + \
		'  viewBox="0 0 ' + strw + ' ' + strh + '"\n' + \
		'  xmlns="http://www.w3.org/2000/svg">\n' + \
		'  <image x="0" y="0" width="' + strw + '" height="' + strh + '" xlink:href="' + encoded_string + '"/>\n' + \
		'</svg>'
	return SVGdata

def insertSVGIntoTDPages(_SVGdata, _selTD):
	for i in _selTD:
		sym = App.ActiveDocument.addObject('TechDraw::DrawViewSymbol', i.Label + '_3D')
		sym.Symbol = _SVGdata
		i.addView(sym)

def restoreConfig(_b_sel, _b_av, _b_cam, _closeActiveView):
	# selection
	Gui.Selection.clearSelection()
	for i in _b_sel:
		Gui.Selection.addSelection(i)
	if _closeActiveView: # the 3D view was created just for the snapshot, so destroy it
		Gui.runCommand("Std_CloseActiveWindow")
	else: # the 3D view was not created just for the snapshot, it was already there. So restore its camera
		Gui.activeDocument().activeView().setCamera(_b_cam)
	# QT call to restore the previous ActiveView
	_b_av.setFocus()

def mainProcedure(): # b_var is backup_variable
	global tmpDir
	global MaxDim

	tmpFileLabel, tmpFileExt = checkTmpDir()
	if tmpFileExt != '.png': # error 1: bad temporary file path
		return error(1, tmpDir + '\nTrace message: ' + tmpFileExt)
	tmpFilePath = tmpDir + tmpFileLabel + tmpFileExt
	
	b_sel, selTD = filterSelectionTD()

	if  len(selTD) == 0: # error 2: no TechDraw pages in selection
		return error(2)

	b_av = Gui.getMainWindow().centralWidget().activeSubWindow().widget() # backup active view
	closeActiveView = activateView3D() # ensure the ActiveView is a View3DInventor
	av = Gui.activeDocument().activeView()
	b_cam = av.getCamera() # backup its camera position
	Gui.SendMsgToActiveView('ViewFit') # fit camera to the visible objects
	# selection was already backed-up in b_sel by splitSelection()
	# clear selection to prevent colour distortion in the snapshot
	Gui.Selection.clearSelection()
	av.saveImage(tmpFilePath, MaxDim*20, MaxDim*20, "Transparent") # snapshot
	w, h, binimg = cropTransparent(tmpFilePath) # get binary image with cropped width and height
	if w + h == 0: # error 3: empty 3D view
		return error(3)
	# wrap the binary image inside a virtual SVG and insert it as a TechDraw symbol. 
	# Advantages over bitmaps:
	#   1. Avoid broken links, since symbols are embedded inside the document
	#   2. No crop feature, so no need to adjust canvas dimensions when changing the scale
	#   3. Reduced disk IO and/or system call footprints
	#   4. Simple to include additional vector or raster graphics in the future, or add metatags
	SVGdata = wrapImageInSVG(w, h, binimg)
	insertSVGIntoTDPages(SVGdata, selTD)
	restoreConfig(b_sel, b_av, b_cam, closeActiveView)

mainProcedure()

Thank you , usefull macro . But the Match instruction is too hight Level . I think it’s a 3.10 Python instruction and in my case even if I use a 0.21. Release of FreeCad My embeded Python Release is a v3.8.10.

So this Macro is nor compatible need to get back to an ugly :

def error(id=0, msg=''):
	errStr = "ERROR {:3}: ".format(id)

	if id == 0: 
		id = 0
	elif id == 1: 
		print(errStr + "cannot create temporary file at the directory: " + msg)
	elif id == 2: 
		print(errStr + "no TechDraw pages were selected")
	elif id == 3: 
		print(errStr + "no objects which appear in the 3D view were selected")
	
	return id

FYI, in v0.21 the ActiveView is no longer funny looking.

This is great news! Looking forward for it to become stable.

Strange. Here it says the embedded python version is 3.10.8. It is the 0.20.2 AppImage, in Linux. Your temporary directory suggests you are using Windows. Aren’t the python versions all the same across the binaries of the same release for different platforms?

Yes there are two installation files for Windows , one with 3.8 Python version and one with the 3.10. Don’t ask me why.

I like your macro and have been using it for a few months. However, it fails when a 2nd file is open. To reproduce just hit the “New” button to create a new and empty file. Now the macro complains “ERROR 3: no objects which appear in the 3D view were selected” and the image is not inserted. Let me know if you need more detailed steps.

0.20.2