From c42f1868124a5c8d0b67f3235671f2c30707895f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 31 Jan 2018 17:08:32 +0100 Subject: [PATCH] CURA-4425 first thumbnail in UFP file; updated CuraSceneModel and PreviewPass --- cura/PreviewPass.py | 17 +++++--- cura/Scene/CuraSceneNode.py | 72 +++++++++++++++++++++++++++++++++ cura/Snapshot.py | 74 ++++++++++++++++++++++++++++++++++ plugins/UFPWriter/UFPWriter.py | 40 +++++++++++++----- 4 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 cura/Snapshot.py diff --git a/cura/PreviewPass.py b/cura/PreviewPass.py index c1880e82ef..af42b59b78 100644 --- a/cura/PreviewPass.py +++ b/cura/PreviewPass.py @@ -1,7 +1,7 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Uranium is released under the terms of the LGPLv3 or higher. - +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application +from UM.Math.Color import Color from UM.Resources import Resources from UM.View.RenderPass import RenderPass @@ -39,7 +39,11 @@ class PreviewPass(RenderPass): def render(self) -> None: if not self._shader: - self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "object.shader")) + self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader")) + self._shader.setUniformValue("u_overhangAngle", 1.0) + + self._gl.glClearColor(0.0, 0.0, 0.0, 0.0) + self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT) # Create a new batch to be rendered batch = RenderBatch(self._shader) @@ -47,7 +51,9 @@ class PreviewPass(RenderPass): # Fill up the batch with objects that can be sliced. ` for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): - batch.addItem(node.getWorldTransformation(), node.getMeshData()) + uniforms = {} + uniforms["diffuse_color"] = node.getDiffuseColor() + batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms) self.bind() if self._camera is None: @@ -55,3 +61,4 @@ class PreviewPass(RenderPass): else: batch.render(self._camera) self.release() + diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index 1bffe4392b..0dbcdd30c3 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,7 +1,10 @@ +from typing import List + from UM.Application import Application from UM.Logger import Logger from UM.Scene.SceneNode import SceneNode from copy import deepcopy +from cura.Settings.ExtrudersModel import ExtrudersModel ## Scene nodes that are models are only seen when selecting the corresponding build plate @@ -23,6 +26,75 @@ class CuraSceneNode(SceneNode): def isSelectable(self) -> bool: return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate + def getPrintingExtruderPosition(self) -> int: + # took bits and pieces from extruders model, solid view + + global_container_stack = Application.getInstance().getGlobalContainerStack() + per_mesh_stack = self.callDecoration("getStack") + # It's only set if you explicitly choose an extruder + extruder_id = self.callDecoration("getActiveExtruder") + + machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") + + extruder_index = 0 + + for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()): + position = extruder.getMetaDataEntry("position", default = "0") # Get the position + try: + position = int(position) + except ValueError: + # Not a proper int. + position = -1 + if position > machine_extruder_count: + continue + + # Find out the extruder index if we know the id. + if extruder_id is not None and extruder_id == extruder.getId(): + extruder_index = position + break + + # Use the support extruder instead of the active extruder if this is a support_mesh + if per_mesh_stack: + if per_mesh_stack.getProperty("support_mesh", "value"): + extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value")) + + return extruder_index + + def getDiffuseColor(self) -> List[float]: + # took bits and pieces from extruders model, solid view + + global_container_stack = Application.getInstance().getGlobalContainerStack() + machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") + + extruder_index = self.getPrintingExtruderPosition() + + material_color = ExtrudersModel.defaultColors[extruder_index] + + # Collect color from the extruder we want + for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()): + position = extruder.getMetaDataEntry("position", default = "0") # Get the position + try: + position = int(position) + except ValueError: + # Not a proper int. + position = -1 + if position > machine_extruder_count: + continue + + if extruder.material and position == extruder_index: + material_color = extruder.material.getMetaDataEntry("color_code", default = material_color) + break + + # Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs + # an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0]) + return [ + int(material_color[1:3], 16) / 255, + int(material_color[3:5], 16) / 255, + int(material_color[5:7], 16) / 255, + 1.0 + ] + + ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode def __deepcopy__(self, memo): copy = CuraSceneNode() diff --git a/cura/Snapshot.py b/cura/Snapshot.py new file mode 100644 index 0000000000..83881dbdee --- /dev/null +++ b/cura/Snapshot.py @@ -0,0 +1,74 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from PyQt5 import QtCore + +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 + + +class Snapshot: + @staticmethod + def snapshot(width = 300, height = 300): + scene = Application.getInstance().getController().getScene() + cam = scene.getActiveCamera() + render_width, render_height = cam.getWindowSize() + pp = PreviewPass(render_width, render_height) + + root = scene.getRoot() + camera = Camera("snapshot", root) + + # determine zoom and look at + bbox = None + for node in DepthFirstIterator(root): + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): + if bbox is None: + bbox = node.getBoundingBox() + else: + bbox += node.getBoundingBox() + if bbox is None: + bbox = AxisAlignedBox() + look_at = bbox.center + size = max(bbox.width, bbox.height, bbox.depth * 0.5) + + looking_from_offset = Vector(1, 1, 2) + if size > 0: + # determine the watch distance depending on the size + looking_from_offset = looking_from_offset * size * 1.3 + camera.setViewportSize(render_width, render_height) + camera.setWindowSize(render_width, render_height) + camera.setPosition(look_at + looking_from_offset) + camera.lookAt(look_at) + + # Somehow the aspect ratio is also influenced in reverse by the screen width/height + # So you have to set it to render_width/render_height to get 1 + projection_matrix = Matrix() + projection_matrix.setPerspective(30, render_width / render_height, 1, 500) + + camera.setProjectionMatrix(projection_matrix) + + pp.setCamera(camera) + pp.setSize(render_width, render_height) # texture size + pp.render() + pixel_output = pp.getOutput() + + # It's a bit annoying that window size has to be taken into account + if pixel_output.width() >= pixel_output.height(): + # Scale it to the correct height + image = pixel_output.scaledToHeight(height, QtCore.Qt.SmoothTransformation) + # Then chop of the width + cropped_image = image.copy(image.width() // 2 - width // 2, 0, width, height) + else: + # Scale it to the correct width + image = pixel_output.scaledToWidth(width, QtCore.Qt.SmoothTransformation) + # Then chop of the height + cropped_image = image.copy(0, image.height() // 2 - height // 2, width, height) + + return cropped_image + # if cropped_image.save("/home/jack/preview.png"): + # print("yooo") diff --git a/plugins/UFPWriter/UFPWriter.py b/plugins/UFPWriter/UFPWriter.py index 7f471d03b2..4e7463735c 100644 --- a/plugins/UFPWriter/UFPWriter.py +++ b/plugins/UFPWriter/UFPWriter.py @@ -3,13 +3,29 @@ from charon.VirtualFile import VirtualFile #To open UFP files. from charon.OpenMode import OpenMode #To indicate that we want to write to UFP files. -from io import StringIO #For converting g-code to bytes. +from io import BytesIO, StringIO #For converting g-code to bytes. import os.path #To get the placeholder kitty icon. +from UM.Application import Application +from UM.Logger import Logger from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement. from UM.PluginRegistry import PluginRegistry #To get the g-code writer. +from PyQt5.QtCore import QBuffer + +from cura.Snapshot import Snapshot + class UFPWriter(MeshWriter): + def __init__(self): + super().__init__() + self._snapshot = None + Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot) + + def _createSnapshot(self, *args): + # must be called from the main thread because of OpenGL + Logger.log("d", "Creating thumbnail image...") + self._snapshot = Snapshot.snapshot() + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): archive = VirtualFile() archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly) @@ -20,16 +36,22 @@ class UFPWriter(MeshWriter): PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None) gcode = archive.getStream("/3D/model.gcode") gcode.write(gcode_textio.getvalue().encode("UTF-8")) - gcode.close() archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") #Store the thumbnail. - #TODO: Generate the thumbnail image. Below is just a placeholder. - archive.addContentType(extension = "png", mime_type = "image/png") - thumbnail = archive.getStream("/Metadata/thumbnail.png") - thumbnail.write(open(os.path.join(os.path.dirname(__file__), "kitten.png"), "rb").read()) - thumbnail.close() - archive.addRelation(virtual_path = "/Metadata/thumbnail.png", relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail", origin = "/3D/model.gcode") + if self._snapshot: + archive.addContentType(extension = "png", mime_type = "image/png") + thumbnail = archive.getStream("/Metadata/thumbnail.png") + + thumbnail_buffer = QBuffer() + thumbnail_buffer.open(QBuffer.ReadWrite) + thumbnail_image = self._snapshot + thumbnail_image.save(thumbnail_buffer, "PNG") + + thumbnail.write(thumbnail_buffer.data()) + archive.addRelation(virtual_path = "/Metadata/thumbnail.png", relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail", origin = "/3D/model.gcode") + else: + Logger.log("d", "Thumbnail not created, cannot save it") archive.close() - return True \ No newline at end of file + return True