From fe4790fe0695739c8e8d41f97c61d1c05d73967f Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Fri, 20 Oct 2023 23:14:58 +0200 Subject: [PATCH 1/3] Add iso view to snapshot --- cura/Snapshot.py | 90 +++++++++++++++++++++++- plugins/MakerbotWriter/MakerbotWriter.py | 2 +- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/cura/Snapshot.py b/cura/Snapshot.py index 1266d3dcb1..53090a5fec 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,88 @@ class Snapshot: return min_x, max_x, min_y, max_y + @staticmethod + def isometric_snapshot(width: int = 300, height: int = 300, *, root: Optional[SceneNode] = None) -> Optional[ + QImage]: + """Create an isometric snapshot of the scene.""" + + root = Application.getInstance().getController().getScene().getRoot() if root is None else root + + # 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 diff --git a/plugins/MakerbotWriter/MakerbotWriter.py b/plugins/MakerbotWriter/MakerbotWriter.py index 2a6243828c..18fb435490 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 From 513454075142d5e4074252027810cc9bd87f2948 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Mon, 23 Oct 2023 11:19:04 +0200 Subject: [PATCH 2/3] Remove unused extra argument --- cura/Snapshot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cura/Snapshot.py b/cura/Snapshot.py index 53090a5fec..8b162403f8 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -37,11 +37,10 @@ class Snapshot: return min_x, max_x, min_y, max_y @staticmethod - def isometric_snapshot(width: int = 300, height: int = 300, *, root: Optional[SceneNode] = None) -> Optional[ - QImage]: + def isometric_snapshot(width: int = 300, height: int = 300) -> Optional[QImage]: """Create an isometric snapshot of the scene.""" - root = Application.getInstance().getController().getScene().getRoot() if root is None else root + 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() From 6c2a468c1896fcb4f82e95a186806a604691e986 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Mon, 23 Oct 2023 11:19:28 +0200 Subject: [PATCH 3/3] Reuse `node_bounds` utility function --- cura/Snapshot.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cura/Snapshot.py b/cura/Snapshot.py index 8b162403f8..4fd8f57b94 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -140,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")