@ -1,5 +1,13 @@
import FreeCAD as App
import FreeCAD as App
import FreeCADGui as Gui
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 :
class RasterView :
def __init__ ( self , view ) :
def __init__ ( self , view ) :
@ -116,7 +124,7 @@ class RasterView:
self . init_image ( )
self . init_image ( )
print ( ' Rasterizing ' + view . Label + " to " + self . image_file_name + " ... " )
print_verbose ( ' Rasterizing ' + view . Label + " to " + self . image_file_name + " ... " )
dir = os . path . dirname ( self . image_file_name )
dir = os . path . dirname ( self . image_file_name )
if not os . path . exists ( dir ) :
if not os . path . exists ( dir ) :
@ -127,6 +135,7 @@ class RasterView:
objects_to_reset = { }
objects_to_reset = { }
duplicated_parts = { }
duplicated_parts = { }
try :
try :
print_verbose ( " Preparing scene... " )
# construct new scene with links to the parts we want
# construct new scene with links to the parts we want
sceneGroup = tmp_doc . addObject ( ' App::DocumentObjectGroup ' , ' Scene ' )
sceneGroup = tmp_doc . addObject ( ' App::DocumentObjectGroup ' , ' Scene ' )
prev_parts = [ ]
prev_parts = [ ]
@ -259,20 +268,25 @@ class RasterView:
resolution [ 1 ] = int ( max_res )
resolution [ 1 ] = int ( max_res )
if fast_render :
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 ) , [ ] )
composite_img = self . _render_lines ( tmp_doc , resolution , prev_parts + new_parts , ( 0.0 , 0.0 , 0.0 ) , [ ] )
else :
else :
# render old parts in gray lines
# 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 )
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)
# 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 )
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
# create the composite image
print_verbose ( " Compositing images... " )
composite_img = prev_parts_img . copy ( )
composite_img = prev_parts_img . copy ( )
composite_img . paste ( new_parts_img , None , new_parts_img )
composite_img . paste ( new_parts_img , None , new_parts_img )
# Optimize the image to reduce storage size
# Optimize the image to reduce storage size
if not fast_render :
if not fast_render :
print_verbose ( " Optimizing PNG size... " )
num_colors = 32
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
# All-or-nothing alpha: we use a white background and only make pixels fully transparent where alpha is zero, to not loose antialiasing
@ -286,6 +300,7 @@ class RasterView:
composite_img = composite_img . quantize ( colors = num_colors , dither = Image . Dither . NONE )
composite_img = composite_img . quantize ( colors = num_colors , dither = Image . Dither . NONE )
finally :
finally :
print_verbose ( " Cleaning scene... " )
#raise Exception("test")
#raise Exception("test")
# restore properties on objects we have modified
# restore properties on objects we have modified
for obj , props in objects_to_reset . items ( ) :
for obj , props in objects_to_reset . items ( ) :
@ -302,6 +317,8 @@ class RasterView:
# remove the temporary document
# remove the temporary document
App . closeDocument ( tmp_doc . Name )
App . closeDocument ( tmp_doc . Name )
print_verbose ( " Finalizing view... " )
# Crop the image, which is also used to deduce the center of the source view
# Crop the image, which is also used to deduce the center of the source view
original_size = composite_img . size
original_size = composite_img . size
@ -350,6 +367,8 @@ class RasterView:
image . Height = composite_img . size [ 1 ] * image_scale / 10.0 * 1.01
image . Height = composite_img . size [ 1 ] * image_scale / 10.0 * 1.01
image . recompute ( )
image . recompute ( )
print_verbose ( " Done " )
def _render_lines ( self , doc , resolution , parts , line_color , masking_parts , fast_render = True ) :
def _render_lines ( self , doc , resolution , parts , line_color , masking_parts , fast_render = True ) :
import tempfile
import tempfile
from PIL import Image , ImageDraw , ImageFilter
from PIL import Image , ImageDraw , ImageFilter
@ -358,12 +377,17 @@ class RasterView:
# render lines in black, background in red, fill shapes in green
# 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
# the green band contains the lines images, the red band contains the inverted alpha layer
configured = [ ]
print_verbose ( ' Preparing objects for line rendering... ' )
for link in doc . findObjects ( ) :
for link in doc . findObjects ( ) :
if link in parts or link in masking_parts :
if link in parts or link in masking_parts :
link . ViewObject . Visibility = True
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
# 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 ] ) :
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 :
if self . _should_render ( obj ) and not fast_render :
obj . ViewObject . LineColor = ( 0.0 , 0.0 , 0.0 , 0.0 ) if link in parts else ( 1.0 , 0.0 , 1.0 )
obj . ViewObject . 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 . AmbientColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
@ -377,6 +401,7 @@ class RasterView:
else :
else :
link . ViewObject . Visibility = False
link . ViewObject . Visibility = False
print_verbose ( ' Rendering lines... ' )
temp_file_name = tempfile . gettempdir ( ) + " /ahb_temp_image.png "
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
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 = self . _read_image ( temp_file_name )
@ -390,14 +415,20 @@ class RasterView:
# Render all shapes with different colors, in order to extract outlines (where color changes)
# 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
# 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
# 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
step = 8
r = step
r = step
g = step
g = step
b = step
b = step
configured = [ ]
for link in doc . findObjects ( ) :
for link in doc . findObjects ( ) :
if link in parts or link in masking_parts :
if link in parts or link in masking_parts :
for obj in self . _flatten_objects_tree ( [ link ] ) :
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 ' :
if self . _should_render ( obj ) and obj . TypeId != ' Part::Part2DObjectPython ' :
configured . append ( obj )
obj . ViewObject . DisplayMode = ' Shaded '
obj . ViewObject . DisplayMode = ' Shaded '
obj . ViewObject . ShapeMaterial . AmbientColor = ( 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 . DiffuseColor = ( 0.0 , 0.0 , 0.0 , 0.0 )
@ -416,9 +447,11 @@ class RasterView:
else :
else :
obj . ViewObject . Visibility = False
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
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 )
shapes_img = self . _read_image ( temp_file_name )
print_verbose ( ' Extracting outlines... ' )
outlines_img = None
outlines_img = None
for x in range ( 0 , 3 ) :
for x in range ( 0 , 3 ) :
for y in range ( 0 , 3 ) :
for y in range ( 0 , 3 ) :
@ -434,6 +467,7 @@ class RasterView:
else :
else :
outlines_img . paste ( partial_outlines , None , partial_outlines . point ( lambda p : 0 if p == 255 else 255 ) )
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 = 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 ( 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))
#lines_fullres.paste(255, alpha_fullres.point(lambda p: 255 if p == 0 else 0))
@ -448,6 +482,7 @@ class RasterView:
alpha_img = alpha_img . point ( lambda p : 0 if p == 0 else 255 )
alpha_img = alpha_img . point ( lambda p : 0 if p == 0 else 255 )
# colorize final image
# colorize final image
print_verbose ( ' Colorizing image... ' )
fill_color = ( 1.0 , 1.0 , 1.0 )
fill_color = ( 1.0 , 1.0 , 1.0 )
result = Image . merge ( " RGBA " , [
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 [ 0 ] * p + line_color [ 0 ] * ( 255.0 - p ) ) ) ,