From 85fa12e301cd6aa1e213fd713a3b9edff69bc0db Mon Sep 17 00:00:00 2001
From: Youen <youen.toupin@youb.fr>
Date: Sun, 16 Oct 2022 16:27:25 +0200
Subject: [PATCH] Added code to highlight in red balloons that reference parts
 not drawn in the view

---
 ahb_cmd_new_step.py               | 172 +++++++++++++++---------------
 ahb_cmd_view_annotate.py          | 122 ++++++++++-----------
 ahb_cmd_view_edit_source_parts.py |  90 ++++++++--------
 ahb_parts_cache.py                | 144 ++++++++++++-------------
 ahb_techdraw_extensions.py        |  22 +++-
 5 files changed, 285 insertions(+), 265 deletions(-)

diff --git a/ahb_cmd_new_step.py b/ahb_cmd_new_step.py
index 63dc557..622b7bc 100644
--- a/ahb_cmd_new_step.py
+++ b/ahb_cmd_new_step.py
@@ -2,95 +2,95 @@ import FreeCADGui as Gui
 import FreeCAD as App
 
 class AHB_New_Step:
-    def GetResources(self):
-        return {"MenuText": "New Step",
-                "ToolTip": "Creates a page for the next step (first, select the view of the preceding step, if any)",
-                "Pixmap": ""
-                }
+	def GetResources(self):
+		return {"MenuText": "New Step",
+				"ToolTip": "Creates a page for the next step (first, select the view of the preceding step, if any)",
+				"Pixmap": ""
+				}
 
-    def IsActive(self):
-        return True
+	def IsActive(self):
+		return True
 
-    def Activated(self):
-        import re
-        import os
-        import sys
-        from PySide.QtCore import QTimer
-        
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        
-        if len(Gui.Selection.getSelection()) > 1:
-            raise Exception("Please either select exactly one TechDraw view, or nothing at all")
+	def Activated(self):
+		import re
+		import os
+		import sys
+		from PySide.QtCore import QTimer
+		
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		
+		if len(Gui.Selection.getSelection()) > 1:
+			raise Exception("Please either select exactly one TechDraw view, or nothing at all")
 
