| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from PySide import QtGui, QtCore |
| | import FreeCAD |
| | import FreeCADGui |
| | import Path.Log |
| | import os |
| | import tempfile |
| |
|
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | class ImageBuilder: |
| | def __init__(self, file_path): |
| | self.file_path = file_path |
| |
|
| | def build_image(self, obj, image_name, as_bytes=False, view="default"): |
| | raise NotImplementedError("Subclass must implement abstract method") |
| |
|
| | def save_image(self, image): |
| | raise NotImplementedError("Subclass must implement abstract method") |
| |
|
| |
|
| | class ImageBuilderFactory: |
| | @staticmethod |
| | def get_image_builder(file_path, **kwargs): |
| | |
| | if FreeCAD.GuiUp: |
| | return GuiImageBuilder(file_path, **kwargs) |
| | else: |
| | return DummyImageBuilder(file_path, **kwargs) |
| | |
| |
|
| |
|
| | class DummyImageBuilder(ImageBuilder): |
| | def __init__(self, file_path): |
| | Path.Log.debug("Initializing dummyimagebuilder") |
| | super().__init__(file_path) |
| |
|
| | def build_image(self, obj, imageName, as_bytes=False, view="default"): |
| | if as_bytes: |
| | return b"" |
| | return self.file_path |
| |
|
| |
|
| | class GuiImageBuilder(ImageBuilder): |
| | """ |
| | A class for generating images of 3D objects in a FreeCAD GUI environment. |
| | """ |
| |
|
| | def __init__(self, file_path): |
| | super().__init__(file_path) |
| |
|
| | Path.Log.debug("Initializing GuiImageBuilder") |
| | self.file_path = file_path |
| | self.currentCamera = FreeCADGui.ActiveDocument.ActiveView.getCameraType() |
| |
|
| | self.doc = FreeCADGui.ActiveDocument |
| |
|
| | def __del__(self): |
| | Path.Log.debug("Destroying GuiImageBuilder") |
| | self.restore_visibility() |
| |
|
| | def prepare_view(self, obj, view="default"): |
| | |
| | Path.Log.debug("CAM - Preparing view\n") |
| |
|
| | mw = FreeCADGui.getMainWindow() |
| | num_windows = len(mw.getWindows()) |
| |
|
| | |
| | view_obj = FreeCADGui.ActiveDocument.createView("Gui::View3DInventor") |
| | view_obj.setAnimationEnabled(False) |
| | view_obj.setCameraType("Orthographic") |
| | if view == "headon": |
| | view_obj.viewFront() |
| | else: |
| | view_obj.viewIsometric() |
| |
|
| | |
| | mdi = mw.findChild(QtGui.QMdiArea) |
| | view_window = mdi.activeSubWindow() |
| | view_window.resize(500, 500) |
| | view_window.showMaximized() |
| |
|
| | |
| | self.record_visibility() |
| |
|
| | |
| | obj.Visibility = True |
| | FreeCADGui.Selection.clearSelection() |
| | FreeCADGui.Selection.addSelection(obj) |
| | FreeCADGui.Selection.clearSelection() |
| |
|
| | |
| | a_view = FreeCADGui.activeDocument().activeView() |
| | try: |
| | a_view.fitAll() |
| | FreeCADGui.updateGui() |
| | a_view.fitSelection() |
| | except Exception: |
| | |
| | pass |
| |
|
| | |
| | return num_windows |
| |
|
| | def record_visibility(self): |
| | self.visible = [o for o in self.doc.Document.Objects if o.Visibility] |
| | for o in self.doc.Document.Objects: |
| | o.Visibility = False |
| |
|
| | def destroy_view(self, idx): |
| | Path.Log.debug("CAM - destroying view\n") |
| | mw = FreeCADGui.getMainWindow() |
| | windows = mw.getWindows() |
| | mw.removeWindow(windows[idx]) |
| |
|
| | def restore_visibility(self): |
| | Path.Log.debug("CAM - Restoring visibility\n") |
| | for o in self.visible: |
| | o.Visibility = True |
| |
|
| | def build_image(self, obj, image_name, as_bytes=False, view="default"): |
| | Path.Log.debug("CAM - Building image\n") |
| | """ |
| | Makes an image of the target object. Returns either the image as bytes or a filename. |
| | """ |
| |
|
| | idx = self.prepare_view(obj, view=view) |
| |
|
| | if as_bytes: |
| | |
| | img_bytes = self.capture_image_to_bytes() |
| | self.destroy_view(idx) |
| | return img_bytes |
| | else: |
| | |
| | file_path = os.path.join(self.file_path, image_name) |
| | self.capture_image(file_path) |
| | self.destroy_view(idx) |
| | result = f"{file_path}_t.png" |
| | Path.Log.debug(f"Image saved to: {result}") |
| | return result |
| |
|
| | def capture_image(self, file_path): |
| | FreeCADGui.updateGui() |
| | Path.Log.debug("CAM - capture image to file\n") |
| | a_view = FreeCADGui.activeDocument().activeView() |
| | |
| | a_view.saveImage(file_path + ".png", 800, 800, "Current") |
| | a_view.saveImage(file_path + "_t.png", 800, 800, "Transparent") |
| | a_view.setAnimationEnabled(True) |
| |
|
| | def capture_image_to_bytes(self): |
| | """Capture the current view directly to bytes without writing to disk""" |
| | FreeCADGui.updateGui() |
| | Path.Log.debug("CAM - capture image to bytes\n") |
| | a_view = FreeCADGui.activeDocument().activeView() |
| |
|
| | try: |
| | |
| | |
| | qimg = a_view.grabFramebuffer() |
| |
|
| | |
| | buffer = QtCore.QBuffer() |
| | buffer.open(QtCore.QIODevice.WriteOnly) |
| | qimg.save(buffer, "PNG") |
| | img_bytes = buffer.data().data() |
| | buffer.close() |
| |
|
| | a_view.setAnimationEnabled(True) |
| | return img_bytes |
| |
|
| | except Exception as e: |
| | |
| | Path.Log.debug(f"Direct image capture failed: {e}, using fallback method") |
| |
|
| | with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp: |
| | temp_path = temp.name |
| |
|
| | |
| | |
| | a_view.saveImage(temp_path, 800, 800, "Transparent") |
| |
|
| | |
| | with open(temp_path, "rb") as f: |
| | img_bytes = f.read() |
| |
|
| | |
| | try: |
| | os.unlink(temp_path) |
| | except Exception: |
| | |
| | pass |
| |
|
| | a_view.setAnimationEnabled(True) |
| | return img_bytes |
| |
|
| |
|
| | class NonGuiImageBuilder(ImageBuilder): |
| | def __init__(self, file_path): |
| | super().__init__(file_path) |
| | Path.Log.debug("nonguiimagebuilder") |
| |
|
| | def build_image(self, obj, image_name, as_bytes=False, view="default"): |
| | """ |
| | Generates a headless picture of a 3D object and saves it as a PNG and optionally a PostScript file. |
| | |
| | Args: |
| | - obj: The 3D object to generate an image for. |
| | - image_name: Base name for the output image file without extension. |
| | |
| | Returns: |
| | - A boolean indicating the success of the operation. |
| | """ |
| | |
| | if not hasattr(obj, "Shape") or not hasattr(obj.Shape, "writeInventor"): |
| | Path.Log.debug("Object does not have the required attributes.") |
| | return False |
| |
|
| | try: |
| | |
| | iv = obj.Shape.writeInventor() |
| |
|
| | |
| | inp = coin.SoInput() |
| | inp.setBuffer(iv) |
| | data = coin.SoDB.readAll(inp) |
| |
|
| | if data is None: |
| | Path.Log.debug("Failed to read Inventor data.") |
| | return False |
| |
|
| | |
| | base = coin.SoBaseColor() |
| | base.rgb.setValue(0.6, 0.7, 1.0) |
| | data.insertChild(base, 0) |
| |
|
| | root = coin.SoSeparator() |
| | light = coin.SoDirectionalLight() |
| | cam = coin.SoOrthographicCamera() |
| | root.addChild(cam) |
| | root.addChild(light) |
| | root.addChild(data) |
| |
|
| | |
| | axo = coin.SbRotation(-0.353553, -0.146447, -0.353553, -0.853553) |
| | viewport = coin.SbViewportRegion(400, 400) |
| | cam.orientation.setValue(axo) |
| | cam.viewAll(root, viewport) |
| | off = coin.SoOffscreenRenderer(viewport) |
| | root.ref() |
| | ret = off.render(root) |
| | root.unref() |
| | if as_bytes: |
| | qimg = off.getQImage() |
| | buffer = QtCore.QBuffer() |
| | buffer.open(QtCore.QIODevice.WriteOnly) |
| | qimg.save(buffer, "PNG") |
| | return buffer.data().data() |
| | else: |
| | if off.isWriteSupported("PNG"): |
| | file_path = f"{self.file_path}{os.path.sep}{image_name}.png" |
| | off.writeToFile(file_path, "PNG") |
| | return file_path |
| | else: |
| | Path.Log.debug("PNG format is not supported.") |
| | return False |
| |
|
| | except Exception as e: |
| | print(f"An error occurred: {e}") |
| | return False |
| |
|