import FreeCAD as App
import FreeCADGui as Gui
from PySide . QtCore import QTimer
import TechDraw , TechDrawGui
from PySide import QtGui , QtCore
TDG = TechDrawGui
class CursorItem ( QtGui . QGraphicsItem ) :
def __init__ ( self , parent = None , view = None ) :
super ( ) . __init__ ( parent )
self . Type = QtGui . QGraphicsItem . UserType + 501
self . setZValue ( 500 )
self . margin = 10.0
self . size = 100.0
self . view = view
def onViewPosChange ( self , callback ) :
self . viewPosChangeCallback = callback
def boundingRect ( self ) :
return QtCore . QRectF ( - self . size / 2 - self . margin , - self . size / 2 - self . margin ,
self . size + 2.0 * self . margin , self . size + 2.0 * self . margin )
def paint ( self , painter , option , widget ) :
#print("paint")
painter . setBrush ( QtCore . Qt . darkRed )
#painter.drawRoundedRect(-100, -100, 200, 200, 50, 50)
h = self . size / 2.0
painter . drawLine ( - h , 0 , h , 0 )
painter . drawLine ( 0 , - h , 0 , h )
def mousePressEvent ( self , event ) :
#print('mouse press', event)
self . startMovePos = event . pos ( )
def mouseMoveEvent ( self , event ) :
#print('mouse move', event)
offset = event . pos ( ) - self . startMovePos
self . moveBy ( offset . x ( ) , offset . y ( ) )
def mouseReleaseEvent ( self , event ) :
#print('mouse release', event)
#print('new pos', self.x(), self.y())
if self . viewPosChangeCallback is not None :
self . viewPosChangeCallback ( self . getViewPos ( ) )
def getViewPos ( self ) :
scale = self . view . Scale * 10.0
return App . Vector ( self . x ( ) / scale , - self . y ( ) / scale )
def setViewPos ( self , p ) :
scale = self . view . Scale * 10.0
self . setPos ( p . x * scale , - p . y * scale )
class ViewCache :
def __init__ ( self ) :
self . reset ( )
def reset ( self ) :
self . projected_origin = None
class TechDrawExtensions :
views_to_repaint = { }
view_cursors = { }
view_cache = { }
updating_balloon = False
edited_view = None
enable_selected_part_highlight = False # disable for now, for performance reasons
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 ) )
def repaint ( self , view ) :
self . views_to_repaint [ view ] = True
QTimer . singleShot ( 10 , self . _do_repaint )
def _do_repaint ( self ) :
workbench = Gui . getWorkbench ( " AssemblyHandbookWorkbench " ) #: :type workbench: AssemblyHandbookWorkbench
selection = Gui . Selection . getSelection ( )
to_repaint = self . views_to_repaint . keys ( )
self . views_to_repaint = { }
for view in to_repaint :
#print("Repainting " + view.Name)
page = self . getViewPage ( view )
view_cache = self . getViewCache ( view )
view_cache . reset ( )
doc = view . Document
if not ' Assembly_handbook_RasterView ' in view . PropertiesList :
view . addProperty ( " App::PropertyBool " , " Assembly_handbook_RasterView " , " Assembly_handbook " )
if view . Assembly_handbook_RasterView :
print ( " Rasterizing view " + view . Label + " ... " )
imageName = view . Label + " _raster "
image = doc . getObject ( imageName )
if image is None :
image = doc . addObject ( ' TechDraw::DrawViewImage ' , view . Label + " _raster " )
if not image in page . Views :
page . addView ( image )
new_views_list = page . Views
new_views_list . remove ( image )
view_idx = new_views_list . index ( view )
new_views_list . insert ( view_idx , image )
page . Views = new_views_list
image_file_name = doc . FileName . replace ( ' .FCStd ' , ' ' ) + ' _raster/ ' + view . Name + ' .png '
image_scale = self . rasterizeView ( view , image_file_name )
image . ImageFile = " "
image . Scale = image_scale
image . X = view . X #- image_center[0]/10.0*image_scale
image . Y = view . Y #+ image_center[1]/10.0*image_scale
image . ImageFile = image_file_name # TODO: see if it's possible to set a relative path
image . recompute ( )
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 )
for part in parts_to_paint :
default_line_thickness = 0.05
line_thickness = default_line_thickness
default_color = ( 0.0 , 0.0 , 0.0 ) if fast_rendering else ( 0.5 , 0.5 , 0.5 )
color = default_color
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
# 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
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 ] )
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 rasterizeView ( self , view , image_file_name ) :
from pivy import coin
import os
from PIL import Image , ImageDraw , ImageChops
print ( ' Rasterizing ' + view . Label + " to " + image_file_name + " ... " )
dir = os . path . dirname ( image_file_name )
if not os . path . exists ( dir ) :
os . makedirs ( dir )
doc = App . newDocument ( ' tmp_raster ' , hidden = False , temp = False )
for part in view . XSource :
link = doc . addObject ( ' App::Link ' , part . Name )
link . Label = part . Label
if part . TypeId == ' App::Link ' :
link . LinkedObject = part . LinkedObject
link . Placement = part . Placement
else :
link . LinkedObject = part
docView = Gui . getDocument ( doc . Name ) . mdiViewsOfType ( ' Gui::View3DInventor ' ) [ 0 ]
cam = docView . getCameraNode ( )
rot = coin . SbRotation ( coin . SbVec3f ( 1 , 0 , 0 ) , coin . SbVec3f ( view . XDirection . x , view . XDirection . y , view . XDirection . z ) )
rot * = coin . SbRotation ( coin . SbVec3f ( 0 , 0 , 1 ) , coin . SbVec3f ( view . Direction . x , view . Direction . y , view . Direction . z ) )
cam . orientation . setValue ( rot )
docView . fitAll ( )
viewVolume = cam . getViewVolume ( 0.0 )
docView . saveImage ( image_file_name , 4096 , 4096 , " #ffffff " )
def project3dPointToViewport ( p3d ) :
YDirection = view . Direction . cross ( view . XDirection )
sb_offset = viewVolume . projectToScreen ( coin . SbVec3f ( 0 , 0 , 0 ) )
offset = App . Vector ( sb_offset [ 0 ] , sb_offset [ 1 ] , 0 )
#print('offset', offset)
p2d = App . Vector ( view . XDirection . dot ( p3d ) / viewVolume . getWidth ( ) + offset . x , YDirection . dot ( p3d ) / viewVolume . getHeight ( ) + offset . y , 0 )
return p2d
p2dA = project3dPointToViewport ( App . Vector ( 0 , 0 , 0 ) )
p2dB = project3dPointToViewport ( view . XDirection )
# page_mm = (p2dB.x - p2dA.x) * resX / 100 * PNGscale = viewScale
imageScale = view . Scale / ( p2dB . x - p2dA . x ) / 4096.0 * 10
print ( ' imageScale ' , imageScale )
with Image . open ( image_file_name ) as img :
original_size = img . size
bg = Image . new ( img . mode , img . size , ' #ffffff ' ) # fills an image with the background color
diff = ImageChops . difference ( img , bg ) # diff between the actual image and the background color
bbox = diff . getbbox ( ) # finds border size (non-black portion of the image)
print ( bbox )
#image_center = (bbox[0] + (bbox[2] - bbox[0])/2 - img.size[0]/2, bbox[1] + (bbox[3] - bbox[1])/2 - img.size[1]/2)
#print(image_center)
img = img . crop ( bbox )
draw = ImageDraw . Draw ( img )
def debugPoint ( p3d ) :
p2d = project3dPointToViewport ( p3d )
pp = App . Vector ( p2d . x * original_size [ 0 ] - bbox [ 0 ] , ( 1.0 - p2d . y ) * original_size [ 1 ] - bbox [ 1 ] )
#print('pp', pp)
len = 100
draw . line ( [ ( pp . x , pp . y - len ) , ( pp . x , pp . y + len ) ] , fill = 128 , width = 7 )
draw . line ( [ ( pp . x - len , pp . y ) , ( pp . x + len , pp . y ) ] , fill = 128 , width = 7 )
#debugPoint(App.Vector(-12.5, 37.5, 25.0))
#debugPoint(App.Vector(-12.5, -1387.5, 25.0))
#debugPoint(App.Vector(131.23702882966705, -655.0000021095163, 145.21130178331268))
img . save ( image_file_name )
App . closeDocument ( doc . Name )
return imageScale
def updateBalloonCursor ( self , view ) :
selected_balloons = [ ]
for obj in Gui . Selection . getSelection ( ) :
if obj . TypeId == ' TechDraw::DrawViewBalloon ' and obj . SourceView == view and ' Assembly_handbook_Source ' in obj . PropertiesList :
selected_balloons . append ( obj )
cursor = self . view_cursors . get ( view , None )
if cursor is None :
cursor = CursorItem ( view = view )
TDG . addQGIToView ( view , cursor ) ;
cursor . onViewPosChange ( lambda new_pos : self . onCursorMoved ( view , new_pos ) )
self . view_cursors [ view ] = cursor
if len ( selected_balloons ) == 0 :
cursor . setVisible ( False )
else :
cursor . setViewPos ( App . Vector ( selected_balloons [ 0 ] . OriginX , selected_balloons [ 0 ] . OriginY ) )
cursor . setVisible ( True )
def setCurrentViewDirection ( self , view ) :
from pivy import coin
doc = view . Document
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 ) ) not in [ " <class ' View3DInventorPy ' > " , " <class ' Gui.View3DInventor ' > " ] :
raise Exception ( " Current view is not a 3D view " )
cam = activeView . getCameraNode ( )
dir = cam . orientation . getValue ( ) . multVec ( coin . SbVec3f ( 0 , 0 , 1 ) ) . getValue ( )
xdir = cam . orientation . getValue ( ) . multVec ( coin . SbVec3f ( 1 , 0 , 0 ) ) . getValue ( )
view . Direction = App . Vector ( dir [ 0 ] , dir [ 1 ] , dir [ 2 ] )
view . XDirection = App . Vector ( xdir [ 0 ] , xdir [ 1 ] , xdir [ 2 ] )
def refreshView ( self , view ) :
doc = view . Document
page = self . getViewPage ( view )
for balloon in page . Views :
if balloon . TypeId == ' TechDraw::DrawViewBalloon ' and " Assembly_handbook_Source " in balloon . PropertiesList and balloon . SourceView == view :
obj = self . getBalloonSourcePart ( balloon )
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
self . view_cache [ view ] = None
def toggleEditViewSourceParts ( self , view ) :
workbench = Gui . getWorkbench ( " AssemblyHandbookWorkbench " ) #: :type workbench: AssemblyHandbookWorkbench
button = self . getToolbarButton ( ' AHB_view_edit_source_parts ' )
if self . edited_view is None :
workbench . docLinkObserver . select_link_mode = True
self . edited_view = view
button . setChecked ( True )
button . setText ( ' End source parts edition ' )
Gui . Selection . clearSelection ( )
for obj in view . XSource :
Gui . Selection . addSelection ( obj )
else :
workbench . docLinkObserver . select_link_mode = False
Gui . Selection . clearSelection ( )
Gui . Selection . addSelection ( self . edited_view )
self . edited_view = None
button . setChecked ( False )
button . setText ( ' Edit view source parts ' )
def editViewSourceParts ( self , parts , add ) :
if self . edited_view is None : return
xsource = self . edited_view . XSource
modified = False
for part in parts :
if ( part in xsource ) != add :
if add :
xsource . append ( part )
else :
xsource . remove ( part )
modified = True
if modified :
self . edited_view . XSource = xsource
def getToolbarButton ( self , buttonName ) :
mainwin = Gui . getMainWindow ( )
toolbar = None
for tb in mainwin . findChildren ( QtGui . QToolBar ) :
if tb . objectName ( ) == ' Assembly Handbook ' :
toolbar = tb
button = None
if toolbar is not None :
for action in toolbar . actions ( ) :
if action . objectName ( ) == buttonName :
button = action
return button
def onCursorMoved ( self , view , new_pos ) :
if len ( Gui . Selection . getSelection ( ) ) == 0 : return
balloon = Gui . Selection . getSelection ( ) [ 0 ]
if balloon . TypeId != ' TechDraw::DrawViewBalloon ' or not ' Assembly_handbook_Source ' in balloon . PropertiesList : return
if balloon . SourceView != view : return
balloon . OriginX = new_pos . x
balloon . OriginY = new_pos . y
obj = self . getBalloonSourcePart ( balloon )
view = balloon . SourceView
center = self . computePartCenter ( view , obj )
balloon . Assembly_handbook_OriginOffsetX = new_pos . x - center . x
balloon . Assembly_handbook_OriginOffsetY = new_pos . y - center . y
def onBalloonSelected ( self , operation , balloon , sub , point ) :
#print(operation, obj.Name, sub, point)
if ' Assembly_handbook_Source ' in balloon . PropertiesList :
#print(operation + " " + balloon.Name)
view = balloon . SourceView
self . updateBalloonCursor ( view )
if self . enable_selected_part_highlight :
self . repaint ( view ) # disabled for now, for performance reasons
def onBalloonChanged ( self , obj , prop ) :
# Avoid reentry
if self . updating_balloon :
return
#print('Balloon changed: ' + obj.Name + '.' + prop)
if prop == ' Y ' and ' Assembly_handbook_Source ' in obj . PropertiesList :
self . updating_balloon = True
self . updateBalloon ( obj )
self . updating_balloon = False
def updateBalloon ( self , balloon ) :
workbench = Gui . getWorkbench ( " AssemblyHandbookWorkbench " ) #: :type workbench: AssemblyHandbookWorkbench
view = balloon . SourceView
obj = self . getBalloonSourcePart ( balloon )
partDisplayName = ' Inconnu ' if obj is None else self . getPartDisplayName ( obj )
objectCenterView = workbench . techDrawExtensions . computePartCenter ( view , obj )
balloon . OriginX = objectCenterView . x + balloon . Assembly_handbook_OriginOffsetX
balloon . OriginY = objectCenterView . y + balloon . Assembly_handbook_OriginOffsetY
balloon . Text = partDisplayName
balloon . ViewObject . Font = ' DejaVu Sans '
balloon . ViewObject . Fontsize = 4
balloon . BubbleShape = ' Inspection '
balloon . EndTypeScale = 1
def getBalloonSourcePart ( self , balloon ) :
try :
return balloon . Assembly_handbook_Source [ 0 ]
except :
return None
def isPartLink ( self , obj ) :
if obj . TypeId == ' App::Link ' :
return True
if obj . TypeId == ' Part::FeaturePython ' and hasattr ( obj , ' LinkedObject ' ) : # variant link
return True
return False
def getPartDisplayName ( self , obj ) :
if self . isPartLink ( obj ) :
linked_obj = obj . LinkedObject
if ' Assembly_handbook_PartDisplayName ' in linked_obj . PropertiesList :
return linked_obj . Assembly_handbook_PartDisplayName
else :
return linked_obj . Document . Name
return obj . Name
def isNewPartInView ( self , view , obj ) :
doc = view . Document
prev_view = None
if ' Assembly_handbook_PreviousStepView ' in view . PropertiesList :
prev_view = doc . getObject ( view . Assembly_handbook_PreviousStepView )
if prev_view is None :
return True
else :
if not obj in prev_view . XSource :
return True
return False
def getActivePage ( self ) :
activeView = Gui . activeView ( )
if activeView is None : return None
activePage = activeView . getPage ( ) if hasattr ( activeView , ' getPage ' ) else None
return activePage
def getViewPage ( self , view ) :
for obj in view . InList :
if obj . TypeId == ' TechDraw::DrawPage ' :
if view in obj . Views :
return obj
return None
def forceRedrawPage ( self , page , callback = None ) :
needPageUpdate = False
for view in page . Views :
if view . TypeId == ' TechDraw::DrawViewPart ' and ' Assembly_handbook_PreviousStepView ' in view . PropertiesList :
if ' Assembly_handbook_RasterView ' not in view . PropertiesList or not view . Assembly_handbook_RasterView :
needPageUpdate = True
self . refreshView ( view )
elif 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 page . KeepUpdated or not needPageUpdate :
for view in page . Views :
if view . TypeId != ' TechDraw::DrawViewPart ' :
view . recompute ( )
for view in page . Views :
if view . TypeId == ' TechDraw::DrawViewPart ' :
view . recompute ( )
self . repaint ( view )
if callback is not None :
callback ( )
else :
page . KeepUpdated = True
def restoreKeepUpdated ( ) :
page . KeepUpdated = False
for view in page . Views :
if view . TypeId == ' TechDraw::DrawViewPart ' :
self . repaint ( view )
if callback is not None :
callback ( )
QTimer . singleShot ( 10 , restoreKeepUpdated )
def computePartCenter ( self , view , obj ) :
if obj . TypeId == ' App::Link ' :
partLink = obj
objectCenterWorld = partLink . LinkPlacement . Matrix . multiply ( 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 )
else :
objectCenterWorld = obj . Shape . CenterOfGravity
''' view_cache = self.getViewCache(view)
key = ( objectCenterWorld . x , objectCenterWorld . y , objectCenterWorld . z )
projected_point = view_cache . projected_points . get ( key , None )
if projected_point is None :
# TechDraw does not expose a way to project a 3D point to 2D view coordinates ; this is a hack to get this value indirectly. The view should be hidden before calling this method, to avoid costly repaints.
vertId = view . makeCosmeticVertex3d ( objectCenterWorld )
vert = view . getCosmeticVertex ( vertId )
projected_point = vert . Point
view . removeCosmeticVertex ( vertId )
view_cache . projected_points [ key ] = projected_point
return projected_point '''
return self . projectPoint ( view , objectCenterWorld )
def projectPoint ( self , view , 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 :
vertId = view . makeCosmeticVertex3d ( App . Vector ( 0 , 0 , 0 ) )
vert = view . getCosmeticVertex ( vertId )
view_cache . projected_origin = vert . Point
view . removeCosmeticVertex ( vertId )
YDirection = view . Direction . cross ( view . XDirection )
return App . Vector ( view . XDirection . dot ( point3d ) + view_cache . projected_origin . x , YDirection . dot ( point3d ) + view_cache . projected_origin . y , 0 )
def getViewCache ( self , view ) :
cache = self . view_cache . get ( view , None )
if cache is None :
cache = ViewCache ( )
self . view_cache [ view ] = cache
return cache