-        
-        prev_view = None if len(Gui.Selection.getSelection()) == 0 else Gui.Selection.getSelection()[0]
-        if prev_view is not None and prev_view.TypeId != 'TechDraw::DrawViewPart':
-            raise Exception("Selected object is not a TechDraw view")
-        
-        doc = App.ActiveDocument if prev_view is None else prev_view.Document
-        
-        freecad_path = os.path.normpath(os.path.dirname(sys.executable) + '/..') # 'usr' absolute path 
-        
-        if prev_view is None:
-            step_number = 1
-            keep_updated = False
-            template_file_name = freecad_path + '/share/Mod/TechDraw/Templates/A4_Landscape_blank.svg'
-            parent_group = None
-        else:
-            prev_page = workbench.techDrawExtensions.getViewPage(prev_view)
-            prev_page_group = prev_page.getParentGroup()
-            if prev_page_group is not None:
-                parent_group = prev_page_group.getParentGroup()
-            keep_updated = prev_page.KeepUpdated
-            template_file_name = prev_page.Template.Template
-            numbers = re.findall(r'\d+', prev_page.Label)
-            if len(numbers) == 0: prev_number = 0
-            else: prev_number = max(1, int(numbers[-1]))
-            step_number = prev_number + 1
-        
-        step_num_str = str(step_number).zfill(3)
-        
-        group = doc.addObject('App::DocumentObjectGroup', 'Etape' + step_num_str)
-        if parent_group is not None:
-            parent_group.addObject(group)
-        
-        page_name = 'Etape' + step_num_str + '_Page'
-        page = doc.addObject('TechDraw::DrawPage', page_name)
-        group.addObject(page)
-        if page.KeepUpdated != keep_updated: page.KeepUpdated = keep_updated
-        template = doc.addObject('TechDraw::DrawSVGTemplate', 'Template')
-        template.Template = template_file_name
-        page.Template = template
-        
-        view = doc.addObject('TechDraw::DrawViewPart', 'View')
-        view.Perspective = False
-        view.addProperty("App::PropertyString", "Assembly_handbook_PreviousStepView", "Assembly_handbook")
-        if prev_view is not None:
-            view.Assembly_handbook_PreviousStepView = prev_view.Name
-            view.X = prev_view.X
-            view.Y = prev_view.Y
-            view.Direction = prev_view.Direction
-            view.XDirection = prev_view.XDirection
-            view.ScaleType = prev_view.ScaleType
-            view.Scale = prev_view.Scale
-            view.XSource = prev_view.XSource
-        
-        page.addView(view)
-        view.recompute()
-        
-        # search for views after the prev view to relink them after the new view (i.e. we insert the new view as an intermediate step)
-        # TODO: re-number next steps if needed
-        if prev_view is not None:
-            for obj in doc.Objects:
-                if obj != view and obj.TypeId == 'TechDraw::DrawViewPart' and 'Assembly_handbook_PreviousStepView' in obj.PropertiesList and obj.Assembly_handbook_PreviousStepView == prev_view.Name:
-                    obj.Assembly_handbook_PreviousStepView = view.Name
-                    print(obj.Label + ' has been moved after the new step')
-        
-        if len(view.XSource) > 0:
-            workbench.techDrawExtensions.forceRedrawPage(page)
-        
-        Gui.Selection.clearSelection()
-        Gui.Selection.addSelection(page)
+		
+		prev_view = None if len(Gui.Selection.getSelection()) == 0 else Gui.Selection.getSelection()[0]
+		if prev_view is not None and prev_view.TypeId != 'TechDraw::DrawViewPart':
+			raise Exception("Selected object is not a TechDraw view")
+		
+		doc = App.ActiveDocument if prev_view is None else prev_view.Document
+		
+		freecad_path = os.path.normpath(os.path.dirname(sys.executable) + '/..') # 'usr' absolute path 
+		
+		if prev_view is None:
+			step_number = 1
+			keep_updated = False
+			template_file_name = freecad_path + '/share/Mod/TechDraw/Templates/A4_Landscape_blank.svg'
+			parent_group = None
+		else:
+			prev_page = workbench.techDrawExtensions.getViewPage(prev_view)
+			prev_page_group = prev_page.getParentGroup()
+			if prev_page_group is not None:
+				parent_group = prev_page_group.getParentGroup()
+			keep_updated = prev_page.KeepUpdated
+			template_file_name = prev_page.Template.Template
+			numbers = re.findall(r'\d+', prev_page.Label)
+			if len(numbers) == 0: prev_number = 0
+			else: prev_number = max(1, int(numbers[-1]))
+			step_number = prev_number + 1
+		
+		step_num_str = str(step_number).zfill(3)
+		
+		group = doc.addObject('App::DocumentObjectGroup', 'Etape' + step_num_str)
+		if parent_group is not None:
+			parent_group.addObject(group)
+		
+		page_name = 'Etape' + step_num_str + '_Page'
+		page = doc.addObject('TechDraw::DrawPage', page_name)
+		group.addObject(page)
+		if page.KeepUpdated != keep_updated: page.KeepUpdated = keep_updated
+		template = doc.addObject('TechDraw::DrawSVGTemplate', 'Template')
+		template.Template = template_file_name
+		page.Template = template
+		
+		view = doc.addObject('TechDraw::DrawViewPart', 'View')
+		view.Perspective = False
+		view.addProperty("App::PropertyString", "Assembly_handbook_PreviousStepView", "Assembly_handbook")
+		if prev_view is not None:
+			view.Assembly_handbook_PreviousStepView = prev_view.Name
+			view.X = prev_view.X
+			view.Y = prev_view.Y
+			view.Direction = prev_view.Direction
+			view.XDirection = prev_view.XDirection
+			view.ScaleType = prev_view.ScaleType
+			view.Scale = prev_view.Scale
+			view.XSource = prev_view.XSource
+		
+		page.addView(view)
+		view.recompute()
+		
+		# search for views after the prev view to relink them after the new view (i.e. we insert the new view as an intermediate step)
+		# TODO: re-number next steps if needed
+		if prev_view is not None:
+			for obj in doc.Objects:
+				if obj != view and obj.TypeId == 'TechDraw::DrawViewPart' and 'Assembly_handbook_PreviousStepView' in obj.PropertiesList and obj.Assembly_handbook_PreviousStepView == prev_view.Name:
+					obj.Assembly_handbook_PreviousStepView = view.Name
+					print(obj.Label + ' has been moved after the new step')
+		
+		if len(view.XSource) > 0:
+			workbench.techDrawExtensions.forceRedrawPage(page)
+		
+		Gui.Selection.clearSelection()
+		Gui.Selection.addSelection(page)
 
 from ahb_command import AHB_CommandWrapper
 AHB_CommandWrapper.addGuiCommand('AHB_new_step', AHB_New_Step())
