From 137b73fea1c8805cd37f9d1af8e79f693ff3d422 Mon Sep 17 00:00:00 2001 From: Youen Date: Mon, 26 Dec 2022 10:19:00 +0100 Subject: [PATCH] refactored and improved view rasterization system (wip) --- ahb_cmd_view_annotate.py | 6 +- ahb_raster_view.py | 147 +++++++++++++++++++++++++++++++ ahb_techdraw_extensions.py | 174 ++++++++++++++++--------------------- 3 files changed, 226 insertions(+), 101 deletions(-) create mode 100644 ahb_raster_view.py diff --git a/ahb_cmd_view_annotate.py b/ahb_cmd_view_annotate.py index 36607ad..6157a9f 100644 --- a/ahb_cmd_view_annotate.py +++ b/ahb_cmd_view_annotate.py @@ -50,6 +50,10 @@ class AHB_View_Annotate: if obj.SourceView != view: continue balloon = obj + overlay_view = doc.getObject(view.Name + '_overlay') + if overlay_view is None: + overlay_view = view + # Create a new balloon if needed if balloon is None: partName = partLink.Name @@ -57,7 +61,7 @@ class AHB_View_Annotate: balloonName = partName + "_Balloon" balloon = doc.addObject("TechDraw::DrawViewBalloon", balloonName) - balloon.SourceView = view + balloon.SourceView = overlay_view balloon.addProperty("App::PropertyXLink", "Assembly_handbook_Source", "Assembly_handbook") balloon.Assembly_handbook_Source = (partLink, partLink.Name) diff --git a/ahb_raster_view.py b/ahb_raster_view.py new file mode 100644 index 0000000..cd7539a --- /dev/null +++ b/ahb_raster_view.py @@ -0,0 +1,147 @@ +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(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.init_image_projection() + + def init_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 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 render(self): + from pivy import coin + import os + from PIL import Image, ImageDraw, ImageChops + + 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) + + doc = App.newDocument('tmp_raster', hidden=False, temp=False) + for part in view.XSource: + link = doc.addObject('App::Link', part.Name) + link.Label = part.Label + if part.TypeId == 'App::Link': + link.LinkedObject = part.LinkedObject + link.Placement = part.Placement + else: + link.LinkedObject = part + + docView = Gui.getDocument(doc.Name).mdiViewsOfType('Gui::View3DInventor')[0] + + cam = docView.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) + + docView.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() + + sb_offset = viewVolume.projectToScreen(coin.SbVec3f(0,0,0)) + self.image_view.Assembly_handbook_ViewVolumeOffset = App.Vector(sb_offset[0], sb_offset[1], sb_offset[2]) + + self.init_image_projection() + + docView.saveImage(self.image_file_name, 4096, 4096, "#ffffff") + + with Image.open(self.image_file_name) as img: + original_size = img.size + + p2dA = self.project3DPointToImageView(App.Vector(0,0,0)) + p2dB = self.project3DPointToImageView(view.XDirection) + imageScale = view.Scale / (p2dB.x - p2dA.x) / original_size[0] * 10 + #print('imageScale', imageScale) + + bg = Image.new(img.mode, img.size, '#ffffff') # fills an image with the background color + diff = ImageChops.difference(img, bg) # diff between the actual image and the background color + bbox = diff.getbbox() # finds border size (non-black portion of the image) + print(bbox) + #image_center = (bbox[0] + (bbox[2] - bbox[0])/2 - img.size[0]/2, bbox[1] + (bbox[3] - bbox[1])/2 - img.size[1]/2) + #print(image_center) + img = img.crop(bbox) + + draw = ImageDraw.Draw(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)) + + img.save(self.image_file_name) + + App.closeDocument(doc.Name) + + image = self.image_view + image.ImageFile = "" + image.Scale = imageScale + 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() diff --git a/ahb_techdraw_extensions.py b/ahb_techdraw_extensions.py index 1d570fc..e763b1d 100644 --- a/ahb_techdraw_extensions.py +++ b/ahb_techdraw_extensions.py @@ -84,6 +84,8 @@ class TechDrawExtensions: QTimer.singleShot(10, self._do_repaint) def _do_repaint(self): + from ahb_raster_view import RasterView + workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench selection = Gui.Selection.getSelection() @@ -92,6 +94,8 @@ class TechDrawExtensions: self.views_to_repaint = {} for view in to_repaint: + if '_overlay' in view.Label: + continue #print("Repainting " + view.Name) page = self.getViewPage(view) @@ -103,31 +107,82 @@ class TechDrawExtensions: if not 'Assembly_handbook_RasterView' in view.PropertiesList: view.addProperty("App::PropertyBool", "Assembly_handbook_RasterView", "Assembly_handbook") + view.Assembly_handbook_RasterView = True if view.Assembly_handbook_RasterView: print("Rasterizing view " + view.Label + "...") - imageName = view.Label + "_raster" - image = doc.getObject(imageName) - if image is None: - image = doc.addObject('TechDraw::DrawViewImage', view.Label + "_raster") - if not image in page.Views: - page.addView(image) + raster_view = RasterView(view) + raster_view.render() + + view.Visibility = False + overlayName = view.Label + "_overlay" + overlay = doc.getObject(overlayName) + if overlay is None: + overlay = doc.addObject('TechDraw::DrawViewPart', overlayName) + page.addView(overlay) + + overlay_frame_name = view.Label + "_frame" + overlay_frame = doc.getObject(overlay_frame_name) + + if overlay_frame is None: + import Draft + ''' + points = [App.Vector(0.0, 0.0, 0.0), App.Vector(1.01, 0, 0)] + obj1 = Draft.makeWire(points, closed=False, face=False, support=None) + + points = [App.Vector(200.0, 0.0, 0.0), App.Vector(201.01, 0, 0)] + obj2 = Draft.makeWire(points, closed=False, face=False, support=None) + + #overlay_frame = Draft.upgrade([], delete=True) + #overlay_frame.Label = overlay_frame_name + overlay_frame = App.ActiveDocument.addObject("Part::Part2DObjectPython", overlay_frame_name) + Draft.Block(overlay_frame) + overlay_frame.Components = [obj1, obj2] + Draft.ViewProviderDraftPart(overlay_frame.ViewObject) + doc.removeObject(obj1.Name) + doc.removeObject(obj2.Name)''' + + #overlay_frame = Draft.makeWire(points, closed=False, face=False, support=None) + overlay_frame = App.ActiveDocument.addObject("Part::Part2DObjectPython", overlay_frame_name) + Draft.Wire(overlay_frame) + pos = raster_view.projectImageViewPointTo3D(App.Vector(0,0,0)) + pos2 = raster_view.projectImageViewPointTo3D(App.Vector(0,0.0001,1)) + overlay_frame.Points = [pos, pos2] + Draft.ViewProviderWire(overlay_frame.ViewObject) + overlay_frame.recompute() + + overlay_frame2_name = view.Label + "_frame2" + overlay_frame2 = doc.getObject(overlay_frame2_name) + if overlay_frame2 is None: + overlay_frame2 = App.ActiveDocument.addObject("Part::Part2DObjectPython", overlay_frame2_name) + Draft.Wire(overlay_frame2) + pos = raster_view.projectImageViewPointTo3D(App.Vector(1,1,0)) + pos2 = raster_view.projectImageViewPointTo3D(App.Vector(1,1.0001,1)) + overlay_frame2.Points = [pos, pos2] + Draft.ViewProviderWire(overlay_frame2.ViewObject) + overlay_frame2.recompute() - new_views_list = page.Views - new_views_list.remove(image) - view_idx = new_views_list.index(view) - new_views_list.insert(view_idx, image) - page.Views = new_views_list + overlay.Source = [overlay_frame, overlay_frame2] - image_file_name = doc.FileName.replace('.FCStd', '') + '_raster/' + view.Name + '.png' - image_scale = self.rasterizeView(view, image_file_name) - image.ImageFile = "" - image.Scale = image_scale - image.X = view.X #- image_center[0]/10.0*image_scale - image.Y = view.Y #+ image_center[1]/10.0*image_scale - image.ImageFile = image_file_name # TODO: see if it's possible to set a relative path - image.recompute() + overlay.X = view.X + overlay.Y = view.Y + overlay.Direction = view.Direction + overlay.XDirection = view.XDirection + overlay.ScaleType = view.ScaleType + overlay.Scale = view.Scale + overlay.ViewObject.LineWidth = 0.01 + + for balloon in page.Views: + if balloon.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_Source" in balloon.PropertiesList and balloon.SourceView == view: + if balloon.SourceView == view: + balloon.SourceView = overlay + page.addView(balloon) + balloon.recompute() + + page.KeepUpdated = True # once we have hidden the source view, there should be no performance issue + overlay.recompute() + page.recompute() else: fast_rendering = False #try: @@ -231,87 +286,6 @@ class TechDrawExtensions: view.requestPaint() - def rasterizeView(self, view, image_file_name): - from pivy import coin - import os - from PIL import Image, ImageDraw, ImageChops - - print('Rasterizing ' + view.Label + " to " + image_file_name + "...") - - dir = os.path.dirname(image_file_name) - if not os.path.exists(dir): - os.makedirs(dir) - - doc = App.newDocument('tmp_raster', hidden=False, temp=False) - for part in view.XSource: - link = doc.addObject('App::Link', part.Name) - link.Label = part.Label - if part.TypeId == 'App::Link': - link.LinkedObject = part.LinkedObject - link.Placement = part.Placement - else: - link.LinkedObject = part - - docView = Gui.getDocument(doc.Name).mdiViewsOfType('Gui::View3DInventor')[0] - - cam = docView.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) - - docView.fitAll() - - viewVolume = cam.getViewVolume(0.0) - - docView.saveImage(image_file_name, 4096, 4096, "#ffffff") - - def project3dPointToViewport(p3d): - YDirection = view.Direction.cross(view.XDirection) - sb_offset = viewVolume.projectToScreen(coin.SbVec3f(0,0,0)) - offset = App.Vector(sb_offset[0], sb_offset[1], 0) - #print('offset', offset) - p2d = App.Vector(view.XDirection.dot(p3d)/viewVolume.getWidth() + offset.x, YDirection.dot(p3d)/viewVolume.getHeight() + offset.y, 0) - return p2d - - p2dA = project3dPointToViewport(App.Vector(0,0,0)) - p2dB = project3dPointToViewport(view.XDirection) - # page_mm = (p2dB.x - p2dA.x) * resX / 100 * PNGscale = viewScale - imageScale = view.Scale / (p2dB.x - p2dA.x) / 4096.0 * 10 - print('imageScale', imageScale) - - with Image.open(image_file_name) as img: - original_size = img.size - - bg = Image.new(img.mode, img.size, '#ffffff') # fills an image with the background color - diff = ImageChops.difference(img, bg) # diff between the actual image and the background color - bbox = diff.getbbox() # finds border size (non-black portion of the image) - print(bbox) - #image_center = (bbox[0] + (bbox[2] - bbox[0])/2 - img.size[0]/2, bbox[1] + (bbox[3] - bbox[1])/2 - img.size[1]/2) - #print(image_center) - img = img.crop(bbox) - - draw = ImageDraw.Draw(img) - - def debugPoint(p3d): - p2d = project3dPointToViewport(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)) - - img.save(image_file_name) - - App.closeDocument(doc.Name) - - return imageScale - def updateBalloonCursor(self, view): selected_balloons = [] for obj in Gui.Selection.getSelection():