@ -89,7 +89,7 @@ class RasterView:
result . extend ( self . _flatten_objects_tree ( [ obj . LinkedObject ] ) )
result . extend ( self . _flatten_objects_tree ( [ obj . LinkedObject ] ) )
elif obj . TypeId in [ ' App::Part ' , ' App::DocumentObjectGroup ' ] :
elif obj . TypeId in [ ' App::Part ' , ' App::DocumentObjectGroup ' ] :
result . extend ( self . _flatten_objects_tree ( obj . Group ) )
result . extend ( self . _flatten_objects_tree ( obj . Group ) )
elif obj . TypeId in [ ' Part::Feature ' , ' Part::FeaturePython ' , ' PartDesign::Body ' , ' PartDesign::CoordinateSystem ' , ' PartDesign::Line ' , ' Part::Mirroring ' , ' Part::Cut ' ] :
elif obj . TypeId in [ ' Part::Feature ' , ' Part::FeaturePython ' , ' PartDesign::Body ' , ' PartDesign::CoordinateSystem ' , ' PartDesign::Line ' , ' Part::Mirroring ' , ' Part::Cut ' , ' Part::Part2DObjectPython ' ] :
result . append ( obj )
result . append ( obj )
if hasattr ( obj , ' Group ' ) :
if hasattr ( obj , ' Group ' ) :
result . extend ( self . _flatten_objects_tree ( obj . Group ) )
result . extend ( self . _flatten_objects_tree ( obj . Group ) )
@ -97,12 +97,15 @@ class RasterView:
return result
return result
def _should_render ( self , obj ) :
def _should_render ( self , obj ) :
return obj . TypeId in [ ' Part::Feature ' , ' Part::FeaturePython ' , ' PartDesign::Body ' , ' Part::Mirroring ' , ' Part::Cut ' ]
return obj . TypeId in [ ' Part::Feature ' , ' Part::FeaturePython ' , ' PartDesign::Body ' , ' Part::Mirroring ' , ' Part::Cut ' , ' Part::Part2DObjectPython ' ]
def render ( self ) :
def render ( self ) :
from pivy import coin
from pivy import coin
import os
import os
from PIL import Image , ImageDraw , ImageChops
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
workbench = Gui . getWorkbench ( " AssemblyHandbookWorkbench " ) #: :type workbench: AssemblyHandbookWorkbench
@ -116,12 +119,16 @@ class RasterView:
if not os . path . exists ( dir ) :
if not os . path . exists ( dir ) :
os . makedirs ( dir )
os . makedirs ( dir )
doc = App . newDocument ( ' tmp_raster ' , hidden = False , temp = False )
tmp_ doc = App . newDocument ( ' tmp_raster ' , hidden = False , temp = False )
objects_to_reset = { }
objects_to_reset = { }
duplicated_parts = { }
try :
try :
# construct new scene with links to the parts we want
prev_parts = [ ]
new_parts = [ ]
for part in view . XSource :
for part in view . XSource :
link = doc . addObject ( ' App::Link ' , part . Name )
link = tmp_ doc. addObject ( ' App::Link ' , part . Name )
link . Label = part . Label
link . Label = part . Label
if part . TypeId == ' App::Link ' :
if part . TypeId == ' App::Link ' :
link . LinkedObject = part . LinkedObject
link . LinkedObject = part . LinkedObject
@ -132,35 +139,50 @@ class RasterView:
else :
else :
link . LinkedObject = part
link . LinkedObject = part
new_part = workbench . techDrawExtensions . isNewPartInView ( view , part )
is_ new_part = workbench . techDrawExtensions . isNewPartInView ( view , part )
link . ViewObject . OverrideMaterial = True
is_conflicting = False
link . ViewObject . ShapeMaterial . DiffuseColor = ( 0.0 , 0.0 , 0.0 , 0.0 ) if new_part else ( 0.5 , 0.5 , 0.5 , 0.0 ) # this actually changes the line color
if link . LinkedObject in duplicated_parts . keys ( ) :
link . LinkedObject = duplicated_parts [ link . LinkedObject ]
else :
other_parts = prev_parts if is_new_part else new_parts
for other_part in other_parts :
other_objects = self . _flatten_objects_tree ( [ other_part ] )
for obj in self . _flatten_objects_tree ( [ link ] ) :
if obj in other_objects :
is_conflicting = True
if is_conflicting :
# We must copy the part because otherwise we can't control the emissive color (link material override does not work for emissive color)
#print("conflict: " + link.LinkedObject.Document.Name + '#' + link.LinkedObject.Label)
shape_copy = Part . getShape ( link . LinkedObject , ' ' , needSubElement = False , refine = False )
part_copy = tmp_doc . addObject ( ' Part::Feature ' , ' ShapeCopy ' )
part_copy . Shape = shape_copy
part_copy . Label = part . Label
duplicated_parts [ link . LinkedObject ] = part_copy
link . LinkedObject = part_copy
if is_new_part :
new_parts . append ( link )
else :
prev_parts . append ( link )
# 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
# 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 ] ) :
for obj in self . _flatten_objects_tree ( [ link ] ) :
if obj in objects_to_reset . keys ( ) :
if obj in objects_to_reset . keys ( ) :
continue
continue
if self . _should_render ( obj ) :
if self . _should_render ( obj ) :
objects_to_reset [ obj ] = (
objects_to_reset [ obj ] = (
obj . ViewObject . Visibility ,
obj . ViewObject . LineColor ,
obj . ViewObject . LineColor ,
obj . ViewObject . ShapeMaterial . AmbientColor ,
obj . ViewObject . ShapeMaterial . AmbientColor ,
obj . ViewObject . ShapeMaterial . DiffuseColor ,
obj . ViewObject . ShapeMaterial . DiffuseColor ,
obj . ViewObject . ShapeMaterial . SpecularColor ,
obj . ViewObject . ShapeMaterial . SpecularColor ,
obj . ViewObject . ShapeMaterial . EmissiveColor ,
obj . ViewObject . ShapeMaterial . EmissiveColor ,
obj . ViewObject . LineWidth
obj . ViewObject . LineWidth ,
obj . ViewObject . DisplayMode
)
)
obj . ViewObject . LineColor = ( 0.0 , 0.0 , 0.0 , 0.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 = ( 1.0 , 1.0 , 1.0 , 0.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 :
else :
objects_to_reset [ obj ] = (
objects_to_reset [ obj ] = (
obj . ViewObject . Visibility ,
obj . ViewObject . Visibility ,
@ -168,81 +190,213 @@ class RasterView:
obj . ViewObject . Visibility = False
obj . ViewObject . Visibility = False
docV iew = Gui . getDocument ( doc . Name ) . mdiViewsOfType ( ' Gui::View3DInventor ' ) [ 0 ]
tmp_doc_v iew = Gui . getDocument ( tmp_ doc. Name ) . mdiViewsOfType ( ' Gui::View3DInventor ' ) [ 0 ]
cam = docV iew. getCameraNode ( )
cam = tmp_doc_v iew. 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 ( 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 ) )
rot * = coin . SbRotation ( coin . SbVec3f ( 0 , 0 , 1 ) , coin . SbVec3f ( view . Direction . x , view . Direction . y , view . Direction . z ) )
cam . orientation . setValue ( rot )
cam . orientation . setValue ( rot )
docV iew. fitAll ( )
tmp_doc_v iew. fitAll ( )
viewVolume = cam . getViewVolume ( 0.0 )
viewVolume = cam . getViewVolume ( 0.0 )
self . image_view . Assembly_handbook_ViewVolumeWidth = viewVolume . getWidth ( )
self . image_view . Assembly_handbook_ViewVolumeWidth = viewVolume . getWidth ( )
self . image_view . Assembly_handbook_ViewVolumeHeight = viewVolume . getHeight ( )
self . image_view . Assembly_handbook_ViewVolumeHeight = viewVolume . getHeight ( )
self . image_view . Assembly_handbook_ViewVolumeDepth = viewVolume . getDepth ( )
self . image_view . Assembly_handbook_ViewVolumeDepth = viewVolume . getDepth ( )
max_res = 4096
max_res = 3200 # todo: keep aspect ratio when we limit max image dimensions
docView . saveImage ( self . image_file_name , int ( min ( max_res , viewVolume . getWidth ( ) * view . Scale * 10 ) ) , int ( min ( max_res , viewVolume . getHeight ( ) * view . Scale * 10 ) ) , " #ffffff " )
#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 )
# render old parts in gray lines
prev_parts_img = self . _render_lines ( tmp_doc , resolution , prev_parts , ( 0.6 , 0.6 , 0.6 ) , [ ] )
# render new parts in black lines (old parts can mask them)
new_parts_img = self . _render_lines ( tmp_doc , resolution , new_parts , ( 0.0 , 0.0 , 0.0 ) , prev_parts )
# create the composite image
composite_img = prev_parts_img . copy ( )
composite_img . paste ( new_parts_img , None , new_parts_img )
#composite_img = new_parts_img.copy()
finally :
finally :
# restore properties on objects we have modified
for obj , props in objects_to_reset . items ( ) :
for obj , props in objects_to_reset . items ( ) :
obj . ViewObject . Visibility = props [ 0 ]
if self . _should_render ( obj ) :
if self . _should_render ( obj ) :
obj . ViewObject . LineColor = props [ 0 ]
obj . ViewObject . LineColor = props [ 1 ]
obj . ViewObject . ShapeMaterial . AmbientColor = props [ 1 ]
obj . ViewObject . ShapeMaterial . AmbientColor = props [ 2 ]
obj . ViewObject . ShapeMaterial . DiffuseColor = props [ 2 ]
obj . ViewObject . ShapeMaterial . DiffuseColor = props [ 3 ]
obj . ViewObject . ShapeMaterial . SpecularColor = props [ 3 ]
obj . ViewObject . ShapeMaterial . SpecularColor = props [ 4 ]
obj . ViewObject . ShapeMaterial . EmissiveColor = props [ 4 ]
obj . ViewObject . ShapeMaterial . EmissiveColor = props [ 5 ]
obj . ViewObject . LineWidth = props [ 5 ]
obj . ViewObject . LineWidth = props [ 6 ]
else :
obj . ViewObject . DisplayMode = props [ 7 ]
obj . ViewObject . Visibility = props [ 0 ]
with Image . open ( self . image_file_name ) as img :
# remove the temporary document
original_size = img . size
App . closeDocument ( tmp_doc . Name )
bg = Image . new ( img . mode , img . size , ' #ffffff ' ) # fills an image with the background color
# Crop the image, which is also used to deduce the center of the source view
diff = ImageChops . difference ( img , bg ) # diff between the actual image and the background color
original_size = composite_img . size
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 )
bg = Image . new ( composite_img . mode , composite_img . size , ' #ffffff ' ) # fills an image with the background color
diff = ImageChops . difference ( composite_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 )
def debugPoint ( p3d ) :
''' draw = ImageDraw.Draw(composite_img)
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
def debugPoint ( p3d ) :
draw . line ( [ ( pp . x , pp . y - len ) , ( pp . x , pp . y + len ) ] , fill = 128 , width = 7 )
p2d = self . project3DPointToImageView ( p3d )
draw . line ( [ ( pp . x - len , pp . y ) , ( pp . x + len , pp . y ) ] , fill = 128 , width = 7 )
pp = App . Vector ( p2d . x * original_size [ 0 ] - bbox [ 0 ] , ( 1.0 - p2d . y ) * original_size [ 1 ] - bbox [ 1 ] )
#print('pp', pp)
#debugPoint(App.Vector(-12.5, 37.5, 25.0))
len = 100
#debugPoint(App.Vector(-12.5, -1387.5, 25.0) )
draw . line ( [ ( pp . x , pp . y - len ) , ( pp . x , pp . y + len ) ] , fill = 128 , width = 7 )
#debugPoint(App.Vector(131.23702882966705, -655.0000021095163, 145.21130178331268) )
draw . line ( [ ( pp . x - len , pp . y ) , ( pp . x + len , pp . y ) ] , fill = 128 , width = 7 )
img . save ( self . image_file_name )
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 ) ) '''
sb_offset = viewVolume . projectToScreen ( coin . SbVec3f ( 0 , 0 , 0 ) )
composite_img . save ( self . image_file_name )
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 ( )
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 ] )
p2dA = self . project3DPointToImageView ( App . Vector ( 0 , 0 , 0 ) )
self . _precompute_image_projection ( )
p2dB = self . project3DPointToImageView ( view . XDirection )
imageScale = view . Scale / ( p2dB . x - p2dA . x ) / original_size [ 0 ] * 10
#print('imageScale', imageScale)
App . closeDocument ( doc . Name )
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 = self . image_view
image . ImageFile = " "
image . ImageFile = " "
image . Scale = imageS cale
image . Scale = image_s cale
image . X = view . X
image . X = view . X
image . Y = view . Y
image . Y = view . Y
image . ImageFile = self . image_file_name # TODO: see if it's possible to set a relative path
image . ImageFile = self . image_file_name # TODO: see if it's possible to set a relative path
image . recompute ( )
image . recompute ( )
def _render_lines ( self , doc , resolution , parts , line_color , masking_parts ) :
import tempfile
from PIL import Image , ImageDraw , ImageFilter
doc_view = Gui . getDocument ( doc . Name ) . mdiViewsOfType ( ' Gui::View3DInventor ' ) [ 0 ]
# render lines in black, background in red, fill shapes in green
# the green band contains the lines images, the red band contains the inverted alpha layer
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 self . _should_render ( obj ) :
obj . ViewObject . LineColor = ( 0.0 , 0.0 , 0.0 , 0.0 ) if link in parts else ( 1.0 , 0.0 , 1.0 )
obj . ViewObject . ShapeMaterial . AmbientColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
obj . ViewObject . ShapeMaterial . DiffuseColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
obj . ViewObject . ShapeMaterial . SpecularColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
obj . ViewObject . ShapeMaterial . EmissiveColor = ( 0.0 , 1.0 , 0.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
temp_file_name = tempfile . gettempdir ( ) + " /ahb_temp_image.png "
doc_view . saveImage ( temp_file_name , resolution [ 0 ] + 2 , resolution [ 1 ] + 2 , " #ff0000 " ) # we add 1 pixel border that we will need to crop later
lines_bands_img = self . _read_image ( temp_file_name )
lines_bands_img . save ( ' /home/youen/dev/vhelio/tmp/tmp.png ' )
lines_bands = lines_bands_img . split ( )
lines_img = lines_bands [ 1 ]
alpha_img = lines_bands [ 0 ] . point ( lambda p : 255 - p )
# Render all shapes with different colors, in order to extract outlines (where color changes)
# This is needed because FreeCAD does not render lines on the boundary of curve shapes, such as spheres or cylinders
# The technique could be improved by using the depth buffer instead, in order to detect boundaries within the same object
step = 8
r = step
g = step
b = step
for link in doc . findObjects ( ) :
if link in parts or link in masking_parts :
for obj in self . _flatten_objects_tree ( [ link ] ) :
if self . _should_render ( obj ) and obj . TypeId != ' Part::Part2DObjectPython ' :
obj . ViewObject . DisplayMode = ' Shaded '
obj . ViewObject . ShapeMaterial . AmbientColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
obj . ViewObject . ShapeMaterial . DiffuseColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
obj . ViewObject . ShapeMaterial . SpecularColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
obj . ViewObject . ShapeMaterial . EmissiveColor = ( r / 255.0 , g / 255.0 , b / 255.0 , 0.0 ) if link in parts else ( 1.0 , 1.0 , 1.0 , 0.0 )
r = r + step
if r > = 256 - step :
r = step
g = g + step
if g > = 256 - step :
g = step
b = b + step
if b > = 256 - step :
b = step
else :
obj . ViewObject . Visibility = False
doc_view . saveImage ( temp_file_name , ( resolution [ 0 ] + 2 ) * 2 , ( resolution [ 1 ] + 2 ) * 2 , " #ffffff " ) # shapes are rendered at twice the resolution for antialiasing
shapes_img = self . _read_image ( temp_file_name )
outlines_img = None
for x in range ( 0 , 3 ) :
for y in range ( 0 , 3 ) :
if x == 1 and y == 1 : continue
kernel = [ 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 ]
kernel [ y * 3 + x ] = - 1
partial_outlines = shapes_img . filter ( ImageFilter . Kernel ( ( 3 , 3 ) , kernel , 1 , 127 ) )
partial_outlines = partial_outlines . point ( lambda p : 255 if p == 127 else 0 )
partial_outlines = partial_outlines . convert ( " L " )
partial_outlines = partial_outlines . point ( lambda p : 255 if p == 255 else 0 )
if outlines_img is None :
outlines_img = partial_outlines
else :
outlines_img . paste ( partial_outlines , None , partial_outlines . point ( lambda p : 0 if p == 255 else 255 ) )
lines_fullres = lines_img . resize ( outlines_img . size , Image . NEAREST )
lines_fullres . paste ( outlines_img , None , outlines_img . point ( lambda p : 255 if p == 0 else 0 ) )
#lines_fullres.paste(255, alpha_fullres.point(lambda p: 255 if p == 0 else 0))
all_lines = lines_fullres . resize ( lines_img . size , Image . BILINEAR )
#all_lines = lines_img.copy()
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 )
# colorize final 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
] )
# 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 ( )