Browse Source

Added code to render outlines (needed for curved shapes like spheres and cylinders)

pull/2/head
Youen 2 years ago
parent
commit
d7f520805d
  1. 2
      ahb_cmd_new_step.py
  2. 302
      ahb_raster_view.py
  3. 2
      ahb_techdraw_extensions.py

2
ahb_cmd_new_step.py

@ -32,7 +32,7 @@ class AHB_New_Step:
if prev_view is None: if prev_view is None:
step_number = 1 step_number = 1
keep_updated = True keep_updated = False
raster_view = True raster_view = True
template_file_name = freecad_path + '/share/Mod/TechDraw/Templates/A4_Landscape_blank.svg' template_file_name = freecad_path + '/share/Mod/TechDraw/Templates/A4_Landscape_blank.svg'
parent_group = None parent_group = None

302
ahb_raster_view.py

@ -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,117 +139,264 @@ 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
# 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 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]): 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,
) )
obj.ViewObject.Visibility = False obj.ViewObject.Visibility = False
docView = Gui.getDocument(doc.Name).mdiViewsOfType('Gui::View3DInventor')[0] tmp_doc_view = Gui.getDocument(tmp_doc.Name).mdiViewsOfType('Gui::View3DInventor')[0]
cam = docView.getCameraNode() 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(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)
docView.fitAll() tmp_doc_view.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:
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 = 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)) # remove the temporary document
#debugPoint(App.Vector(-12.5, -1387.5, 25.0)) App.closeDocument(tmp_doc.Name)
#debugPoint(App.Vector(131.23702882966705, -655.0000021095163, 145.21130178331268))
# Crop the image, which is also used to deduce the center of the source view
img.save(self.image_file_name) original_size = composite_img.size
sb_offset = viewVolume.projectToScreen(coin.SbVec3f(0,0,0)) bg = Image.new(composite_img.mode, composite_img.size, '#ffffff') # fills an image with the background color
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) diff = ImageChops.difference(composite_img, bg) # diff between the actual image and the background color
self.image_view.Assembly_handbook_ViewVolumeOffset = App.Vector(sb_offset[0] - crop_offset.x, sb_offset[1] + crop_offset.y, sb_offset[2]) bbox = diff.getbbox() # finds border size (non-black portion of the image)
composite_img = composite_img.crop(bbox)
self._precompute_image_projection()
'''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)
p2dA = self.project3DPointToImageView(App.Vector(0,0,0)) len = 100
p2dB = self.project3DPointToImageView(view.XDirection) draw.line([(pp.x, pp.y-len), (pp.x, pp.y+len)], fill=128, width = 7)
imageScale = view.Scale / (p2dB.x - p2dA.x) / original_size[0] * 10 draw.line([(pp.x-len, pp.y), (pp.x+len, pp.y)], fill=128, width = 7)
#print('imageScale', imageScale)
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))'''
App.closeDocument(doc.Name) 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 = self.image_view
image.ImageFile = "" image.ImageFile = ""
image.Scale = imageScale image.Scale = image_scale
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()

2
ahb_techdraw_extensions.py

@ -613,7 +613,7 @@ class TechDrawExtensions:
return self.projectPoint(view, objectCenterWorld) return self.projectPoint(view, objectCenterWorld)
def projectPoint(self, view, point3d): def projectPoint(self, view, point3d):
if 'Assembly_handbook_RasterView' in view.PropertiesList or view.Assembly_handbook_RasterView: if 'Assembly_handbook_RasterView' in view.PropertiesList and view.Assembly_handbook_RasterView:
from ahb_raster_view import RasterView from ahb_raster_view import RasterView
raster_view = RasterView(view) raster_view = RasterView(view)
if raster_view.init_image_projection(): if raster_view.init_image_projection():

Loading…
Cancel
Save