Compare commits

...

40 Commits

Author SHA1 Message Date
Youen dfe5d329b3 Improved part metadata system 2 weeks ago
Youen f92482eb46 Fixed error when a document has multiple "main" objects 3 months ago
Youen cbf1d1b275 Added code to increase near/far distance during rendering to avoid cutting objects 3 months ago
Youen 46698319f6 fixed CSV export 3 months ago
Youen b63643b153 Added system to export part list in CSV file 1 year ago
Youen 731b203c5e Added possibility to render multiple pages in batch 1 year ago
Youen 85b680d2f4 Added possibility to remove transparency from rasterized images 1 year ago
Youen 4bb23e5684 Added code to hide points in rasterized images (only lines should be visible) 1 year ago
Youen a7201934be fixed bug related to clipping in fast render mode 1 year ago
Youen c06af9db24 Added code to handle clipping view 1 year ago
Youen 5e65e68c13 raster rendering optimizations 1 year ago
Youen ec97d10a51 Added possibility to hide specific objects in the rasterized image 1 year ago
Youen a8e10a2b99 fixed rasterization issues, and added possibility to render Draft objects (dimensions, lines, etc.) 1 year ago
Youen ff8b904dc4 Added code to optimize PNG images file size (indexed colors with all-or-nothing alpha) 1 year ago
Youen 8a9c6d8069 Added possibility to manually define a view volume, and fixed some bugs 1 year ago
Youen f180c72f81 New Step button must always be active (to be able to create the first step) 1 year ago
Youen 20895bbf4b fixed error when a document is being loaded (balloons source object are not available yet) 1 year ago
Youen 13ff11293a Added code to initialize pages when loading a document (fix for balloons visibility and image file path) 1 year ago
Youen f62ab9eb70 fixed bugs for link branch 1 year ago
youen 8bea399e48 Merge pull request 'Divers fixes mineurs et annotations générées en cercle' (#2) from AndreasL/assembly_handbook:dev/better-annotations into master 1 year ago
youen 078ce07b86 Merge pull request 'Part name used in sub assembly are now properly display' (#1) from AndreasL/assembly_handbook:master into master 1 year ago
Andréas Livet 90f3402437 anotation grouping : various fix 1 year ago
Andréas Livet 1652f32504 annotation arrows are now scaled at 0.5 1 year ago
Andréas Livet 7214c541f1 Annotation balloon are regrouped and created in circle 1 year ago
Andréas Livet aaeea1784f Button are activated only if view is selected 1 year ago
Andréas Livet 11116df87e Part name used in sub assembly are now properly display 1 year ago
Youen b8f1e40de3 Added code to work arround TechDraw bugs when using the "annotate view" button 1 year ago
Youen c4a4f09818 fixed alpha bug that caused some lines to be too thick 1 year ago
Youen 580abc0960 Added code to correctly crop image views (otherwise they don't always display entirely) 1 year ago
Youen 1966fb7e13 fixed image cropping bug (that was introducing an offset compared to the TechDraw view) 1 year ago
Youen 3c7bdc2a4c Added button for faster rendering and fixed problem with overlay not recomputing 1 year ago
Youen d6fb938d96 Added possibility to annotate parts of sub-assemblies 1 year ago
Youen d7f520805d Added code to render outlines (needed for curved shapes like spheres and cylinders) 1 year ago
Youen e5c06a5796 Improved performances and fixed bugs 1 year ago
Youen 25c5149e9f Added code to change colors of parts rendered in rasterized views (white with black lines) 1 year ago
Youen 95034aa0bc fixed bug when rasterizing variant links 1 year ago
Youen f9d58c66fc fixed bugs 1 year ago
Youen 137b73fea1 refactored and improved view rasterization system (wip) 1 year ago
Youen 14e8aee319 rasterized image automatic alignment with TechDraw view (wip) 1 year ago
Youen be9ff003e3 alternative rendering method (rasterization) ; wip 1 year ago
  1. 1
      .gitignore
  2. 16
      InitGui.py
  3. 109
      ahb_cmd_export_parts_list.py
  4. 19
      ahb_cmd_new_step.py
  5. 135
      ahb_cmd_view_annotate.py
  6. 60
      ahb_cmd_view_annotate_detail.py
  7. 6
      ahb_cmd_view_edit_source_parts.py
  8. 21
      ahb_cmd_view_refresh.py
  9. 22
      ahb_cmd_view_refresh_fast.py
  10. 12
      ahb_cmd_view_set_direction.py
  11. 22
      ahb_document_observer.py
  12. 24
      ahb_material.py
  13. 541
      ahb_raster_view.py
  14. 561
      ahb_techdraw_extensions.py
  15. 8
      ahb_utils.py

1
.gitignore vendored

@ -3,3 +3,4 @@
__pycache__/
*.py[cod]
*$py.class
/pydev/

16
InitGui.py

@ -1,4 +1,5 @@
sys.path.append('/usr/local/lib/python3.9/dist-packages/')
#sys.path.append('/usr/local/lib/python3.9/dist-packages/')
sys.path.append('/home/youen/.FreeCAD/Mod/assembly_handbook/pydev')
#import pydevd
#pydevd.settrace()
@ -77,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")
@ -89,13 +90,22 @@ class AssemblyHandbookWorkbench(Gui.Workbench):
self.importModule('ahb_cmd_view_annotate')
toolbox.append("AHB_view_annotate")
self.importModule('ahb_cmd_view_annotate_detail')
toolbox.append('AHB_view_annotate_detail')
self.importModule('ahb_cmd_view_edit_source_parts')
toolbox.append("AHB_view_edit_source_parts")
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")
self.importModule('ahb_cmd_export_parts_list')
toolbox.append("AHB_export_parts_list")
if self.dev:
self.importModule('ahb_cmd_reload')

109
ahb_cmd_export_parts_list.py

@ -0,0 +1,109 @@
import FreeCADGui as Gui
import FreeCAD as App
import ahb_utils
from ahb_material import Material
class PartInfo:
def __init__(self, workbench, document, obj):
self.document = document
self.reference = obj.Label
if len(self.reference) == 3 and self.reference[0:1] in ['L', 'M', 'T', 'R', 'E']:
self.reference = 'TB_' + self.reference
workbench.techDrawExtensions.initPartMetadata(obj)
self.material = 'Unknown'
try:
self.material = obj.Assembly_handbook_Material
except:
pass
self.size = [obj.Shape.BoundBox.XLength, obj.Shape.BoundBox.YLength, obj.Shape.BoundBox.ZLength] # in mm
self.volume = obj.Shape.Volume # in mm3
self.mass = -1 # in g (negative means unknown mass)
density = -1 # in g/cm3
material = Material.Get(self.material)
if material is not None:
density = material.density
if density >= 0: self.mass = density * (self.volume / 1000)
try:
part_mass = obj.Assembly_handbook_Weight
if part_mass >= 0:
self.mass = part_mass
except:
pass
self.count = 0
class AHB_ExportPartsList:
def GetResources(self):
return {"MenuText": "Export parts list (CSV)",
"ToolTip": "Exports all parts of the selected assembly in CSV format",
"Pixmap": ""
}
def IsActive(self):
obj = Gui.Selection.getSelection()
if len(obj) == 1:
obj = obj[0]
return obj.TypeId == 'App::Part' and 'AssemblyType' in obj.PropertiesList
return False
def Activated(self):
all_parts = {}
rootAssembly = Gui.Selection.getSelection()[0]
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
def add_part(part):
#print(part.Label + " (" + part.LinkedObject.Label + ")")
try:
info = all_parts[part.LinkedObject]
except:
info = PartInfo(workbench, part.LinkedObject.Document.Name, part.LinkedObject)
all_parts[part.LinkedObject] = info
info.count += 1
def process_group(group):
for part in group.Group:
if not part.Visibility:
continue
isPartLink = part.TypeId == 'App::Link' or (part.TypeId == 'Part::FeaturePython' and hasattr(part, 'LinkedObject'))
partType = None
isSinglePart = None
try:
partType = part.Type
isSinglePart = part.Assembly_handbook_IsSinglePart
except:
pass
if isPartLink:
if isSinglePart != True and (partType == 'Assembly' or (part.Group[1].Label == 'Constraints' and part.Group[2].Label == 'Variables' and part.Group[3].Label == 'Configurations')):
process_group(part.LinkedObject)
else:
add_part(part)
else:
if part.TypeId != 'App::DocumentObjectGroup' and part.TypeId != 'Spreadsheet::Sheet' and partType != 'App::PropertyContainer':
print("??? " + part.Label)
pass
print("Exporting all parts contained in assembly: " + rootAssembly.Label + "...")
process_group(rootAssembly)
file_name = rootAssembly.Document.FileName.replace('.FCStd', '') + '_list.csv'
with open(file_name, "w") as f:
f.write("Document, Reference, Material, SizeX (mm), SizeY (mm), SizeZ (mm), Volume (cm3), Mass (g), Count\n")
for part in all_parts.values():
mass_str = ''
if part.mass >= 10:
mass_str = str(int(part.mass + 0.5))
elif part.mass >= 0:
mass_str = str(int(part.mass*10 + 0.5) / 10.0)
f.write(part.document + "," + part.reference + "," + part.material + "," + str(int(part.size[0]+0.5)) + "," + str(int(part.size[1]+0.5)) + "," + str(int(part.size[2]+0.5)) + "," + str(int(part.volume/100+0.5)/10.0) + "," + mass_str + ", " + str(part.count) + "\n")
print("Part list exported to " + file_name)
from ahb_command import AHB_CommandWrapper
AHB_CommandWrapper.addGuiCommand('AHB_export_parts_list', AHB_ExportPartsList())

19
ahb_cmd_new_step.py

@ -1,6 +1,7 @@
import FreeCADGui as Gui
import FreeCAD as App
import ahb_utils
class AHB_New_Step:
def GetResources(self):
return {"MenuText": "New Step",
@ -33,6 +34,7 @@ class AHB_New_Step:
if prev_view is None:
step_number = 1
keep_updated = False
raster_view = True
template_file_name = freecad_path + '/share/Mod/TechDraw/Templates/A4_Landscape_blank.svg'
parent_group = None
else:
@ -41,6 +43,12 @@ class AHB_New_Step:
if prev_page_group is not None:
parent_group = prev_page_group.getParentGroup()
keep_updated = prev_page.KeepUpdated
try:
raster_view = prev_page.Assembly_handbook_RasterView
except:
raster_view = True
if raster_view:
keep_updated = False
template_file_name = prev_page.Template.Template
numbers = re.findall(r'\d+', prev_page.Label)
if len(numbers) == 0: prev_number = 0
@ -65,6 +73,11 @@ class AHB_New_Step:
view.Perspective = False
view.CoarseView = True
view.addProperty("App::PropertyString", "Assembly_handbook_PreviousStepView", "Assembly_handbook")
view.addProperty("App::PropertyBool", "Assembly_handbook_RasterView", "Assembly_handbook")
view.addProperty("App::PropertyXLinkList", "Assembly_handbook_HideParts", "Assembly_handbook")
view.Assembly_handbook_RasterView = raster_view
if raster_view:
view.Visibility = False
if prev_view is None:
try:
workbench.techDrawExtensions.setCurrentViewDirection(view)
@ -81,7 +94,11 @@ class AHB_New_Step:
view.XSource = prev_view.XSource
page.addView(view)
view.recompute()
if raster_view:
view.purgeTouched()
else:
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

135
ahb_cmd_view_annotate.py

@ -1,6 +1,9 @@
import math
import FreeCADGui as Gui
import FreeCAD as App
import ahb_utils
class AHB_View_Annotate:
def GetResources(self):
return {"MenuText": "Annotate view",
@ -14,25 +17,50 @@ class AHB_View_Annotate:
def Activated(self):
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
if len(Gui.Selection.getSelection()) == 0:
page = workbench.techDrawExtensions.getActivePage()
workbench.techDrawExtensions.refreshOverlays(page)
return
if len(Gui.Selection.getSelection()) != 1:
raise Exception("Please select exactly one TechDraw view")
view = Gui.Selection.getSelection()[0]
if view.TypeId != 'TechDraw::DrawViewPart':
if view.TypeId == 'TechDraw::DrawPage':
workbench.techDrawExtensions.refreshOverlays(view)
return
elif view.TypeId != 'TechDraw::DrawViewPart':
raise Exception("Selected object is not a TechDraw view")
overlay_view = workbench.techDrawExtensions.getOverlayView(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")
def list_sub_parts(parts):
result = []
for part in parts:
if part.TypeId == 'App::Link':
result.append(part)
elif part.TypeId == 'Part::FeaturePython' and hasattr(part, 'LinkedObject'): # variant link
result.append(part)
if hasattr(part, 'Group'):
result.extend(list_sub_parts(part.Group))
return result
all_parts = list_sub_parts(view.XSource)
# Remove balloons referencing missing objects
for balloon in page.Views:
if balloon.TypeId == 'TechDraw::DrawViewBalloon' and "Assembly_handbook_Source" in balloon.PropertiesList:
if balloon.SourceView != view: continue
if balloon.SourceView != view and balloon.SourceView != overlay_view: continue
partLink = workbench.techDrawExtensions.getBalloonSourcePart(balloon)
if partLink is None or partLink not in view.XSource:
if partLink is None or partLink not in all_parts:
ref_name = "<no ref>"
try:
ref_name = balloon.Assembly_handbook_Source[1]
@ -41,41 +69,78 @@ class AHB_View_Annotate:
print(balloon.Name + " references missing object " + ref_name + ", removing balloon")
doc.removeObject(balloon.Name)
balloonsCreated = []
for partLink in view.XSource:
balloon = None
# Search an existing balloon to update
for obj in page.Views:
if obj.TypeId == 'TechDraw::DrawViewBalloon' and workbench.techDrawExtensions.getBalloonSourcePart(obj) == partLink:
if obj.SourceView != view: continue
balloon = obj
balloonsCreated.extend(workbench.techDrawExtensions.add_or_update_balloon(view, partLink, ''))
# Create a new balloon if needed
if balloon is None:
partName = partLink.Name
balloonName = partName + "_Balloon"
balloon = doc.addObject("TechDraw::DrawViewBalloon", balloonName)
balloon.SourceView = view
balloon.addProperty("App::PropertyXLink", "Assembly_handbook_Source", "Assembly_handbook")
balloon.Assembly_handbook_Source = (partLink, partLink.Name)
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
if not workbench.techDrawExtensions.isNewPartInView(view, partLink):
balloon.ViewObject.Visibility = False
if len(balloonsCreated) > 0:
regroupedBalloons = self.RegroupNearestSimilarBalloons(balloonsCreated)
#self.PlaceBalloonsInCircle(regroupedBalloons)
self.PlaceBalloonsInCircle(balloonsCreated)
workbench.techDrawExtensions.refreshOverlays(page)
def CalculatePointsCenter(self, balloons):
totalX = 0
totalY = 0
for balloon in balloons:
realBalloon = balloon[0] if type(balloon) is list else balloon
totalX = totalX + int(realBalloon.OriginX)
totalY = totalY + int(realBalloon.OriginY)
return App.Vector(totalX / len(balloons), totalY / len(balloons))
def IsSimilarBalloonNear(self, balloonA, balloonB):
MAX_DISTANCE_BETWEEN_REGROUPED_BALLOONS = 50
if balloonA.Text == balloonB.Text:
pos = App.Vector(balloonA.OriginX, balloonA.OriginY)
dist = pos.distanceToPoint(App.Vector(balloonB.OriginX, balloonB.OriginY))
return dist < MAX_DISTANCE_BETWEEN_REGROUPED_BALLOONS
else:
return False
def RegroupNearestSimilarBalloons(self, balloons):
regroupedBalloons = []
for balloon in balloons:
nearestBalloons = []
for otherBalloon in balloons:
if otherBalloon != balloon and self.IsSimilarBalloonNear(balloon, otherBalloon):
nearestBalloons.append(otherBalloon)
balloons.remove(otherBalloon)
if len(nearestBalloons) == 0:
regroupedBalloons.append(balloon)
else:
workbench.techDrawExtensions.updateBalloon(balloon)
nearestBalloons.append(balloon)
regroupedBalloons.append(nearestBalloons)
return regroupedBalloons
def PlaceBalloonsInCircle(self, balloons):
center = self.CalculatePointsCenter(balloons)
nbBalloons = len(balloons)
balloonPosStep = (math.pi * 2) / nbBalloons
for i in range(nbBalloons):
xPos = round(center.x + 600 * math.cos(balloonPosStep * i))
yPos = round(center.y + 600 * math.sin(balloonPosStep * i))
balloonPos = App.Vector(xPos, yPos)
# Find nearest arrow to avoid arrow crossing each other
smallestDistance = 0
balloonToUse = None
for balloon in balloons:
realBalloon = balloon[0] if type(balloon) is list else balloon
dist = balloonPos.distanceToPoint(App.Vector(realBalloon.OriginX, realBalloon.OriginY))
if smallestDistance == 0 or dist < smallestDistance:
smallestDistance = dist
balloonToUse = balloon
if balloonToUse is not None:
balloons.remove(balloonToUse)
if type(balloonToUse) is list:
for realBalloon in balloonToUse:
realBalloon.X = balloonPos.x
realBalloon.Y = balloonPos.y
else:
balloonToUse.X = balloonPos.x
balloonToUse.Y = balloonPos.y
from ahb_command import AHB_CommandWrapper
AHB_CommandWrapper.addGuiCommand('AHB_view_annotate', AHB_View_Annotate())

60
ahb_cmd_view_annotate_detail.py

@ -0,0 +1,60 @@
import FreeCADGui as Gui
import FreeCAD as App
class AHB_View_Annotate_Detail:
def GetResources(self):
return {"MenuText": "Annotate sub-assembly",
"ToolTip": "Annotates each part of selected sub-assembly balloons",
"Pixmap": ""
}
def IsActive(self):
return True
def Activated(self):
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
if len(Gui.Selection.getSelection()) == 0:
raise Exception("Please select at least one annotation balloon")
view = None
for balloon in Gui.Selection.getSelection():
if balloon.TypeId != 'TechDraw::DrawViewBalloon' or "Assembly_handbook_Source" not in balloon.PropertiesList:
raise Exception("All selected objects must be annotation balloons")
if view is not None and view != balloon.SourceView or balloon.SourceView is None:
raise Exception("Please only select balloons from the same view")
view = balloon.SourceView
overlay_view = workbench.techDrawExtensions.getOverlayView(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")
balloons_to_split = Gui.Selection.getSelection()
for balloon in balloons_to_split:
sub_assembly_link = workbench.techDrawExtensions.getBalloonSourcePart(balloon)
sub_assembly = sub_assembly_link.LinkedObject
if sub_assembly.TypeId != 'App::Part': continue
def list_first_level_sub_parts(group):
results = []
for obj in group.Group:
if obj.TypeId == 'App::Link':
results.append(obj)
elif obj.TypeId == 'Part::FeaturePython' and hasattr(obj, 'LinkedObject'): # variant link
results.append(obj)
elif hasattr(obj, 'Group'):
results.extend(list_first_level_sub_parts(obj))
return results
parts = list_first_level_sub_parts(sub_assembly)
for part in parts:
workbench.techDrawExtensions.add_or_update_balloon(view, part, workbench.techDrawExtensions.getBalloonSourcePartPath(balloon))
from ahb_command import AHB_CommandWrapper
AHB_CommandWrapper.addGuiCommand('AHB_view_annotate_detail', AHB_View_Annotate_Detail())

6
ahb_cmd_view_edit_source_parts.py

@ -1,6 +1,8 @@
import FreeCADGui as Gui
import FreeCAD as App
import ahb_utils
class AHB_EditViewSourceParts:
def GetResources(self):
return {"MenuText": "Edit view source parts",
@ -19,9 +21,7 @@ class AHB_EditViewSourceParts:
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]
view = ahb_utils.getCurrentView()
workbench.techDrawExtensions.toggleEditViewSourceParts(view)
class AHB_AddSourcePartsToView:

21
ahb_cmd_view_refresh.py

@ -3,8 +3,8 @@ import FreeCAD as App
class AHB_RefreshView:
def GetResources(self):
return {"MenuText": "Refresh page",
"ToolTip": "Redraws the current page",
return {"MenuText": "Refresh page (final quality)",
"ToolTip": "Redraws the current page, or if one or more pages are selected, redraw all the selected pages",
"Pixmap": ""
}
@ -14,9 +14,20 @@ class AHB_RefreshView:
def Activated(self):
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
page = workbench.techDrawExtensions.getActivePage()
if page is not None:
workbench.techDrawExtensions.forceRedrawPage(page)
selection = Gui.Selection.getSelection()
has_selected_pages = len(selection) > 0
for s in selection:
if s.TypeId != 'TechDraw::DrawPage':
has_selected_pages = False
if has_selected_pages:
for page in selection:
workbench.techDrawExtensions.forceRedrawPage(page, fast_render = False)
else:
page = workbench.techDrawExtensions.getActivePage()
if page is not None:
workbench.techDrawExtensions.forceRedrawPage(page, fast_render = False)
from ahb_command import AHB_CommandWrapper
AHB_CommandWrapper.addGuiCommand('AHB_view_refresh', AHB_RefreshView())

22
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())

12
ahb_cmd_view_set_direction.py

@ -1,6 +1,7 @@
import FreeCADGui as Gui
import FreeCAD as App
import ahb_utils
class AHB_SetViewDirection:
def GetResources(self):
return {"MenuText": "Set view direction",
@ -9,22 +10,15 @@ class AHB_SetViewDirection:
}
def IsActive(self):
view = self._get_view()
return view is not None
return ahb_utils.getCurrentView() is not None
def Activated(self):
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
view = self._get_view()
view = ahb_utils.getCurrentView()
if view is None:
raise Exception("Please select a TechDraw view")
workbench.techDrawExtensions.setCurrentViewDirection(view)
def _get_view(self):
view = None if len(Gui.Selection.getSelection()) == 0 else Gui.Selection.getSelection()[0]
if view is not None and view.TypeId != 'TechDraw::DrawViewPart':
view = None
return view
from ahb_command import AHB_CommandWrapper
AHB_CommandWrapper.addGuiCommand('AHB_view_set_direction', AHB_SetViewDirection())

22
ahb_document_observer.py

@ -43,11 +43,28 @@ class DocLinkObserver:
class DocObserver:
changed_object_by_type = {}
selection_by_type = {}
doc_callbacks = {}
was_selected = []
def __init__(self):
Gui.Selection.addObserver(self)
def slotActivateDocument(self, doc):
#print('slotActivateDocument', doc.Name)
self._triggerDocumentEvent(doc, 'activate')
def slotCreatedDocument(self, doc):
#print('slotCreatedDocument', doc.Name)
self._triggerDocumentEvent(doc, 'created')
def slotDeletedDocument(self, doc):
#print('slotDeletedDocument', doc.Name)
self._triggerDocumentEvent(doc, 'deleted')
def _triggerDocumentEvent(self, doc, event):
for callback in self.doc_callbacks.values():
callback(doc, event)
def slotChangedObject(self, obj, prop):
#print("object changed: " + str(obj).replace('<', '').replace(' object>', '') + " " + obj.Name + " : " + str(prop))
@ -94,4 +111,7 @@ class DocObserver:
if callbacks is None:
callbacks = {}
self.selection_by_type[type_id] = callbacks
callbacks[callback_id] = callback
callbacks[callback_id] = callback
def onDocumentEvent(self, callback_id: str, callback):
self.doc_callbacks[callback_id] = callback

24
ahb_material.py

@ -0,0 +1,24 @@
class Material:
def __init__(self, ID, density):
self.ID = ID
self.density = density
DB = []
@staticmethod
def GetMaterialIDs():
result = []
for m in Material.DB:
result.append(m.ID)
return result
@staticmethod
def Get(ID):
for m in Material.DB:
if m.ID == ID: return m
return None
Material.DB.append(Material('Stainless steel', density = 8.00))
Material.DB.append(Material('Aluminium', density = 2.71))
Material.DB.append(Material('Wood (pine)', density = 0.55))
Material.DB.append(Material('Plywood', density = 0.6))

541
ahb_raster_view.py

@ -0,0 +1,541 @@
import FreeCAD as App
import FreeCADGui as Gui
from datetime import datetime
def print_verbose(msg):
verbose = False
if verbose:
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
print(current_time, msg)
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 self._should_render(obj) or self._should_render_as_is(obj) or obj.TypeId in ['PartDesign::CoordinateSystem', 'PartDesign::Line']:
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', 'Part::MultiFuse', 'Part::Loft', 'Part::Torus', 'Part::Cylinder']
def _should_render_as_is(self, obj):
return obj.TypeId in ['App::FeaturePython']
def render(self, fast_render = True):
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_verbose('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)
if 'Assembly_handbook_RasterSavedView' in view.PropertiesList and view.Assembly_handbook_RasterSavedView is not None:
tmp_doc = view.Assembly_handbook_RasterSavedView.Document
close_tmp_doc = False
else:
tmp_doc = App.newDocument('tmp_raster', hidden=False, temp=False)
close_tmp_doc = True
transparent_background = True
if 'Assembly_handbook_TransparentBackground' in view.PropertiesList:
transparent_background = view.Assembly_handbook_TransparentBackground
objects_to_reset = {}
duplicated_parts = {}
try:
print_verbose("Preparing scene...")
# Clean existing scene (if any)
sceneGroup = tmp_doc.getObject('Scene')
if sceneGroup is not None:
sceneGroup.removeObjectsFromDocument()
tmp_doc.removeObject(sceneGroup.Name)
# construct new scene with links to the parts we want
sceneGroup = tmp_doc.addObject('App::DocumentObjectGroup', 'Scene')
prev_parts = []
new_parts = []
all_parts = view.XSource + view.Source
objects_to_hide = []
if 'Assembly_handbook_HideParts' in view.PropertiesList:
objects_to_hide = self._flatten_objects_tree(view.Assembly_handbook_HideParts)
for part in all_parts:
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
if part.TypeId in ['Part::Part2DObjectPython']:
link.Placement = part.Placement
is_new_part = workbench.techDrawExtensions.isNewPartInView(view, part)
if not fast_render:
# check if another part with different render settings will conflict with ours
# a conflict occurs when two parts link to the same object (directly or indirectly), because render settings (such as color) are set at the object level
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 self._should_render(obj) and 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
part_copy.ViewObject.Visibility = False
sceneGroup.addObject(link)
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) and not obj in objects_to_hide:
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
)
if not obj.ViewObject.Visibility:
obj.ViewObject.ShapeMaterial.AmbientColor = (0, 0, 0)
obj.ViewObject.ShapeMaterial.DiffuseColor = (0, 0, 0)
obj.ViewObject.ShapeMaterial.SpecularColor = (0, 0, 0)
obj.ViewObject.ShapeMaterial.EmissiveColor = (0, 0, 0)
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)
targetViewVolume = None
try:
targetViewVolume = view.Assembly_handbook_ViewVolume
except:
pass
if targetViewVolume is None:
tmp_doc_view.fitAll()
else:
sceneGroup.ViewObject.Visibility = False
viewVolumeLink = tmp_doc.addObject('App::Link', 'ViewVolume')
viewVolumeLink.LinkedObject = targetViewVolume
viewVolumeLink.Placement = targetViewVolume.Placement
tmp_doc_view.fitAll()
tmp_doc.removeObject(viewVolumeLink.Name)
sceneGroup.ViewObject.Visibility = True
print_verbose("Near=" + str(cam.nearDistance.getValue()) + ", far="+str(cam.farDistance.getValue()))
cam.nearDistance.setValue(cam.nearDistance.getValue() - 1000)
cam.farDistance.setValue(cam.farDistance.getValue() + 1000)
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
#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)
if fast_render:
print_verbose("Fast rasterization...")
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
print_verbose("Rendering old parts (gray)...")
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)
print_verbose("Rendering new parts (black)...")
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
print_verbose("Compositing images...")
composite_img = prev_parts_img.copy()
composite_img.paste(new_parts_img, None, new_parts_img)
# Optimize the image to reduce storage size
if not fast_render:
print_verbose("Optimizing PNG size...")
num_colors = 32
# All-or-nothing alpha: we use a white background and only make pixels fully transparent where alpha is zero, to not loose antialiasing
bg_img = Image.new(composite_img.mode, composite_img.size, color = '#ffffff')
bg_img.paste(composite_img.convert('RGB'), composite_img)
final_alpha = composite_img.split()[3].point(lambda p: 0 if p <= int(255/num_colors+0.5) else 255)
if not transparent_background:
final_alpha = final_alpha.point(lambda p: 255)
composite_img = bg_img
composite_img.putalpha(final_alpha)
# Convert to indexed colors
composite_img = composite_img.quantize(colors=num_colors, dither=Image.Dither.NONE)
finally:
print_verbose("Cleaning scene...")
#raise Exception("test")
# 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) and not obj in objects_to_hide:
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]
obj.ViewObject.PointMaterial.Transparency = 0
# remove the temporary document
if close_tmp_doc:
App.closeDocument(tmp_doc.Name)
print_verbose("Finalizing view...")
# Crop the image, which is also used to deduce the center of the source view
original_size = composite_img.size
diff_source_img = composite_img.split()[-1]
bg = Image.new(diff_source_img.mode, diff_source_img.size, '#000000') # fills an image with the background color
diff = ImageChops.difference(diff_source_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
image.ViewObject.Crop = True
image.Width = composite_img.size[0] * image_scale / 10.0 * 1.01
image.Height = composite_img.size[1] * image_scale / 10.0 * 1.01
image.recompute()
print_verbose("Done")
def _render_lines(self, doc, resolution, parts, line_color, masking_parts, fast_render = True):
import tempfile
from PIL import Image, ImageDraw, ImageFilter
doc_view = Gui.getDocument(doc.Name).mdiViewsOfType('Gui::View3DInventor')[0]
# render lines in blue, background in red, fill shapes in green
# the green band contains the lines image, the red band contains the inverted alpha layer
# if there is a clipping plane set with "Fill clip plane", the blue band contains the intersection with the clip plane
configured = []
print_verbose('Preparing objects for line rendering...')
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 obj in configured: continue
configured.append(obj)
if self._should_render(obj) and not fast_render:
obj.ViewObject.DisplayMode = 'Flat Lines'
obj.ViewObject.PointMaterial.Transparency = 1.0 # hide points
obj.ViewObject.LineColor = (0.0, 0.0, 1.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, 1.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
print_verbose('Rendering lines...')
temp_file_name = tempfile.gettempdir() + "/ahb_temp_image.png"
#temp_file_name = "/home/youen/tmp/ahb_temp_image.png"
doc_view.saveImage(temp_file_name, resolution[0]+2, resolution[1]+2, "#ff00ff") # we add 1 pixel border that we will need to crop later
lines_bands_img = self._read_image(temp_file_name)
lines_bands = lines_bands_img.split()
lines_img = lines_bands[1]
alpha_img = lines_bands[0].point(lambda p: 255 - p)
clip_img = lines_bands[2]
generate_outlines = not fast_render
#generate_outlines = False
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
print_verbose('Preparing objects for outline rendering...')
step = 8
r = step
g = step
b = step
configured = []
for link in doc.findObjects():
if link in parts or link in masking_parts:
for obj in self._flatten_objects_tree([link]):
if obj in configured: continue
configured.append(obj)
if self._should_render(obj) and obj.TypeId != 'Part::Part2DObjectPython':
configured.append(obj)
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
print_verbose('Rendering shapes...')
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)
print_verbose('Extracting outlines...')
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))
print_verbose('Combining lines and outlines...')
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
alpha_img = alpha_img.point(lambda p: 0 if p == 0 else 255)
# colorize final image
print_verbose('Colorizing 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
])
# set clip color
if not fast_render:
clip_color = (0.5, 0.5, 0.5)
colorized_clip_img = Image.merge("RGB", [
clip_img.point(lambda p: int(clip_color[0] * (255.0 - p))),
clip_img.point(lambda p: int(clip_color[1] * (255.0 - p))),
clip_img.point(lambda p: int(clip_color[2] * (255.0 - p)))
])
result.paste(colorized_clip_img, clip_img.point(lambda p: 255 - p))
# 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()

561
ahb_techdraw_extensions.py

@ -7,6 +7,8 @@ import TechDraw, TechDrawGui
from PySide import QtGui, QtCore
TDG = TechDrawGui
from ahb_material import Material
class CursorItem(QtGui.QGraphicsItem):
def __init__(self, parent = None, view = None):
super().__init__(parent)
@ -16,6 +18,9 @@ class CursorItem(QtGui.QGraphicsItem):
self.size = 100.0
self.view = view
def removeSceneEventFilter(self, a, b):
print('removeSceneEventFilter', a, b)
def onViewPosChange(self, callback):
self.viewPosChangeCallback = callback
@ -74,133 +79,230 @@ class TechDrawExtensions:
enable_selected_part_highlight = False # disable for now, for performance reasons
initialized_documents = []
def __init__(self):
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
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))
workbench.docObserver.onDocumentEvent('techdrawext_doc_event', lambda doc, event: self.onDocumentEvent(doc, event))
if App.ActiveDocument is not None:
self.onDocumentEvent(App.ActiveDocument, 'activate')
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):
from ahb_raster_view import RasterView
import Draft
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
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)
page = self.getViewPage(view)
view_cache = self.getViewCache(view)
view_cache.reset()
doc = view.Document
fast_rendering = False
#try:
# fast_rendering = view.Assembly_handbook_FastRendering
#except:
# pass
if view.CoarseView:
fast_rendering = True
selected_balloons = []
for obj in Gui.Selection.getSelection():
if obj.TypeId == 'TechDraw::DrawViewBalloon' and obj.SourceView == view and 'Assembly_handbook_Source' in obj.PropertiesList:
selected_balloons.append(obj)
#view.clearGeomFormats() # for an unknown reason, this will crash freecad
if not fast_rendering:
is_first_part = True
parts_to_paint = []
# repaint parts that are highlighted by selection
if self.enable_selected_part_highlight:
for balloon in selected_balloons:
part = self.getBalloonSourcePart(balloon)
if part in view.XSource:
parts_to_paint.append(part)
# repaint parts that are new in this step (thick line)
prev_view = None
if 'Assembly_handbook_PreviousStepView' in view.PropertiesList:
prev_view = doc.getObject(view.Assembly_handbook_PreviousStepView)
for part in view.XSource:
if (prev_view is None or part not in prev_view.XSource) and part not in parts_to_paint:
parts_to_paint.append(part)
if not 'Assembly_handbook_RasterView' in view.PropertiesList:
view.addProperty("App::PropertyBool", "Assembly_handbook_RasterView", "Assembly_handbook")
view.Assembly_handbook_RasterView = True
# make sure the list is not empty, so that we reset all lines
if len(parts_to_paint) == 0:
parts_to_paint.append(None)
if view.Assembly_handbook_RasterView:
print("Rasterizing view " + view.Label + "...")
for part in parts_to_paint:
default_line_thickness = 0.05
line_thickness = default_line_thickness
raster_view = RasterView(view)
raster_view.render(fast_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 not None:
doc.removeObject(overlay_frame.Name)
#overlay_frame = Draft.makeWire(points, closed=False, face=False, support=None)
overlay_frame = doc.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.001,0.001,1))
overlay_frame.Points = [pos, pos2]
Draft.ViewProviderWire(overlay_frame.ViewObject)
overlay_frame.recompute()
default_color = (0.0, 0.0, 0.0) if fast_rendering else (0.5, 0.5, 0.5)
color = default_color
overlay_frame2_name = view.Label + "_frame2"
overlay_frame2 = doc.getObject(overlay_frame2_name)
if overlay_frame2 is not None:
doc.removeObject(overlay_frame2.Name)
overlay_frame2 = doc.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.001,1.001,1))
overlay_frame2.Points = [pos, pos2]
Draft.ViewProviderWire(overlay_frame2.ViewObject)
overlay_frame2.recompute()
overlay.Source = [overlay_frame, overlay_frame2]
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
# migrate balloons from source view to overlay
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:
old_source = balloon.Assembly_handbook_Source
old_OriginOffsetX = balloon.Assembly_handbook_OriginOffsetX
old_OriginOffsetY = balloon.Assembly_handbook_OriginOffsetY
old_X = balloon.X
old_Y = balloon.Y
old_Visibility = balloon.ViewObject.Visibility
balloonName = balloon.Name
doc.removeObject(balloon.Name)
balloon = doc.addObject("TechDraw::DrawViewBalloon", balloonName)
balloon.SourceView = overlay
balloon.addProperty("App::PropertyXLink", "Assembly_handbook_Source", "Assembly_handbook")
balloon.Assembly_handbook_Source = old_source
balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetX", "Assembly_handbook")
balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetY", "Assembly_handbook")
balloon.Assembly_handbook_OriginOffsetX = old_OriginOffsetX
balloon.Assembly_handbook_OriginOffsetY = old_OriginOffsetY
page.addView(balloon)
self.updateBalloon(balloon)
balloon.X = old_X
balloon.Y = old_Y
balloon.ViewObject.Visibility = old_Visibility
balloon.recompute()
overlay.recompute()
page.recompute()
else:
fast_rendering = False
#try:
# fast_rendering = view.Assembly_handbook_FastRendering
#except:
# pass
if view.CoarseView:
fast_rendering = True
selected_balloons = []
for obj in Gui.Selection.getSelection():
if obj.TypeId == 'TechDraw::DrawViewBalloon' and obj.SourceView == view and 'Assembly_handbook_Source' in obj.PropertiesList:
selected_balloons.append(obj)
#view.clearGeomFormats() # for an unknown reason, this will crash freecad
if not fast_rendering:
is_first_part = True
parts_to_paint = []
# repaint parts that are highlighted by selection
if self.enable_selected_part_highlight:
for balloon in selected_balloons:
part = self.getBalloonSourcePart(balloon)
if part in view.XSource:
parts_to_paint.append(part)
# repaint parts that are new in this step (thick line)
prev_view = None
if 'Assembly_handbook_PreviousStepView' in view.PropertiesList:
prev_view = doc.getObject(view.Assembly_handbook_PreviousStepView)
for part in view.XSource:
if (prev_view is None or part not in prev_view.XSource) and part not in parts_to_paint:
parts_to_paint.append(part)
# make sure the list is not empty, so that we reset all lines
if len(parts_to_paint) == 0:
parts_to_paint.append(None)
if part is not None:
part_view = workbench.partsCache.getPart2DView(view, part)
for part in parts_to_paint:
default_line_thickness = 0.05
line_thickness = default_line_thickness
center = self.computePartCenter(view, part)
if self.isNewPartInView(view, part):
line_thickness = 0.2
color = (0, 0, 0)
default_color = (0.0, 0.0, 0.0) if fast_rendering else (0.5, 0.5, 0.5)
color = default_color
if self.enable_selected_part_highlight:
for balloon in selected_balloons:
if part == self.getBalloonSourcePart(balloon):
color = (0.0, 0.85, 0.0) # selection highlighting
# iterate edges of actual view and highlight matching edges
for edgeIdx in range(10000):
hasEdge = False
try:
edge = view.getEdgeByIndex(edgeIdx)
hasEdge = True
except:
pass
if not hasEdge:
break
if part is not None:
part_view = workbench.partsCache.getPart2DView(view, part)
center = self.computePartCenter(view, part)
if self.isNewPartInView(view, part):
line_thickness = 0.2
color = (0, 0, 0)
if self.enable_selected_part_highlight:
for balloon in selected_balloons:
if part == self.getBalloonSourcePart(balloon):
color = (0.0, 0.85, 0.0) # selection highlighting
is_edge_of_part = False
if part is not None and (not hasattr(edge.Curve, 'Degree') or edge.Curve.Degree == 1) and len(edge.Vertexes) == 2:
edgeData = [
edge.Vertexes[0].X - center.x,
edge.Vertexes[0].Y - center.y,
edge.Vertexes[1].X - center.x,
edge.Vertexes[1].Y - center.y
]
v0 = App.Vector(edgeData[0], edgeData[1])
v1 = App.Vector(edgeData[2], edgeData[3])
# iterate edges of actual view and highlight matching edges
for edgeIdx in range(10000):
hasEdge = False
try:
edge = view.getEdgeByIndex(edgeIdx)
hasEdge = True
except:
pass
if not hasEdge:
break
for line in part_view.cached_lines:
l0 = App.Vector(line[0], line[1])
l1 = App.Vector(line[2], line[3])
#d = abs(edgeData[0] - line[0]) + abs(edgeData[1] - line[1]) + abs(edgeData[2] - line[2]) + abs(edgeData[3] - line[3])
d = v0.distanceToLineSegment(l0, l1).Length + v1.distanceToLineSegment(l0, l1).Length
if d < 0.01:
is_edge_of_part = True
break
is_edge_of_part = False
if part is not None and (not hasattr(edge.Curve, 'Degree') or edge.Curve.Degree == 1) and len(edge.Vertexes) == 2:
edgeData = [
edge.Vertexes[0].X - center.x,
edge.Vertexes[0].Y - center.y,
edge.Vertexes[1].X - center.x,
edge.Vertexes[1].Y - center.y
]
v0 = App.Vector(edgeData[0], edgeData[1])
v1 = App.Vector(edgeData[2], edgeData[3])
if is_edge_of_part:
view.formatGeometricEdge(edgeIdx,1,line_thickness,color,True)
elif is_first_part:
# reset edge format
view.formatGeometricEdge(edgeIdx,1,default_line_thickness,default_color,True)
is_first_part = False
view.requestPaint()
for line in part_view.cached_lines:
l0 = App.Vector(line[0], line[1])
l1 = App.Vector(line[2], line[3])
#d = abs(edgeData[0] - line[0]) + abs(edgeData[1] - line[1]) + abs(edgeData[2] - line[2]) + abs(edgeData[3] - line[3])
d = v0.distanceToLineSegment(l0, l1).Length + v1.distanceToLineSegment(l0, l1).Length
if d < 0.01:
is_edge_of_part = True
break
if is_edge_of_part:
view.formatGeometricEdge(edgeIdx,1,line_thickness,color,True)
elif is_first_part:
# reset edge format
view.formatGeometricEdge(edgeIdx,1,default_line_thickness,default_color,True)
is_first_part = False
view.requestPaint()
def updateBalloonCursor(self, view):
selected_balloons = []
for obj in Gui.Selection.getSelection():
@ -208,6 +310,13 @@ class TechDrawExtensions:
selected_balloons.append(obj)
cursor = self.view_cursors.get(view, None)
if cursor is not None:
try:
cursor.x() # this can throw an exception if the Qt item has been deleted (for example when closing the page)
except Exception as ex:
print("Re-generating cursor...")
cursor = None
if cursor is None:
cursor = CursorItem(view = view)
TDG.addQGIToView(view, cursor);
@ -227,7 +336,7 @@ class TechDrawExtensions:
if doc != Gui.ActiveDocument.Document:
raise Exception("Current view is not for the same document as TechDraw view " + view.Name)
activeView = Gui.ActiveDocument.ActiveView
if str(type(activeView)) != "<class 'View3DInventorPy'>":
if str(type(activeView)) not in ["<class 'View3DInventorPy'>", "<class 'Gui.View3DInventor'>"]:
raise Exception("Current view is not a 3D view")
cam = activeView.getCameraNode()
@ -315,7 +424,7 @@ class TechDrawExtensions:
obj = self.getBalloonSourcePart(balloon)
view = balloon.SourceView
center = self.computePartCenter(view, obj)
center = self.computePartCenter(view, obj, self.getBalloonSourcePartPath(balloon))
balloon.Assembly_handbook_OriginOffsetX = new_pos.x - center.x
balloon.Assembly_handbook_OriginOffsetY = new_pos.y - center.y
@ -327,7 +436,7 @@ class TechDrawExtensions:
view = balloon.SourceView
self.updateBalloonCursor(view)
if self.enable_selected_part_highlight:
self.repaint(view) # disabled for now, for performance reasons
self.repaint(view)
def onBalloonChanged(self, obj, prop):
# Avoid reentry
@ -339,31 +448,94 @@ class TechDrawExtensions:
self.updating_balloon = True
self.updateBalloon(obj)
self.updating_balloon = False
def add_or_update_balloon(self, view, part, parent_path):
balloonsCreated = []
page = self.getViewPage(view)
overlay_view = self.getOverlayView(view)
doc = page.Document
path = parent_path
if path == '':
path = part.Document.Name + '#' + part.Name
else:
path += '.'
path += part.Name
# Search an existing balloon to update
balloon = None
for obj in page.Views:
if obj.TypeId == 'TechDraw::DrawViewBalloon' and self.getBalloonSourcePart(obj) == part and self.getBalloonSourcePartPath(obj) == path:
if obj.SourceView != overlay_view: continue
balloon = obj
# Create a new balloon if needed
if balloon is None:
if self.isNewPartInView(view, part):
partName = part.Name
balloonName = partName + "_Balloon"
balloon = doc.addObject("TechDraw::DrawViewBalloon", balloonName)
balloon.SourceView = overlay_view
balloon.addProperty("App::PropertyXLink", "Assembly_handbook_Source", "Assembly_handbook")
balloon.Assembly_handbook_Source = (part, part.Name)
balloon.addProperty("App::PropertyString", "Assembly_handbook_SourcePath", "Assembly_handbook")
balloon.Assembly_handbook_SourcePath = path
balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetX", "Assembly_handbook")
balloon.addProperty("App::PropertyFloat", "Assembly_handbook_OriginOffsetY", "Assembly_handbook")
page.addView(balloon)
self.updateBalloon(balloon)
if not self.isNewPartInView(view, part):
balloon.ViewObject.Visibility = False
else:
balloonsCreated.append(balloon)
else:
self.updateBalloon(balloon)
return balloonsCreated
def updateBalloon(self, balloon):
workbench = Gui.getWorkbench("AssemblyHandbookWorkbench") #: :type workbench: AssemblyHandbookWorkbench
view = balloon.SourceView
obj = self.getBalloonSourcePart(balloon)
path = self.getBalloonSourcePartPath(balloon)
partDisplayName = self.getPartDisplayName(obj)
objectCenterView = workbench.techDrawExtensions.computePartCenter(view, obj)
if obj is not None:
objectCenterView = workbench.techDrawExtensions.computePartCenter(view, obj, path)
balloon.OriginX = objectCenterView.x + balloon.Assembly_handbook_OriginOffsetX
balloon.OriginY = objectCenterView.y + balloon.Assembly_handbook_OriginOffsetY
balloon.OriginX = objectCenterView.x + balloon.Assembly_handbook_OriginOffsetX
balloon.OriginY = objectCenterView.y + balloon.Assembly_handbook_OriginOffsetY
partDisplayName = 'Inconnu' if obj is None else self.getPartDisplayName(obj)
balloon.Text = partDisplayName
balloon.Text = partDisplayName
balloon.ViewObject.Font = 'DejaVu Sans'
balloon.ViewObject.Fontsize = 4
balloon.BubbleShape = 'Inspection'
balloon.EndTypeScale = 1
balloon.EndTypeScale = 0.5
def getBalloonSourcePart(self, balloon):
try:
return balloon.Assembly_handbook_Source[0]
except:
return None
def getBalloonSourcePartPath(self, balloon):
try:
return balloon.Assembly_handbook_SourcePath
except:
part = self.getBalloonSourcePart(balloon)
if part is None:
return ''
return part.Document.Name + '#' + part.Name
def isPartLink(self, obj):
if obj is None:
@ -409,9 +581,17 @@ class TechDrawExtensions:
return obj
return None
def forceRedrawPage(self, page, callback = None):
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' 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:
@ -434,29 +614,133 @@ class TechDrawExtensions:
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():
for view in page.Views:
if view.TypeId == 'TechDraw::DrawViewPart':
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)
def refreshOverlays(self, page, callback = None):
import os
for view in page.Views:
if view.TypeId == 'TechDraw::DrawViewPart' and '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)
doc = page.Document
for image in page.Views:
if image.TypeId == 'TechDraw::DrawViewImage':
folder_name = '/' + os.path.basename(doc.FileName).replace('.FCStd', '') + '_raster/'
if folder_name in image.ImageFile:
full_path = doc.FileName.replace('.FCStd', '') + '_raster/' + image.ImageFile.split(folder_name)[1]
if image.ImageFile != full_path:
image.ImageFile = full_path
if page.KeepUpdated:
if callback:
callback()
else:
page.KeepUpdated = True
def restoreKeepUpdated():
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
page.KeepUpdated = False
for view in page.Views:
if view.TypeId == 'TechDraw::DrawViewBalloon':
if view.ViewObject.Visibility:
# workaround for a TechDraw bug: sometimes the balloon should be visible but doesn't appear, showing it again fixes the issue
view.ViewObject.Visibility = False
def makeRedrawCallback(view):
def redrawBalloon():
view.ViewObject.Visibility = True
return redrawBalloon
QTimer.singleShot(0, makeRedrawCallback(view))
else:
# workaround for a TechDraw bug: sometimes the balloon text is visible even if the balloon is hidden, hiding it again fixes the issue
view.ViewObject.Visibility = True
view.ViewObject.Visibility = False
if callback is not None:
callback()
QTimer.singleShot(10, restoreKeepUpdated)
def computePartCenter(self, view, obj):
def getSourceView(self, view):
if view.Name.endswith('_overlay'):
view = view.Document.getObject(view.Name[0:-8])
if view is None:
raise Exception("Can't find source view of " + view.Name)
return view
def getOverlayView(self, view):
if view.Name.endswith('_overlay'):
return view
overlay = view.Document.getObject(view.Name + '_overlay')
return overlay if overlay is not None else view
def computePartCenter(self, view, obj, path = None):
view = self.getSourceView(view)
mat = App.Matrix()
if path is not None:
path_parts = path.split('.')
path_parts.pop()
parent = None
for part in path_parts:
if parent is None:
doc_obj = part.split('#')
doc = App.getDocument(doc_obj[0])
link = doc.getObject(doc_obj[1])
else:
link = parent.Document.getObject(part)
mat = link.LinkPlacement * mat
parent = link.LinkedObject
if obj.TypeId == 'App::Link':
partLink = obj
objectCenterWorld = partLink.LinkPlacement.Matrix.multiply(partLink.LinkedObject.Shape.CenterOfGravity)
mat = mat.multiply(partLink.LinkPlacement.Matrix)
objectCenterWorld = partLink.LinkedObject.Shape.CenterOfGravity
elif obj.TypeId == 'Part::FeaturePython' and hasattr(obj, 'LinkedObject'): # variant link
partLink = obj
objectCenterWorld = partLink.Placement.Matrix.multiply(partLink.LinkedObject.Shape.CenterOfGravity)
mat = mat.multiply(partLink.Placement.Matrix)
objectCenterWorld = partLink.LinkedObject.Shape.CenterOfGravity
else:
objectCenterWorld = obj.Shape.CenterOfGravity
objectCenterWorld = mat.multiply(objectCenterWorld)
'''view_cache = self.getViewCache(view)
@ -475,6 +759,12 @@ class TechDrawExtensions:
return self.projectPoint(view, objectCenterWorld)
def projectPoint(self, view, point3d):
if 'Assembly_handbook_RasterView' in view.PropertiesList and view.Assembly_handbook_RasterView:
from ahb_raster_view import RasterView
raster_view = RasterView(view)
if raster_view.init_image_projection():
return raster_view.project3DPointToSourceView(point3d)
# DrawViewPart::projectPoint should be exposed to python in freecad 0.21, but for 0.20 we have to use a workaround
view_cache = self.getViewCache(view)
if view_cache.projected_origin is None:
@ -492,3 +782,48 @@ class TechDrawExtensions:
cache = ViewCache()
self.view_cache[view] = cache
return cache
def onDocumentEvent(self, doc, event):
if event == 'activate':
if doc not in self.initialized_documents:
self.initialized_documents.append(doc)
self.initializeDocument(doc)
elif event == 'deleted':
if doc in self.initialized_documents:
self.initialized_documents.remove(doc)
def initializeDocument(self, doc):
def doInit():
main_part = None
try:
for obj in doc.Objects:
if obj.TypeId == 'TechDraw::DrawPage':
self.onPageLoaded(obj)
main_parts = doc.getObjectsByLabel(doc.Name)
if len(main_parts) == 1:
main_part = main_parts[0]
except:
pass
if main_part is not None:
self.initPartMetadata(main_part)
QTimer.singleShot(0, doInit)
def initPartMetadata(self, part):
current_material = 'Unknown'
if 'Assembly_handbook_Material' in part.PropertiesList:
current_material = part.Assembly_handbook_Material
else:
part.addProperty("App::PropertyEnumeration", "Assembly_handbook_Material", "Assembly_handbook")
material_list = ['Unknown'] + Material.GetMaterialIDs()
part.Assembly_handbook_Material = material_list
part.Assembly_handbook_Material = material_list.index(current_material) if current_material in material_list else 0
if 'Assembly_handbook_Weight' not in part.PropertiesList:
part.addProperty("App::PropertyFloat", "Assembly_handbook_Weight", "Assembly_handbook", 'Part weight in grams. Set a negative number if weight is unknown.')
part.Assembly_handbook_Weight = -1
def onPageLoaded(self, page):
self.refreshOverlays(page)

8
ahb_utils.py

@ -0,0 +1,8 @@
import FreeCADGui as Gui
def getCurrentView():
currentSel = Gui.Selection.getSelection()
if len(currentSel) == 1 and currentSel[0].TypeId == 'TechDraw::DrawViewPart':
return currentSel[0]
else:
return None
Loading…
Cancel
Save