import FreeCAD as App import FreeCADGui as Gui class RasterView: def __init__(self, view): self.source_view = view doc = view.Document self.image_file_name = doc.FileName.replace('.FCStd', '') + '_raster/' + view.Name + '.png' def init_image_projection(self): doc = self.source_view.Document image_name = self.source_view.Label + "_raster" image = doc.getObject(image_name) if image is None: return False self.image_view = image if image.Assembly_handbook_ViewVolumeWidth > 0: self._precompute_image_projection() return True return False def init_image(self): workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench doc = self.source_view.Document page = workbench.techDrawExtensions.getViewPage(self.source_view) image_name = self.source_view.Label + "_raster" image = doc.getObject(image_name) if image is None: image = doc.addObject('TechDraw::DrawViewImage', image_name) image.addProperty("App::PropertyFloat", "Assembly_handbook_ViewVolumeWidth", "Assembly_handbook") image.addProperty("App::PropertyFloat", "Assembly_handbook_ViewVolumeHeight", "Assembly_handbook") image.addProperty("App::PropertyFloat", "Assembly_handbook_ViewVolumeDepth", "Assembly_handbook") image.addProperty("App::PropertyVector", "Assembly_handbook_ViewVolumeOffset", "Assembly_handbook") if not image in page.Views: page.addView(image) new_views_list = page.Views new_views_list.remove(image) view_idx = new_views_list.index(self.source_view) new_views_list.insert(view_idx, image) page.Views = new_views_list self.image_view = image if image.Assembly_handbook_ViewVolumeWidth > 0: self._precompute_image_projection() def _precompute_image_projection(self): YDirection = self.source_view.Direction.cross(self.source_view.XDirection) self.image_x_dir = self.source_view.XDirection / self.image_view.Assembly_handbook_ViewVolumeWidth self.image_y_dir = YDirection / self.image_view.Assembly_handbook_ViewVolumeHeight self.image_z_dir = self.source_view.Direction / self.image_view.Assembly_handbook_ViewVolumeDepth self.image_x_dir_inv = self.source_view.XDirection * self.image_view.Assembly_handbook_ViewVolumeWidth self.image_y_dir_inv = YDirection * self.image_view.Assembly_handbook_ViewVolumeHeight self.image_z_dir_inv = self.source_view.Direction * self.image_view.Assembly_handbook_ViewVolumeDepth def project3DPointToImageView(self, point3d): offset = self.image_view.Assembly_handbook_ViewVolumeOffset return App.Vector(self.image_x_dir.dot(point3d) + offset.x, self.image_y_dir.dot(point3d) + offset.y, self.image_z_dir.dot(point3d) + offset.z) def project3DPointToSourceView(self, point3d): offset = self.image_view.Assembly_handbook_ViewVolumeOffset offset = App.Vector((offset.x-0.5) * self.image_view.Assembly_handbook_ViewVolumeWidth, (offset.y-0.5) * self.image_view.Assembly_handbook_ViewVolumeHeight, (offset.z-0.5) * self.image_view.Assembly_handbook_ViewVolumeDepth) #image_view_point = App.Vector(self.image_x_dir.dot(point3d), self.image_y_dir.dot(point3d), self.image_z_dir.dot(point3d)) #return App.Vector(image_view_point.x * self.image_view.Assembly_handbook_ViewVolumeWidth, image_view_point.y * self.image_view.Assembly_handbook_ViewVolumeHeight, 0) YDirection = self.source_view.Direction.cross(self.source_view.XDirection) return App.Vector(self.source_view.XDirection.dot(point3d) + offset.x, YDirection.dot(point3d) + offset.y, self.image_z_dir.dot(point3d) + offset.z) def projectImageViewPointTo3D(self, point2d): offset = self.image_view.Assembly_handbook_ViewVolumeOffset p = point2d - offset return self.image_x_dir_inv * p.x + self.image_y_dir_inv * p.y + self.image_z_dir_inv * p.z def _flatten_objects_tree(self, obj_list): result = [] for obj in obj_list: if obj.TypeId == 'Part::FeaturePython' and hasattr(obj, 'LinkedObject'): # variant link result.extend(self._flatten_objects_tree(obj.Group)) elif obj.TypeId in ['App::Link']: result.extend(self._flatten_objects_tree([obj.LinkedObject])) elif obj.TypeId in ['App::Part', 'App::DocumentObjectGroup']: result.extend(self._flatten_objects_tree(obj.Group)) elif obj.TypeId in ['Part::Feature', 'Part::FeaturePython', 'PartDesign::Body', 'PartDesign::CoordinateSystem', 'PartDesign::Line', 'Part::Mirroring', 'Part::Cut', 'Part::Part2DObjectPython']: result.append(obj) if hasattr(obj, 'Group'): result.extend(self._flatten_objects_tree(obj.Group)) return result def _should_render(self, obj): return obj.TypeId in ['Part::Feature', 'Part::FeaturePython', 'PartDesign::Body', 'Part::Mirroring', 'Part::Cut', 'Part::Part2DObjectPython'] def render(self): from pivy import coin import os from PIL import Image, ImageDraw, ImageChops import Part Image.MAX_IMAGE_PIXELS = 9999999999 # allow very high resolution images workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench view = self.source_view self.init_image() print('Rasterizing ' + view.Label + " to " + self.image_file_name + "...") dir = os.path.dirname(self.image_file_name) if not os.path.exists(dir): os.makedirs(dir) tmp_doc = App.newDocument('tmp_raster', hidden=False, temp=False) objects_to_reset = {} duplicated_parts = {} try: # construct new scene with links to the parts we want prev_parts = [] new_parts = [] for part in view.XSource: link = tmp_doc.addObject('App::Link', part.Name) link.Label = part.Label if part.TypeId == 'App::Link': link.LinkedObject = part.LinkedObject link.Placement = part.Placement elif part.TypeId == 'Part::FeaturePython' and hasattr(part, 'LinkedObject'): # variant link link.LinkedObject = part.LinkedObject link.Placement = part.Placement else: link.LinkedObject = part is_new_part = workbench.techDrawExtensions.isNewPartInView(view, part) is_conflicting = False if link.LinkedObject in duplicated_parts.keys(): link.LinkedObject = duplicated_parts[link.LinkedObject] else: other_parts = prev_parts if is_new_part else new_parts for other_part in other_parts: other_objects = self._flatten_objects_tree([other_part]) for obj in self._flatten_objects_tree([link]): if obj in other_objects: is_conflicting = True if is_conflicting: # We must copy the part because otherwise we can't control the emissive color (link material override does not work for emissive color) #print("conflict: " + link.LinkedObject.Document.Name + '#' + link.LinkedObject.Label) shape_copy = Part.getShape(link.LinkedObject,'',needSubElement=False,refine=False) part_copy = tmp_doc.addObject('Part::Feature','ShapeCopy') part_copy.Shape = shape_copy part_copy.Label = part.Label duplicated_parts[link.LinkedObject] = part_copy link.LinkedObject = part_copy if is_new_part: new_parts.append(link) else: prev_parts.append(link) # hide objects that we don't want to display ; also make a backup of properties we want to reset after we're done for obj in self._flatten_objects_tree([link]): if obj in objects_to_reset.keys(): continue if self._should_render(obj): objects_to_reset[obj] = ( obj.ViewObject.Visibility, obj.ViewObject.LineColor, obj.ViewObject.ShapeMaterial.AmbientColor, obj.ViewObject.ShapeMaterial.DiffuseColor, obj.ViewObject.ShapeMaterial.SpecularColor, obj.ViewObject.ShapeMaterial.EmissiveColor, obj.ViewObject.LineWidth, obj.ViewObject.DisplayMode ) else: objects_to_reset[obj] = ( obj.ViewObject.Visibility, ) obj.ViewObject.Visibility = False tmp_doc_view = Gui.getDocument(tmp_doc.Name).mdiViewsOfType('Gui::View3DInventor')[0] cam = tmp_doc_view.getCameraNode() rot = coin.SbRotation(coin.SbVec3f(1,0,0), coin.SbVec3f(view.XDirection.x,view.XDirection.y,view.XDirection.z)) rot *= coin.SbRotation(coin.SbVec3f(0,0,1), coin.SbVec3f(view.Direction.x,view.Direction.y,view.Direction.z)) cam.orientation.setValue(rot) tmp_doc_view.fitAll() viewVolume = cam.getViewVolume(0.0) self.image_view.Assembly_handbook_ViewVolumeWidth = viewVolume.getWidth() self.image_view.Assembly_handbook_ViewVolumeHeight = viewVolume.getHeight() self.image_view.Assembly_handbook_ViewVolumeDepth = viewVolume.getDepth() max_res = 3200 # todo: keep aspect ratio when we limit max image dimensions #max_res = 1500 resolution = [ int(viewVolume.getWidth() * view.Scale * 10), int(viewVolume.getHeight() * view.Scale * 10) ] if resolution[0] > max_res: resolution[1] = int(resolution[1] * max_res / resolution[0]) resolution[0] = int(max_res) if resolution[1] > max_res: resolution[0] = int(resolution[0] * max_res / resolution[1]) resolution[1] = int(max_res) # render old parts in gray lines prev_parts_img = self._render_lines(tmp_doc, resolution, prev_parts, (0.6, 0.6, 0.6), []) # render new parts in black lines (old parts can mask them) new_parts_img = self._render_lines(tmp_doc, resolution, new_parts, (0.0, 0.0, 0.0), prev_parts) # create the composite image composite_img = prev_parts_img.copy() composite_img.paste(new_parts_img, None, new_parts_img) #composite_img = new_parts_img.copy() finally: # restore properties on objects we have modified for obj, props in objects_to_reset.items(): obj.ViewObject.Visibility = props[0] if self._should_render(obj): obj.ViewObject.LineColor = props[1] obj.ViewObject.ShapeMaterial.AmbientColor = props[2] obj.ViewObject.ShapeMaterial.DiffuseColor = props[3] obj.ViewObject.ShapeMaterial.SpecularColor = props[4] obj.ViewObject.ShapeMaterial.EmissiveColor = props[5] obj.ViewObject.LineWidth = props[6] obj.ViewObject.DisplayMode = props[7] # remove the temporary document App.closeDocument(tmp_doc.Name) # Crop the image, which is also used to deduce the center of the source view original_size = composite_img.size bg = Image.new(composite_img.mode, composite_img.size, '#ffffff') # fills an image with the background color diff = ImageChops.difference(composite_img, bg) # diff between the actual image and the background color bbox = diff.getbbox() # finds border size (non-black portion of the image) composite_img = composite_img.crop(bbox) '''draw = ImageDraw.Draw(composite_img) def debugPoint(p3d): p2d = self.project3DPointToImageView(p3d) pp = App.Vector(p2d.x * original_size[0] - bbox[0], (1.0-p2d.y) * original_size[1] - bbox[1]) #print('pp', pp) len = 100 draw.line([(pp.x, pp.y-len), (pp.x, pp.y+len)], fill=128, width = 7) draw.line([(pp.x-len, pp.y), (pp.x+len, pp.y)], fill=128, width = 7) debugPoint(App.Vector(-12.5, 37.5, 25.0)) debugPoint(App.Vector(-12.5, -1387.5, 25.0)) debugPoint(App.Vector(131.23702882966705, -655.0000021095163, 145.21130178331268))''' composite_img.save(self.image_file_name) sb_offset = viewVolume.projectToScreen(coin.SbVec3f(0,0,0)) crop_offset = App.Vector(((bbox[0] + bbox[2])/2 - original_size[0]/2)/original_size[0], ((bbox[1] + bbox[3])/2 - original_size[1]/2)/original_size[1], 0) self.image_view.Assembly_handbook_ViewVolumeOffset = App.Vector(sb_offset[0] - crop_offset.x, sb_offset[1] + crop_offset.y, sb_offset[2]) self._precompute_image_projection() p2dA = self.project3DPointToImageView(App.Vector(0,0,0)) p2dB = self.project3DPointToImageView(view.XDirection) image_scale = view.Scale / (p2dB.x - p2dA.x) / original_size[0] * 10 # display the image in the view image = self.image_view image.ImageFile = "" image.Scale = image_scale image.X = view.X image.Y = view.Y image.ImageFile = self.image_file_name # TODO: see if it's possible to set a relative path image.recompute() def _render_lines(self, doc, resolution, parts, line_color, masking_parts): import tempfile from PIL import Image, ImageDraw, ImageFilter doc_view = Gui.getDocument(doc.Name).mdiViewsOfType('Gui::View3DInventor')[0] # render lines in black, background in red, fill shapes in green # the green band contains the lines images, the red band contains the inverted alpha layer for link in doc.findObjects(): if link in parts or link in masking_parts: link.ViewObject.Visibility = True # in current version of freecad, link override material does not allow to override all material properties, for example emissive color, so we have to change material of the linked object for obj in self._flatten_objects_tree([link]): if self._should_render(obj): obj.ViewObject.LineColor = (0.0, 0.0, 0.0, 0.0) if link in parts else (1.0, 0.0, 1.0) obj.ViewObject.ShapeMaterial.AmbientColor = (0.0, 0.0, 0.0, 0.0) obj.ViewObject.ShapeMaterial.DiffuseColor = (0.0, 0.0, 0.0, 0.0) obj.ViewObject.ShapeMaterial.SpecularColor = (0.0, 0.0, 0.0, 0.0) obj.ViewObject.ShapeMaterial.EmissiveColor = (0.0, 1.0, 0.0, 0.0) if link in parts else (1.0, 0.0, 1.0) # We need to set two different values otherwise freecad does not always update LineWidth of sub-elements obj.ViewObject.LineWidth = 1.0 obj.ViewObject.LineWidth = 2.0 else: link.ViewObject.Visibility = False temp_file_name = tempfile.gettempdir() + "/ahb_temp_image.png" doc_view.saveImage(temp_file_name, resolution[0]+2, resolution[1]+2, "#ff0000") # we add 1 pixel border that we will need to crop later lines_bands_img = self._read_image(temp_file_name) lines_bands_img.save('/home/youen/dev/vhelio/tmp/tmp.png') lines_bands = lines_bands_img.split() lines_img = lines_bands[1] alpha_img = lines_bands[0].point(lambda p: 255 - p) # Render all shapes with different colors, in order to extract outlines (where color changes) # This is needed because FreeCAD does not render lines on the boundary of curve shapes, such as spheres or cylinders # The technique could be improved by using the depth buffer instead, in order to detect boundaries within the same object step = 8 r = step g = step b = step for link in doc.findObjects(): if link in parts or link in masking_parts: for obj in self._flatten_objects_tree([link]): if self._should_render(obj) and obj.TypeId != 'Part::Part2DObjectPython': obj.ViewObject.DisplayMode = 'Shaded' obj.ViewObject.ShapeMaterial.AmbientColor = (0.0, 0.0, 0.0, 0.0) obj.ViewObject.ShapeMaterial.DiffuseColor = (0.0, 0.0, 0.0, 0.0) obj.ViewObject.ShapeMaterial.SpecularColor = (0.0, 0.0, 0.0, 0.0) obj.ViewObject.ShapeMaterial.EmissiveColor = (r/255.0, g/255.0, b/255.0, 0.0) if link in parts else (1.0, 1.0, 1.0, 0.0) r = r + step if r >= 256 - step: r = step g = g + step if g >= 256 - step: g = step b = b + step if b >= 256 - step: b = step else: obj.ViewObject.Visibility = False doc_view.saveImage(temp_file_name, (resolution[0]+2)*2, (resolution[1]+2)*2, "#ffffff") # shapes are rendered at twice the resolution for antialiasing shapes_img = self._read_image(temp_file_name) outlines_img = None for x in range(0, 3): for y in range(0, 3): if x == 1 and y == 1: continue kernel = [0, 0, 0, 0, 1, 0, 0, 0, 0] kernel[y * 3 + x] = -1 partial_outlines = shapes_img.filter(ImageFilter.Kernel((3, 3), kernel, 1, 127)) partial_outlines = partial_outlines.point(lambda p: 255 if p == 127 else 0) partial_outlines = partial_outlines.convert("L") partial_outlines = partial_outlines.point(lambda p: 255 if p == 255 else 0) if outlines_img is None: outlines_img = partial_outlines else: outlines_img.paste(partial_outlines, None, partial_outlines.point(lambda p: 0 if p == 255 else 255)) lines_fullres = lines_img.resize(outlines_img.size, Image.NEAREST) lines_fullres.paste(outlines_img, None, outlines_img.point(lambda p: 255 if p == 0 else 0)) #lines_fullres.paste(255, alpha_fullres.point(lambda p: 255 if p == 0 else 0)) all_lines = lines_fullres.resize(lines_img.size, Image.BILINEAR) #all_lines = lines_img.copy() alpha_fullres = alpha_img.resize(outlines_img.size, Image.NEAREST) alpha_fullres.paste(outlines_img.point(lambda p: 255), None, outlines_img.point(lambda p: 255 if p == 0 else 0)) alpha_img = alpha_fullres.resize(all_lines.size, Image.BILINEAR) # colorize final image fill_color = (1.0, 1.0, 1.0) result = Image.merge("RGBA", [ all_lines.point(lambda p: int(fill_color[0] * p + line_color[0] * (255.0 - p))), all_lines.point(lambda p: int(fill_color[1] * p + line_color[1] * (255.0 - p))), all_lines.point(lambda p: int(fill_color[2] * p + line_color[2] * (255.0 - p))), alpha_img ]) # crop 1px borders result = result.crop((1, 1, result.size[0] - 1, result.size[1] - 1)) return result def _read_image(self, file_name): from PIL import Image with Image.open(file_name) as image: return image.copy()