@ -1,13 +1,5 @@
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 ) :
@ -97,7 +89,7 @@ class RasterView:
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 ' ] :
elif obj . TypeId in [ ' Part::Feature ' , ' Part::FeaturePython ' , ' PartDesign::Body ' , ' PartDesign::CoordinateSystem ' , ' PartDesign::Line ' , ' Part::Mirroring ' , ' Part::Cut ' , ' Part::Part2DObjectPython ' ] :
result . append ( obj )
if hasattr ( obj , ' Group ' ) :
result . extend ( self . _flatten_objects_tree ( obj . Group ) )
@ -105,10 +97,7 @@ class RasterView:
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 ' ]
return obj . TypeId in [ ' Part::Feature ' , ' Part::FeaturePython ' , ' PartDesign::Body ' , ' Part::Mirroring ' , ' Part::Cut ' , ' Part::Part2DObjectPython ' ]
def render ( self , fast_render = True ) :
from pivy import coin
@ -124,45 +113,21 @@ class RasterView:
self . init_image ( )
print_verbose ( ' Rasterizing ' + view . Label + " to " + self . image_file_name + " ... " )
print ( ' 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 :
for part in view . XSource :
link = tmp_doc . addObject ( ' App::Link ' , part . Name )
link . Label = part . Label
if part . TypeId == ' App::Link ' :
@ -173,14 +138,10 @@ class RasterView:
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 ]
@ -189,7 +150,7 @@ class RasterView:
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 :
if obj in other_objects :
is_conflicting = True
if is_conflicting :
@ -201,9 +162,6 @@ class RasterView:
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 )
@ -215,7 +173,7 @@ class RasterView:
if obj in objects_to_reset . keys ( ) :
continue
if self . _should_render ( obj ) and not obj in objects_to_hide :
if self . _should_render ( obj ) :
if not fast_render :
objects_to_reset [ obj ] = (
obj . ViewObject . Visibility ,
@ -227,12 +185,6 @@ class RasterView:
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 ,
@ -248,32 +200,14 @@ class RasterView:
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 = 3200 # todo: keep aspect ratio when we limit max image dimensions
#max_res = 1500
resolution = [
int ( viewVolume . getWidth ( ) * view . Scale * 10 ) ,
@ -287,46 +221,23 @@ class RasterView:
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 :
if self . _should_render ( obj ) :
obj . ViewObject . LineColor = props [ 1 ]
obj . ViewObject . ShapeMaterial . AmbientColor = props [ 2 ]
obj . ViewObject . ShapeMaterial . DiffuseColor = props [ 3 ]
@ -334,18 +245,14 @@ class RasterView:
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 ]
diff_source_img = composite_img . split ( ) [ 3 ]
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)
@ -384,80 +291,60 @@ class RasterView:
image . Scale = image_scale
image . X = view . X
image . Y = view . Y
image . ImageFile = self . image_file_name
image . ImageFile = self . image_file_name # TODO: see if it's possible to set a relative path
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... ' )
# 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 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 . 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 , 1 .0, 0.0 ) if link in parts else ( 1.0 , 0.0 , 1.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 = 3 .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
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 = 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 )
@ -476,19 +363,16 @@ class RasterView:
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 , 5 ) :
for y in range ( 0 , 5 ) :
if x == 2 and y == 2 : continue
if ( x - 2 ) * ( x - 2 ) + ( y - 2 ) + ( y - 2 ) > 4 : continue
kernel = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]
kernel [ y * 5 + x ] = - 1
partial_outlines = shapes_img . filter ( ImageFilter . Kernel ( ( 5 , 5 ) , kernel , 1 , 127 ) )
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 )
@ -497,7 +381,6 @@ class RasterView:
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))
@ -512,7 +395,6 @@ class RasterView:
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 ) ) ) ,
@ -521,16 +403,6 @@ class RasterView:
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 ) )