From 3c7bdc2a4cef72aa5c8c8e9b8fec294605d15b7b Mon Sep 17 00:00:00 2001 From: Youen Date: Sat, 31 Dec 2022 13:05:01 +0100 Subject: [PATCH] Added button for faster rendering and fixed problem with overlay not recomputing --- InitGui.py | 7 +- ahb_cmd_view_annotate_detail.py | 4 +- ahb_cmd_view_refresh.py | 4 +- ahb_cmd_view_refresh_fast.py | 22 ++++ ahb_raster_view.py | 204 +++++++++++++++++--------------- ahb_techdraw_extensions.py | 38 ++++-- 6 files changed, 162 insertions(+), 117 deletions(-) create mode 100644 ahb_cmd_view_refresh_fast.py diff --git a/InitGui.py b/InitGui.py index 2e792c9..263872b 100644 --- a/InitGui.py +++ b/InitGui.py @@ -78,8 +78,8 @@ class AssemblyHandbookWorkbench(Gui.Workbench): #self.importModule('ahb_cmd_export_csv') #toolbox.append("AHB_exportCsv") - self.importModule('ahb_cmd_render') - toolbox.append("AHB_render") + #self.importModule('ahb_cmd_render') + #toolbox.append("AHB_render") self.importModule('ahb_cmd_new_step') toolbox.append("AHB_new_step") @@ -98,6 +98,9 @@ class AssemblyHandbookWorkbench(Gui.Workbench): toolbox.append("AHB_view_add_source_parts") toolbox.append("AHB_view_remove_source_parts") + self.importModule('ahb_cmd_view_refresh_fast') + toolbox.append("AHB_view_refresh_fast") + self.importModule('ahb_cmd_view_refresh') toolbox.append("AHB_view_refresh") diff --git a/ahb_cmd_view_annotate_detail.py b/ahb_cmd_view_annotate_detail.py index 0bf5ee9..c619dbd 100644 --- a/ahb_cmd_view_annotate_detail.py +++ b/ahb_cmd_view_annotate_detail.py @@ -3,8 +3,8 @@ import FreeCAD as App class AHB_View_Annotate_Detail: def GetResources(self): - return {"MenuText": "Add annotation details", - "ToolTip": "Annotates each part of a sub-assembly", + return {"MenuText": "Annotate sub-assembly", + "ToolTip": "Annotates each part of selected sub-assembly balloons", "Pixmap": "" } diff --git a/ahb_cmd_view_refresh.py b/ahb_cmd_view_refresh.py index 9f334db..78ae684 100644 --- a/ahb_cmd_view_refresh.py +++ b/ahb_cmd_view_refresh.py @@ -3,7 +3,7 @@ import FreeCAD as App class AHB_RefreshView: def GetResources(self): - return {"MenuText": "Refresh page", + return {"MenuText": "Refresh page (final quality)", "ToolTip": "Redraws the current page", "Pixmap": "" } @@ -16,7 +16,7 @@ class AHB_RefreshView: workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench page = workbench.techDrawExtensions.getActivePage() if page is not None: - workbench.techDrawExtensions.forceRedrawPage(page) + workbench.techDrawExtensions.forceRedrawPage(page, fast_render = False) from ahb_command import AHB_CommandWrapper AHB_CommandWrapper.addGuiCommand('AHB_view_refresh', AHB_RefreshView()) diff --git a/ahb_cmd_view_refresh_fast.py b/ahb_cmd_view_refresh_fast.py new file mode 100644 index 0000000..b9cfef0 --- /dev/null +++ b/ahb_cmd_view_refresh_fast.py @@ -0,0 +1,22 @@ +import FreeCADGui as Gui +import FreeCAD as App + +class AHB_RefreshViewFast: + def GetResources(self): + return {"MenuText": "Refresh page (fast)", + "ToolTip": "Redraws the current page", + "Pixmap": "" + } + + def IsActive(self): + workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench + return workbench.techDrawExtensions.getActivePage() is not None + + def Activated(self): + workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench + page = workbench.techDrawExtensions.getActivePage() + if page is not None: + workbench.techDrawExtensions.forceRedrawPage(page, fast_render = True) + +from ahb_command import AHB_CommandWrapper +AHB_CommandWrapper.addGuiCommand('AHB_view_refresh_fast', AHB_RefreshViewFast()) diff --git a/ahb_raster_view.py b/ahb_raster_view.py index 3e7fced..d135689 100644 --- a/ahb_raster_view.py +++ b/ahb_raster_view.py @@ -99,7 +99,7 @@ class RasterView: def _should_render(self, obj): return obj.TypeId in ['Part::Feature', 'Part::FeaturePython', 'PartDesign::Body', 'Part::Mirroring', 'Part::Cut', 'Part::Part2DObjectPython'] - def render(self): + def render(self, fast_render = True): from pivy import coin import os from PIL import Image, ImageDraw, ImageChops @@ -141,26 +141,27 @@ class RasterView: 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 not fast_render: + 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) @@ -173,16 +174,17 @@ class RasterView: 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 - ) + if not fast_render: + 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, @@ -217,17 +219,19 @@ class RasterView: 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() + if fast_render: + composite_img = self._render_lines(tmp_doc, resolution, prev_parts + new_parts, (0.0, 0.0, 0.0), []) + else: + # render old parts in gray lines + prev_parts_img = self._render_lines(tmp_doc, resolution, prev_parts, (0.6, 0.6, 0.6), [], fast_render) + + # 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, fast_render) + + # create the composite image + composite_img = prev_parts_img.copy() + composite_img.paste(new_parts_img, None, new_parts_img) finally: # restore properties on objects we have modified @@ -289,7 +293,7 @@ class RasterView: 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): + def _render_lines(self, doc, resolution, parts, line_color, masking_parts, fast_render = True): import tempfile from PIL import Image, ImageDraw, ImageFilter @@ -303,7 +307,7 @@ class RasterView: # 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): + if self._should_render(obj) and not fast_render: 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) @@ -325,62 +329,66 @@ class RasterView: 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 + generate_outlines = not fast_render + if generate_outlines: + # 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: - 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() + outlines_img.paste(partial_outlines, None, partial_outlines.point(lambda p: 0 if p == 255 else 255)) - 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) + 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) + else: + all_lines = lines_img # colorize final image fill_color = (1.0, 1.0, 1.0) @@ -388,7 +396,7 @@ class RasterView: 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 + alpha_img.point(lambda p: 0 if p == 0 else 255) ]) # crop 1px borders diff --git a/ahb_techdraw_extensions.py b/ahb_techdraw_extensions.py index a42809f..f70a10b 100644 --- a/ahb_techdraw_extensions.py +++ b/ahb_techdraw_extensions.py @@ -82,8 +82,8 @@ class TechDrawExtensions: workbench.docObserver.onObjectTypeChanged('balloon_changed', 'TechDraw::DrawViewBalloon', lambda obj, prop: self.onBalloonChanged(obj, prop)) workbench.docObserver.onObjectTypeSelected('balloon_selected', 'TechDraw::DrawViewBalloon', lambda operation, obj, sub, point: self.onBalloonSelected(operation, obj, sub, point)) - def repaint(self, view): - self.views_to_repaint[view] = True + def repaint(self, view, fast_render = True): + self.views_to_repaint[view] = fast_render QTimer.singleShot(10, self._do_repaint) def _do_repaint(self): @@ -93,10 +93,10 @@ class TechDrawExtensions: selection = Gui.Selection.getSelection() - to_repaint = self.views_to_repaint.keys() + to_repaint = self.views_to_repaint.copy() self.views_to_repaint = {} - for view in to_repaint: + for view, fast_render in to_repaint.items(): if '_overlay' in view.Label: continue #print("Repainting " + view.Name) @@ -116,7 +116,7 @@ class TechDrawExtensions: print("Rasterizing view " + view.Label + "...") raster_view = RasterView(view) - raster_view.render() + raster_view.render(fast_render) view.Visibility = False overlayName = view.Label + "_overlay" @@ -583,16 +583,17 @@ class TechDrawExtensions: return obj return None - def forceRedrawPage(self, page, callback = None): - needPageUpdate = False + def forceRedrawPage(self, page, callback = None, fast_render = True): for view in page.Views: if view.TypeId == 'TechDraw::DrawViewPart' and 'Assembly_handbook_PreviousStepView' in view.PropertiesList: if not 'Assembly_handbook_RasterView' in view.PropertiesList: view.addProperty("App::PropertyBool", "Assembly_handbook_RasterView", "Assembly_handbook") view.Assembly_handbook_RasterView = True - if 'Assembly_handbook_RasterView' not in view.PropertiesList or not view.Assembly_handbook_RasterView: - needPageUpdate = True + if 'Assembly_handbook_RasterView' in view.PropertiesList and view.Assembly_handbook_RasterView: + view.purgeTouched() # make sure we don't trigger rendering of source views (this is awfully slow and doesn't even work for a lot of models) + else: + view.touch() self.refreshView(view) elif view.TypeId == 'TechDraw::DrawViewBalloon': if view.ViewObject.Visibility: @@ -608,23 +609,34 @@ class TechDrawExtensions: view.ViewObject.Visibility = True view.ViewObject.Visibility = False - if page.KeepUpdated or not needPageUpdate: + if page.KeepUpdated: for view in page.Views: if view.TypeId != 'TechDraw::DrawViewPart': view.recompute() for view in page.Views: if view.TypeId == 'TechDraw::DrawViewPart': view.recompute() - self.repaint(view) + self.repaint(view, fast_render) if callback is not None: callback() else: page.KeepUpdated = True def restoreKeepUpdated(): - page.KeepUpdated = False for view in page.Views: if view.TypeId == 'TechDraw::DrawViewPart': - self.repaint(view) + if view.Name.endswith('_overlay'): + view.touch() + view.recompute() + for sub_view in page.Views: + try: + if sub_view.SourceView == view: + sub_view.recompute() + except: + pass + else: + view.recompute() + self.repaint(view, fast_render) + page.KeepUpdated = False if callback is not None: callback() QTimer.singleShot(10, restoreKeepUpdated)