diff --git a/ahb_cmd_view_annotate.py b/ahb_cmd_view_annotate.py
index a9d0722..1b678d5 100644
--- a/ahb_cmd_view_annotate.py
+++ b/ahb_cmd_view_annotate.py
@@ -2,70 +2,70 @@ import FreeCADGui as Gui
 import FreeCAD as App
 
 class AHB_View_Annotate:
-    def GetResources(self):
-        return {"MenuText": "View/Annotate",
-                "ToolTip": "Annotates a TechDraw view with object names",
-                "Pixmap": ""
-                }
+	def GetResources(self):
+		return {"MenuText": "Annotate view",
+				"ToolTip": "Annotates a TechDraw view with object names",
+				"Pixmap": ""
+				}
 
-    def IsActive(self):
-        return True
+	def IsActive(self):
+		return True
 
-    def Activated(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        
-        if len(Gui.Selection.getSelection()) != 1:
-            raise Exception("Please select exactly one TechDraw view")
+	def Activated(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		
+		if len(Gui.Selection.getSelection()) != 1:
+			raise Exception("Please select exactly one TechDraw view")
 
-        view = Gui.Selection.getSelection()[0]
-        if view.TypeId != 'TechDraw::DrawViewPart':
-            raise Exception("Selected object is not a TechDraw view")
-        
-        doc = view.Document
-        
-        page = workbench.techDrawExtensions.getViewPage(view)
-        if page is None:
-            raise Exception("Can't find page in which the selected view is located")
-        
-        # Remove balloons referencing missing objects
-        for balloon in page.Views:
-            if balloon.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_PartName" in balloon.PropertiesList:
-                if balloon.SourceView != view: continue
-                partLink = doc.getObject(balloon.Assembly_handbook_PartName)
-                if partLink is None or partLink not in view.XSource:
-                    print(balloon.Name + " references missing object " + balloon.Assembly_handbook_PartName + ", removing balloon")
-                    doc.removeObject(balloon.Name)
-        
-        for partLink in view.XSource:
-            balloon = None
-            
-            # Search an existing balloon to update
-            for obj in page.Views:
-                if obj.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_PartName" in obj.PropertiesList and obj.Assembly_handbook_PartName == partLink.Name:
-                    if obj.SourceView != view: continue
-                    balloon = obj
-            
-            # Create a new balloon if needed
-            if balloon is None:
-                partName = partLink.Name
-                
-                balloon = doc.addObject("TechDraw::DrawViewBalloon", "Balloon" + partName)
-                balloon.SourceView = view
-                
-                balloon.addProperty("App::PropertyString", "Assembly_handbook_PartName", "Assembly_handbook")
-                balloon.Assembly_handbook_PartName = partName
-                
-                balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetX", "Assembly_handbook")
-                balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetY", "Assembly_handbook")
-                
-                page.addView(balloon)
-                
-                workbench.techDrawExtensions.updateBalloon(balloon)
-                
-                balloon.X = int(balloon.OriginX) + 20
-                balloon.Y = int(balloon.OriginY) + 20
-            else:
-                workbench.techDrawExtensions.updateBalloon(balloon)
+		view = Gui.Selection.getSelection()[0]
+		if view.TypeId != 'TechDraw::DrawViewPart':
+			raise Exception("Selected object is not a TechDraw view")
+		
+		doc = view.Document
+		
+		page = workbench.techDrawExtensions.getViewPage(view)
+		if page is None:
+			raise Exception("Can't find page in which the selected view is located")
+		
+		# Remove balloons referencing missing objects
+		for balloon in page.Views:
+			if balloon.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_PartName" in balloon.PropertiesList:
+				if balloon.SourceView != view: continue
+				partLink = doc.getObject(balloon.Assembly_handbook_PartName)
+				if partLink is None or partLink not in view.XSource:
+					print(balloon.Name + " references missing object " + balloon.Assembly_handbook_PartName + ", removing balloon")
+					doc.removeObject(balloon.Name)
+		
+		for partLink in view.XSource:
+			balloon = None
+			
+			# Search an existing balloon to update
+			for obj in page.Views:
+				if obj.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_PartName" in obj.PropertiesList and obj.Assembly_handbook_PartName == partLink.Name:
+					if obj.SourceView != view: continue
+					balloon = obj
+			
+			# Create a new balloon if needed
+			if balloon is None:
+				partName = partLink.Name
+				
+				balloon = doc.addObject("TechDraw::DrawViewBalloon", "Balloon" + partName)
+				balloon.SourceView = view
+				
+				balloon.addProperty("App::PropertyString", "Assembly_handbook_PartName", "Assembly_handbook")
+				balloon.Assembly_handbook_PartName = partName
+				
+				balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetX", "Assembly_handbook")
+				balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetY", "Assembly_handbook")
+				
+				page.addView(balloon)
+				
+				workbench.techDrawExtensions.updateBalloon(balloon)
+				
+				balloon.X = int(balloon.OriginX) + 20
+				balloon.Y = int(balloon.OriginY) + 20
+			else:
+				workbench.techDrawExtensions.updateBalloon(balloon)
 
 from ahb_command import AHB_CommandWrapper
 AHB_CommandWrapper.addGuiCommand('AHB_view_annotate', AHB_View_Annotate())
diff --git a/ahb_cmd_view_edit_source_parts.py b/ahb_cmd_view_edit_source_parts.py
index 435c4a7..e0747aa 100644
--- a/ahb_cmd_view_edit_source_parts.py
+++ b/ahb_cmd_view_edit_source_parts.py
@@ -2,59 +2,59 @@ import FreeCADGui as Gui
 import FreeCAD as App
 
 class AHB_EditViewSourceParts:
-    def GetResources(self):
-        return {"MenuText": "Edit view source parts",
-                "ToolTip": "Edits the list of parts that will be rendered in the selected TechDraw view",
-                "Pixmap": ""
-                }
+	def GetResources(self):
+		return {"MenuText": "Edit view source parts",
+				"ToolTip": "Edits the list of parts that will be rendered in the selected TechDraw view",
+				"Pixmap": ""
+				}
 
-    def IsActive(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        edit_mode = workbench.techDrawExtensions.edited_view is not None
-        
-        if edit_mode:
-            return True
-        else:
-            return len(Gui.Selection.getSelection()) == 1 and Gui.Selection.getSelection()[0].TypeId == 'TechDraw::DrawViewPart'
+	def IsActive(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		edit_mode = workbench.techDrawExtensions.edited_view is not None
+		
+		if edit_mode:
+			return True
+		else:
+			return len(Gui.Selection.getSelection()) == 1 and Gui.Selection.getSelection()[0].TypeId == 'TechDraw::DrawViewPart'
 
-    def Activated(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        view = None
-        if len(Gui.Selection.getSelection()) == 1 and Gui.Selection.getSelection()[0].TypeId == 'TechDraw::DrawViewPart':
-            view = Gui.Selection.getSelection()[0]
-        workbench.techDrawExtensions.toggleEditViewSourceParts(view)
-        
+	def Activated(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		view = None
+		if len(Gui.Selection.getSelection()) == 1 and Gui.Selection.getSelection()[0].TypeId == 'TechDraw::DrawViewPart':
+			view = Gui.Selection.getSelection()[0]
+		workbench.techDrawExtensions.toggleEditViewSourceParts(view)
+		
 class AHB_AddSourcePartsToView:
-    def GetResources(self):
-        return {"MenuText": "Add",
-                "ToolTip": "Adds the selected part(s) to the currently edited view",
-                "Pixmap": ""
-                }
+	def GetResources(self):
+		return {"MenuText": "Add",
+				"ToolTip": "Adds the selected part(s) to the currently edited view",
+				"Pixmap": ""
+				}
 
-    def IsActive(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        edit_mode = workbench.techDrawExtensions.edited_view is not None
-        return edit_mode
+	def IsActive(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		edit_mode = workbench.techDrawExtensions.edited_view is not None
+		return edit_mode
 
-    def Activated(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        workbench.techDrawExtensions.editViewSourceParts(Gui.Selection.getSelection(), True)
-        
+	def Activated(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		workbench.techDrawExtensions.editViewSourceParts(Gui.Selection.getSelection(), True)
+		
 class AHB_RemoveSourcePartsToView:
-    def GetResources(self):
-        return {"MenuText": "Remove",
-                "ToolTip": "Removes the selected part(s) from the currently edited view",
-                "Pixmap": ""
-                }
+	def GetResources(self):
+		return {"MenuText": "Remove",
+				"ToolTip": "Removes the selected part(s) from the currently edited view",
+				"Pixmap": ""
+				}
 
-    def IsActive(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        edit_mode = workbench.techDrawExtensions.edited_view is not None
-        return edit_mode
+	def IsActive(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		edit_mode = workbench.techDrawExtensions.edited_view is not None
+		return edit_mode
 
-    def Activated(self):
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        workbench.techDrawExtensions.editViewSourceParts(Gui.Selection.getSelection(), False)
+	def Activated(self):
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		workbench.techDrawExtensions.editViewSourceParts(Gui.Selection.getSelection(), False)
 
 from ahb_command import AHB_CommandWrapper
 AHB_CommandWrapper.addGuiCommand('AHB_view_edit_source_parts', AHB_EditViewSourceParts())
diff --git a/ahb_parts_cache.py b/ahb_parts_cache.py
index d23c182..6360abc 100644
--- a/ahb_parts_cache.py
+++ b/ahb_parts_cache.py
@@ -2,77 +2,77 @@ import FreeCAD as App
 import FreeCADGui as Gui
 
 class PartCachedView:
-    def __init__(self, direction, x_direction, obj):
-        self.direction = direction
-        self.x_direction = x_direction
-        self.doc_name = obj.Document.Name
-        self.obj_name = obj.Name
-        self.cached_lines = None
-        
-    def render(self):
-        import numpy as np
-        import os
-        
-        print("Rendering " + self.obj_name + " in cache")
-        
-        workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
-        
-        doc = App.getDocument(self.doc_name) #: :type doc: App.Document
-        obj = doc.getObject(self.obj_name) #: :type obj: App.DocumentObject
-        
-        # create temporary view
-        page = doc.addObject('TechDraw::DrawPage', 'TmpPage')
-        if not page.KeepUpdated: page.KeepUpdated = True
-        template = doc.addObject('TechDraw::DrawSVGTemplate', 'Template')
-        import ahb_locator
-        template.Template = os.path.join(os.path.dirname(ahb_locator.__file__), "resources/A4_Landscape_blank.svg")
-        page.Template = template
-            
-        tmpView = doc.addObject('TechDraw::DrawViewPart', 'TmpView')
-        tmpView.Direction = self.direction
-        tmpView.XDirection = self.x_direction
-        tmpView.Perspective = False
-        tmpView.ScaleType = 'Custom'
-        tmpView.Scale = 1.0
-        tmpView.XSource = [obj]
-        page.addView(tmpView)
-        tmpView.recompute()
-        
-        # copy edges relative to center
-        tmpCenter = workbench.techDrawExtensions.computePartCenter(tmpView, obj)
-        
-        # count lines
-        tmpEdges = tmpView.getVisibleEdges()
-        numLines = 0
-        for edge in tmpEdges:
-            if not hasattr(edge.Curve, 'Degree') or edge.Curve.Degree == 1: numLines += 1
-        
-        # store all lines in a packed array of floats (for each line: X1, Y1, X2, Y2)
-        self.cached_lines = np.empty([numLines, 4])
-        lineIdx = 0
-        for edge in tmpEdges:
-            if (not hasattr(edge.Curve, 'Degree') or edge.Curve.Degree == 1) and len(edge.Vertexes) == 2:
-                sx = 1.0
-                sy = -1.0
-                self.cached_lines[lineIdx] = [
-                    edge.Vertexes[0].Point.x*sx - tmpCenter.x,
-                    edge.Vertexes[0].Point.y*sy - tmpCenter.y,
-                    edge.Vertexes[1].Point.x*sx - tmpCenter.x,
-                    edge.Vertexes[1].Point.y*sy - tmpCenter.y
-                ]
-                lineIdx = lineIdx + 1
-        
-        # delete temporary view
-        doc.removeObject(page.Name)
+	def __init__(self, direction, x_direction, obj):
+		self.direction = direction
+		self.x_direction = x_direction
+		self.doc_name = obj.Document.Name
+		self.obj_name = obj.Name
+		self.cached_lines = None
+		
+	def render(self):
+		import numpy as np
+		import os
+		
+		print("Rendering " + self.obj_name + " in cache")
+		
+		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
+		
+		doc = App.getDocument(self.doc_name) #: :type doc: App.Document
+		obj = doc.getObject(self.obj_name) #: :type obj: App.DocumentObject
+		
+		# create temporary view
+		page = doc.addObject('TechDraw::DrawPage', 'TmpPage')
+		if not page.KeepUpdated: page.KeepUpdated = True
+		template = doc.addObject('TechDraw::DrawSVGTemplate', 'Template')
+		import ahb_locator
+		template.Template = os.path.join(os.path.dirname(ahb_locator.__file__), "resources/A4_Landscape_blank.svg")
+		page.Template = template
+			
+		tmpView = doc.addObject('TechDraw::DrawViewPart', 'TmpView')
+		tmpView.Direction = self.direction
+		tmpView.XDirection = self.x_direction
+		tmpView.Perspective = False
+		tmpView.ScaleType = 'Custom'
+		tmpView.Scale = 1.0
+		tmpView.XSource = [obj]
+		page.addView(tmpView)
+		tmpView.recompute()
+		
+		# copy edges relative to center
+		tmpCenter = workbench.techDrawExtensions.computePartCenter(tmpView, obj)
+		
+		# count lines
+		tmpEdges = tmpView.getVisibleEdges()
+		numLines = 0
+		for edge in tmpEdges:
+			if not hasattr(edge.Curve, 'Degree') or edge.Curve.Degree == 1: numLines += 1
+		
+		# store all lines in a packed array of floats (for each line: X1, Y1, X2, Y2)
+		self.cached_lines = np.empty([numLines, 4])
+		lineIdx = 0
+		for edge in tmpEdges:
+			if (not hasattr(edge.Curve, 'Degree') or edge.Curve.Degree == 1) and len(edge.Vertexes) == 2:
+				sx = 1.0
+				sy = -1.0
+				self.cached_lines[lineIdx] = [
+					edge.Vertexes[0].Point.x*sx - tmpCenter.x,
+					edge.Vertexes[0].Point.y*sy - tmpCenter.y,
+					edge.Vertexes[1].Point.x*sx - tmpCenter.x,
+					edge.Vertexes[1].Point.y*sy - tmpCenter.y
+				]
+				lineIdx = lineIdx + 1
+		
+		# delete temporary view
+		doc.removeObject(page.Name)
 
 class PartsCache:
-    part_views = {}
-    
-    def getPart2DView(self, view, obj):
-        key = (view.Direction.x, view.Direction.y, view.Direction.z, view.XDirection.x, view.XDirection.y, view.XDirection.z, obj.Document.Name, obj.Name)
-        part_view = self.part_views.get(key, None)
-        if part_view is None:
-            part_view = PartCachedView(view.Direction, view.XDirection, obj)
-            part_view.render()
-            self.part_views[key] = part_view
-        return part_view
\ No newline at end of file
+	part_views = {}
+	
+	def getPart2DView(self, view, obj):
+		key = (view.Direction.x, view.Direction.y, view.Direction.z, view.XDirection.x, view.XDirection.y, view.XDirection.z, obj.Document.Name, obj.Name)
+		part_view = self.part_views.get(key, None)
+		if part_view is None:
+			part_view = PartCachedView(view.Direction, view.XDirection, obj)
+			part_view.render()
+			self.part_views[key] = part_view
+		return part_view
\ No newline at end of file
diff --git a/ahb_techdraw_extensions.py b/ahb_techdraw_extensions.py
index 905a97d..0b3a967 100644
--- a/ahb_techdraw_extensions.py
+++ b/ahb_techdraw_extensions.py
@@ -150,6 +150,18 @@ class TechDrawExtensions:
 				cursor.setViewPos(App.Vector(selected_balloons[0].OriginX, selected_balloons[0].OriginY))
 				cursor.setVisible(True)
 				
+	def refreshView(self, view):
+		doc = view.Document
+		page = self.getViewPage(view)
+		for balloon in page.Views:
+			if balloon.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_PartName" in balloon.PropertiesList and balloon.SourceView == view:
+				obj = doc.getObject(balloon.Assembly_handbook_PartName)
+				balloonColor = (0.0, 0.0, 0.0)
+				if obj is None or not obj in view.XSource:
+					balloonColor = (1.0, 0.0, 0.0)
+				balloon.ViewObject.Color = balloonColor
+		
+				
 	def toggleEditViewSourceParts(self, view):
 		workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
 		
@@ -272,9 +284,17 @@ class TechDrawExtensions:
 		return None
 	
 	def forceRedrawPage(self, page, callback = None):
+		for view in page.Views:
+			if view.TypeId == 'TechDraw::DrawViewPart' and 'Assembly_handbook_PreviousStepView' in view.PropertiesList:
+				self.refreshView(view) 
+		
 		if page.KeepUpdated:
 			for view in page.Views:
-				view.recompute() 
+				if view.TypeId != 'TechDraw::DrawViewPart':
+					view.recompute()
+			for view in page.Views:
+				if view.TypeId == 'TechDraw::DrawViewPart':
+					view.recompute() 
 			if callback is not None:
 				callback()
 		else: