diff --git a/cura/Snapshot.py b/cura/Snapshot.py index 1266d3dcb1..4fd8f57b94 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -1,7 +1,9 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2023 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. import numpy +from typing import Optional + from PyQt6 import QtCore from PyQt6.QtCore import QCoreApplication from PyQt6.QtGui import QImage @@ -10,11 +12,13 @@ from UM.Logger import Logger from cura.PreviewPass import PreviewPass from UM.Application import Application +from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.Matrix import Matrix from UM.Math.Vector import Vector from UM.Scene.Camera import Camera from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator - +from UM.Scene.SceneNode import SceneNode +from UM.Qt.QtRenderer import QtRenderer class Snapshot: @staticmethod @@ -32,6 +36,87 @@ class Snapshot: return min_x, max_x, min_y, max_y + @staticmethod + def isometric_snapshot(width: int = 300, height: int = 300) -> Optional[QImage]: + """Create an isometric snapshot of the scene.""" + + root = Application.getInstance().getController().getScene().getRoot() + + # the direction the camera is looking at to create the isometric view + iso_view_dir = Vector(-1, -1, -1).normalized() + + bounds = Snapshot.node_bounds(root) + if bounds is None: + Logger.log("w", "There appears to be nothing to render") + return None + + camera = Camera("snapshot") + + # find local x and y directional vectors of the camera + s = iso_view_dir.cross(Vector.Unit_Y).normalized() + u = s.cross(iso_view_dir).normalized() + + # find extreme screen space coords of the scene + x_points = [p.dot(s) for p in bounds.points] + y_points = [p.dot(u) for p in bounds.points] + min_x = min(x_points) + max_x = max(x_points) + min_y = min(y_points) + max_y = max(y_points) + camera_width = max_x - min_x + camera_height = max_y - min_y + + if camera_width == 0 or camera_height == 0: + Logger.log("w", "There appears to be nothing to render") + return None + + # increase either width or height to match the aspect ratio of the image + if camera_width / camera_height > width / height: + camera_height = camera_width * height / width + else: + camera_width = camera_height * width / height + + # Configure camera for isometric view + ortho_matrix = Matrix() + ortho_matrix.setOrtho( + -camera_width / 2, + camera_width / 2, + -camera_height / 2, + camera_height / 2, + -10000, + 10000 + ) + camera.setPerspective(False) + camera.setProjectionMatrix(ortho_matrix) + camera.setPosition(bounds.center) + camera.lookAt(bounds.center + iso_view_dir) + + # Render the scene + renderer = QtRenderer() + render_pass = PreviewPass(width, height) + renderer.setViewportSize(width, height) + renderer.setWindowSize(width, height) + render_pass.setCamera(camera) + renderer.addRenderPass(render_pass) + renderer.beginRendering() + renderer.render() + + return render_pass.getOutput() + + @staticmethod + def node_bounds(root_node: SceneNode) -> Optional[AxisAlignedBox]: + axis_aligned_box = None + for node in DepthFirstIterator(root_node): + if not getattr(node, "_outside_buildarea", False): + if node.callDecoration( + "isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration( + "isNonThumbnailVisibleMesh"): + if axis_aligned_box is None: + axis_aligned_box = node.getBoundingBox() + else: + axis_aligned_box = axis_aligned_box + node.getBoundingBox() + return axis_aligned_box + @staticmethod def snapshot(width = 300, height = 300): """Return a QImage of the scene @@ -55,14 +140,7 @@ class Snapshot: camera = Camera("snapshot", root) # determine zoom and look at - bbox = None - for node in DepthFirstIterator(root): - if not getattr(node, "_outside_buildarea", False): - if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"): - if bbox is None: - bbox = node.getBoundingBox() - else: - bbox = bbox + node.getBoundingBox() + bbox = Snapshot.node_bounds(root) # If there is no bounding box, it means that there is no model in the buildplate if bbox is None: Logger.log("w", "Unable to create snapshot as we seem to have an empty buildplate") diff --git a/plugins/MakerbotWriter/MakerbotWriter.py b/plugins/MakerbotWriter/MakerbotWriter.py index 87206a8aaa..4aa8120b67 100644 --- a/plugins/MakerbotWriter/MakerbotWriter.py +++ b/plugins/MakerbotWriter/MakerbotWriter.py @@ -61,7 +61,7 @@ class MakerbotWriter(MeshWriter): Logger.warning("Can't create snapshot when renderer not initialized.") return try: - snapshot = Snapshot.snapshot(width, height) + snapshot = Snapshot.isometric_snapshot(width, height) except: Logger.logException("w", "Failed to create snapshot image") return