class AreaLight(PointableFeatureMixin, FeatureBase): """An area light.""" VIEWPROVIDER = "ViewProviderAreaLight" PROPERTIES = { "SizeU": Prop( "App::PropertyLength", "Light", QT_TRANSLATE_NOOP("Render", "Size on U axis"), 4.0, ), "SizeV": Prop( "App::PropertyLength", "Light", QT_TRANSLATE_NOOP("Render", "Size on V axis"), 2.0, ), "Color": Prop( "App::PropertyColor", "Light", QT_TRANSLATE_NOOP("Render", "Color of light"), (1.0, 1.0, 1.0), ), "Power": Prop( "App::PropertyFloat", "Light", QT_TRANSLATE_NOOP("Render", "Rendering power"), 60.0, ), "Transparent": Prop( "App::PropertyBool", "Light", QT_TRANSLATE_NOOP("Render", "Area light transparency"), False, ), }
class PointLight(FeatureBase): """A point light object.""" VIEWPROVIDER = "ViewProviderPointLight" PROPERTIES = { "Location": Prop( "App::PropertyVector", "Light", QT_TRANSLATE_NOOP("Render", "Location of light"), (0, 0, 15), ), "Color": Prop( "App::PropertyColor", "Light", QT_TRANSLATE_NOOP("Render", "Color of light"), (1.0, 1.0, 1.0), ), "Power": Prop( "App::PropertyFloat", "Light", QT_TRANSLATE_NOOP("Render", "Rendering power"), 60.0, ), "Radius": Prop( "App::PropertyLength", "Light", QT_TRANSLATE_NOOP( "Render", "Light representation radius.\n" "Note: This parameter has no impact " "on rendering", ), 2.0, ), }
class SunskyLight(FeatureBase): """A sun+sky light - Hosek-Wilkie.""" VIEWPROVIDER = "ViewProviderSunskyLight" PROPERTIES = { "SunDirection": Prop( "App::PropertyVector", "Light", QT_TRANSLATE_NOOP( "Render", "Direction of sun from observer's point of view " "-- (0,0,1) is zenith", ), (1, 1, 1), ), "Turbidity": Prop( "App::PropertyFloat", "Light", QT_TRANSLATE_NOOP( "Render", "Atmospheric haziness (turbidity can go from 2.0 to 30+. 2-6 " "are most useful for clear days)", ), 2.0, ), "GroundAlbedo": Prop( "App::PropertyFloatConstraint", "Light", QT_TRANSLATE_NOOP( "Render", "Ground albedo (reflection coefficient of the ground)", ), (0.3, 0.0, 1.0, 0.01), ), }
class ImageLight(FeatureBase): """An image-based light.""" VIEWPROVIDER = "ViewProviderImageLight" PROPERTIES = { "ImageFile": Prop( "App::PropertyFileIncluded", "Light", QT_TRANSLATE_NOOP("Render", "Image file (included in document)"), "", ), }
class Camera(PointableFeatureMixin, FeatureBase): """A camera for rendering. This object allows to record camera settings from the Coin camera, and to reuse them for rendering. Camera Orientation is defined by a Rotation Axis and a Rotation Angle, applied to 'default camera'. Default camera looks from (0,0,1) towards the origin (target is (0,0,-1), and the up direction is (0,1,0). For more information, see Coin documentation, Camera section. <https://developer.openinventor.com/UserGuides/Oiv9/Inventor_Mentor/Cameras_and_Lights/Cameras.html> """ VIEWPROVIDER = "ViewProviderCamera" PROPERTIES = { "Projection": Prop( "App::PropertyEnumeration", "Camera", QT_TRANSLATE_NOOP("Render", "Type of projection: Perspective/Orthographic"), ("Perspective", "Orthographic"), ), "ViewportMapping": Prop( "App::PropertyEnumeration", "Camera", QT_TRANSLATE_NOOP("Render", "(See Coin documentation)"), VIEWPORTMAPPINGENUM, ), "AspectRatio": Prop( "App::PropertyFloat", "Camera", QT_TRANSLATE_NOOP("Render", "Ratio width/height of the camera."), 1.0, ), "NearDistance": Prop( "App::PropertyDistance", "Camera", QT_TRANSLATE_NOOP("Render", "Near distance, for clipping"), 0.0, ), "FarDistance": Prop( "App::PropertyDistance", "Camera", QT_TRANSLATE_NOOP("Render", "Far distance, for clipping"), 200.0, ), "FocalDistance": Prop( "App::PropertyDistance", "Camera", QT_TRANSLATE_NOOP("Render", "Focal distance"), 100.0, ), "Height": Prop( "App::PropertyLength", "Camera", QT_TRANSLATE_NOOP("Render", "Height, for orthographic camera"), 5.0, ), "HeightAngle": Prop( "App::PropertyAngle", "Camera", QT_TRANSLATE_NOOP( "Render", "Height angle, for perspective camera, in " "degrees. Important: This value will be sent as " "'Field of View' to the renderers.", ), 60, ), } def on_create_cb(self, fpo, viewp, **kwargs): """Complete 'create' (callback).""" if App.GuiUp: viewp.set_camera_from_gui() else: set_cam_from_coin_string(fpo, DEFAULT_CAMERA_STRING)
class View(FeatureBase): """A rendering view of a FreeCAD object. 'create' factory method should be provided a project and a source (the object for which the view is created), via keyword arguments. Please note that providing a project is mandatory to create a view: no rendering view should be created "off-ground". """ VIEWPROVIDER = "ViewProviderView" PROPERTIES = { "Source": Prop( "App::PropertyLink", "Render", QT_TRANSLATE_NOOP("App::Property", "The source object of this view"), None, 0, ), "Material": Prop( "App::PropertyLink", "Render", QT_TRANSLATE_NOOP("App::Property", "The material of this view"), None, 0, ), "ViewResult": Prop( "App::PropertyString", "Render", QT_TRANSLATE_NOOP("App::Property", "The rendering output of this view"), "", 0, ), } @classmethod def pre_create_cb(cls, **kwargs): """Precede the operation of 'create' (callback).""" project = kwargs["project"] # Note: 'project' kw argument is mandatory source = kwargs["source"] # Note: 'source' kw argument is mandatory assert project.Document == source.Document or FCDVERSION >= ( 0, 19, ), "Unable to create View: Project and Object not in same document" def on_create_cb(self, fpo, viewp, **kwargs): """Complete 'create' (callback).""" project = kwargs["project"] source = kwargs["source"] fpo.Label = View.view_label(source, project) if project.Document != source.Document: # Cross-link: Transform Source into App::PropertyXLink name = "Source" fpo.removeProperty(name) spec = self.PROPERTIES[name] prop = fpo.addProperty("App::PropertyXLink", name, spec.Group, spec.Doc, 0) setattr(prop, name, spec.Default) fpo.setEditorMode(name, spec.EditorMode) fpo.Source = source project.addObject(fpo) def execute(self, obj): # pylint: disable=no-self-use """Respond to document recomputation event (callback, mandatory). Write or rewrite the ViewResult string if containing project is not 'delayed build' """ # Find containing project and check DelayedBuild is false try: proj = next(x for x in obj.InListRecursive if RendererHandler.is_project(x)) assert not proj.DelayedBuild except (StopIteration, AttributeError, AssertionError): return # Get object rendering string and set ViewResult property renderer = RendererHandler( rdrname=proj.Renderer, linear_deflection=proj.LinearDeflection, angular_deflection=proj.AngularDeflection, transparency_boost=proj.TransparencySensitivity, ) obj.ViewResult = renderer.get_rendering_string(obj) @staticmethod def view_label(obj, proj, is_group=False): """Give a standard label for the view of an object. Args: obj -- object which the view is built for proj -- project which the view will be inserted in is_group -- flag to indicate whether the view is a group Both obj and proj should have valid Label attributes """ obj_label = str(obj.Label) proj_label = str(proj.Label) proj_label = proj_label.replace(" ", "") fmt = "{o}Group@{p}" if is_group else "{o}@{p}" res = fmt.format(o=obj_label, p=proj_label) return res
class Project(FeatureBase): """A rendering project.""" VIEWPROVIDER = "ViewProviderProject" PROPERTIES = { "Renderer": Prop( "App::PropertyString", "Base", QT_TRANSLATE_NOOP( "App::Property", "The name of the raytracing engine to use", ), "", ), "DelayedBuild": Prop( "App::PropertyBool", "Output", QT_TRANSLATE_NOOP( "App::Property", "If true, the views will be updated on render only", ), True, ), "Template": Prop( "App::PropertyString", "Base", QT_TRANSLATE_NOOP( "App::Property", "The template to be used by this rendering " "(use Project's context menu to modify)", ), "", 1, ), "PageResult": Prop( "App::PropertyFileIncluded", "Render", QT_TRANSLATE_NOOP( "App::Property", "The result file to be sent to the renderer" ), "", 2, ), "RenderWidth": Prop( "App::PropertyInteger", "Render", QT_TRANSLATE_NOOP( "App::Property", "The width of the rendered image in pixels" ), PARAMS.GetInt("RenderWidth", 800), ), "RenderHeight": Prop( "App::PropertyInteger", "Render", QT_TRANSLATE_NOOP( "App::Property", "The height of the rendered image in pixels" ), PARAMS.GetInt("RenderHeight", 600), ), "GroundPlane": Prop( "App::PropertyBool", "Ground Plane", QT_TRANSLATE_NOOP( "App::Property", "If true, a default ground plane will be added to the scene", ), False, ), "GroundPlaneZ": Prop( "App::PropertyDistance", "Ground Plane", QT_TRANSLATE_NOOP("App::Property", "Z position for ground plane"), 0, ), "GroundPlaneColor": Prop( "App::PropertyColor", "Ground Plane", QT_TRANSLATE_NOOP("App::Property", "Ground plane color"), (0.8, 0.8, 0.8), ), "GroundPlaneSizeFactor": Prop( "App::PropertyFloat", "Ground Plane", QT_TRANSLATE_NOOP("App::Property", "Ground plane size factor"), 1.0, ), "OutputImage": Prop( "App::PropertyFile", "Output", QT_TRANSLATE_NOOP( "App::Property", "The image saved by this render" ), "", ), "OpenAfterRender": Prop( "App::PropertyBool", "Output", QT_TRANSLATE_NOOP( "App::Property", "If true, the rendered image is opened in FreeCAD after " "the rendering is finished", ), True, ), "LinearDeflection": Prop( "App::PropertyFloat", "Mesher", QT_TRANSLATE_NOOP( "App::Property", "Linear deflection for the mesher: " "The maximum linear deviation of a mesh section from the " "surface of the object.", ), 0.1, ), "AngularDeflection": Prop( "App::PropertyFloat", "Mesher", QT_TRANSLATE_NOOP( "App::Property", "Angular deflection for the mesher: " "The maximum angular deviation from one mesh section to " "the next, in radians. This setting is used when meshing " "curved surfaces.", ), math.pi / 6, ), "TransparencySensitivity": Prop( "App::PropertyIntegerConstraint", "Render", QT_TRANSLATE_NOOP( "App::Property", "Overweigh transparency in rendering " "(0=None (default), 10=Very high)." "When this parameter is set, low transparency ratios will " "be rendered more transparent. NB: This parameter affects " "only implicit materials (generated via Shape " "Appearance), not explicit materials (defined via Material" " property).", ), (0, 0, 10, 1), ), } ON_CHANGED = { "DelayedBuild": "_on_changed_delayed_build", "Renderer": "_on_changed_renderer", } def on_set_properties_cb(self, fpo): """Complete the operation of internal _set_properties (callback).""" if "Group" not in fpo.PropertiesList: if FCDVERSION >= (0, 19): fpo.addExtension("App::GroupExtensionPython") # See https://forum.freecadweb.org/viewtopic.php?f=10&t=54370 else: fpo.addExtension("App::GroupExtensionPython", self) fpo.setEditorMode("Group", 2) def _on_changed_delayed_build(self, fpo): """Respond to DelayedBuild property change event.""" if fpo.DelayedBuild: return for view in self.all_views(): view.touch() def _on_changed_renderer(self, fpo): # pylint: disable=no-self-use """Respond to Renderer property change event.""" fpo.PageResult = "" def on_create_cb(self, fpo, viewp, **kwargs): """Complete the operation of 'create' (callback).""" rdr = str(kwargs["renderer"]) template = str(kwargs.get("template", "")) fpo.Label = f"{rdr} Project" fpo.Renderer = rdr fpo.Template = template def get_bounding_box(self): """Compute project bounding box. This the bounding box of the underlying objects referenced in the scene. """ bbox = App.BoundBox() for view in self.all_views(): source = view.Source for attr_name in ("Shape", "Mesh"): try: attr = getattr(source, attr_name) except AttributeError: pass else: bbox.add(attr.BoundBox) break return bbox def write_groundplane(self, renderer): """Generate a ground plane rendering string for the scene. Args: ---------- renderer -- the renderer handler Returns ------- The rendering string for the ground plane """ bbox = self.get_bounding_box() result = "" if bbox.isValid(): zpos = self.fpo.GroundPlaneZ color = self.fpo.GroundPlaneColor factor = self.fpo.GroundPlaneSizeFactor result = renderer.get_groundplane_string(bbox, zpos, color, factor) return result def add_views(self, objs): """Add objects as new views to the project. This method can handle objects groups, recursively. This function checks if each object is renderable before adding it, via 'RendererHandler.is_renderable'; if not, a warning is issued and the faulty object is ignored. Args:: ----------- objs -- an iterable on FreeCAD objects to add to project """ def add_to_group(objs, group): """Add objects as views to a group. objs -- FreeCAD objects to add group -- The group (App::DocumentObjectGroup) to add to """ for obj in objs: success = False if ( hasattr(obj, "Group") and not obj.isDerivedFrom("App::Part") and not obj.isDerivedFrom("PartDesign::Body") ): assert obj != group # Just in case (infinite recursion)... label = View.view_label(obj, group, True) new_group = App.ActiveDocument.addObject( "App::DocumentObjectGroup", label ) new_group.Label = label group.addObject(new_group) add_to_group(obj.Group, new_group) success = True if RendererHandler.is_renderable(obj): View.create(source=obj, project=group) success = True if not success: msg = ( translate( "Render", "[Render] Unable to create rendering view for " "object '{o}': unhandled object type", ) + "\n" ) App.Console.PrintWarning(msg.format(o=obj.fpo.Label)) # add_views starts here add_to_group(iter(objs), self.fpo) def add_view(self, obj): """Add a single object as a new view to the project. This method is essentially provided for console mode, as above method 'add_views' may not be handy for a single object. However, it heavily relies on 'add_views', so please check the latter for more information. Args:: ----------- obj -- a FreeCAD object to add to project """ objs = [] objs.append(obj) self.add_views(objs) def all_views(self, include_groups=False): """Give the list of all the views contained in the project. Args: include_groups -- Flag to include containing groups (including the project) in returned objects. If False, only pure views are returned. """ def all_group_objs(group, include_groups=False): """Return all objects in a group. This method recursively explodes subgroups. Args: group -- The group where the objects are to be searched. include_groups -- See 'all_views' """ res = [] if not include_groups else [group] for obj in group.Group: res.extend( [obj] if not obj.isDerivedFrom("App::DocumentObjectGroup") else all_group_objs(obj, include_groups) ) return res return all_group_objs(self.fpo, include_groups) # TODO Remove external def render(self, external=True, wait_for_completion=False): """Render the project, calling an external renderer. Args: external -- flag to switch between internal/external version of renderer wait_for_completion -- flag to wait for rendering completion before return, in a blocking way (default to False) Returns: Output file path """ obj = self.fpo wait_for_completion = bool(wait_for_completion) # Get a handle to renderer module try: renderer = RendererHandler( rdrname=obj.Renderer, linear_deflection=obj.LinearDeflection, angular_deflection=obj.AngularDeflection, transparency_boost=obj.TransparencySensitivity, ) except ModuleNotFoundError: msg = ( translate( "Render", "[Render] Cannot render project: Renderer '%s' not " "found", ) + "\n" ) App.Console.PrintError(msg % obj.Renderer) return "" # Get the rendering template if obj.getTypeIdOfProperty("Template") == "App::PropertyFile": # Legacy template path (absolute path) template_path = obj.Template else: # Current template path (relative path) template_path = os.path.join(TEMPLATEDIR, obj.Template) if not os.path.isfile(template_path): msg = translate( "Render", "Cannot render project: Template not found ('%s')" ) msg = "[Render] " + (msg % template_path) + "\n" App.Console.PrintError(msg) return "" with open(template_path, "r", encoding="utf8") as template_file: template = template_file.read() # Build a default camera, to be used if no camera is present in the # scene camstr = ( Gui.ActiveDocument.ActiveView.getCamera() if App.GuiUp else DEFAULT_CAMERA_STRING ) cam = renderer.get_camsource_string(get_cam_from_coin_string(camstr)) # Get objects rendering strings (including lights, cameras...) # views = self.all_views() get_rdr_string = ( renderer.get_rendering_string if obj.DelayedBuild else attrgetter("ViewResult") ) if App.GuiUp: objstrings = [ get_rdr_string(v) for v in self.all_views() if v.Source.ViewObject.Visibility ] else: objstrings = [get_rdr_string(v) for v in self.all_views()] # Add a ground plane if required if getattr(obj, "GroundPlane", False): objstrings.append(self.write_groundplane(renderer)) # Merge all strings (cam, objects, ground plane...) into rendering # template renderobjs = "\n".join(objstrings) if "RaytracingCamera" in template: template = re.sub("(.*RaytracingCamera.*)", cam, template) template = re.sub("(.*RaytracingContent.*)", renderobjs, template) else: template = re.sub( "(.*RaytracingContent.*)", cam + "\n" + renderobjs, template ) version_major = sys.version_info.major template = template.encode("utf8") if version_major < 3 else template # Write instantiated template into a temporary file fhandle, fpath = mkstemp( prefix=obj.Name, suffix=os.path.splitext(obj.Template)[-1] ) with open(fpath, "w", encoding="utf8") as fobj: fobj.write(template) os.close(fhandle) obj.PageResult = fpath os.remove(fpath) assert obj.PageResult, "Rendering error: No page result" # Fetch the rendering parameters prefix = PARAMS.GetString("Prefix", "") if prefix: prefix += " " try: output = obj.OutputImage assert output except (AttributeError, AssertionError): output = os.path.splitext(obj.PageResult)[0] + ".png" try: width = int(obj.RenderWidth) except (AttributeError, ValueError, TypeError): width = 800 try: height = int(obj.RenderHeight) except (AttributeError, ValueError, TypeError): height = 600 # Get the renderer command on the generated temp file, with rendering # params cmd, img = renderer.render( obj, prefix, external, output, width, height ) img = img if obj.OpenAfterRender else None if not cmd: # Command is empty (perhaps lack of data in parameters) return None # Execute renderer rdr_executor = RendererExecutor(cmd, img) rdr_executor.start() if wait_for_completion: # Useful in console mode... rdr_executor.join() # And eventually return result path return img