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..969d491f49 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,7 +1,9 @@ +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 +25,53 @@ class CuraSceneNode(SceneNode): def isSelectable(self) -> bool: return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate + ## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned + # TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded + def getPrintingExtruder(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + per_mesh_stack = self.callDecoration("getStack") + extruders = list(global_container_stack.extruders.values()) + + # 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"): + return extruders[int(global_container_stack.getProperty("support_extruder_nr", "value"))] + + # It's only set if you explicitly choose an extruder + extruder_id = self.callDecoration("getActiveExtruder") + + for extruder in extruders: + # Find out the extruder if we know the id. + if extruder_id is not None: + if extruder_id == extruder.getId(): + return extruder + else: # If the id is unknown, then return the extruder in the position 0 + try: + if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero + return extruder + except ValueError: + continue + + # This point should never be reached + return None + + ## Return the color of the material used to print this model + def getDiffuseColor(self) -> List[float]: + printing_extruder = self.getPrintingExtruder() + + material_color = "#808080" # Fallback color + if printing_extruder is not None and printing_extruder.material: + material_color = printing_extruder.material.getMetaDataEntry("color_code", default = material_color) + + # 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..f12ff3e0e1 --- /dev/null +++ b/cura/Snapshot.py @@ -0,0 +1,124 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import numpy + +from PyQt5 import QtCore + +from cura.PreviewPass import PreviewPass +from cura.Scene import ConvexHullNode + +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.Mesh.MeshData import transformVertices +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() + active_camera = scene.getActiveCamera() + render_width, render_height = active_camera.getWindowSize() + preview_pass = PreviewPass(render_width, render_height) + + root = scene.getRoot() + camera = Camera("snapshot", root) + + # determine zoom and look at + bbox = None + hulls = None + for node in DepthFirstIterator(root): + if type(node) == ConvexHullNode: + print(node) + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): + if bbox is None: + bbox = node.getBoundingBox() + else: + bbox = bbox + node.getBoundingBox() + convex_hull = node.getMeshData().getConvexHullTransformedVertices(node.getWorldTransformation()) + if hulls is None: + hulls = convex_hull + else: + hulls = numpy.concatenate((hulls, convex_hull), axis = 0) + + if bbox is None: + bbox = AxisAlignedBox() + + look_at = bbox.center + size = max(bbox.width, bbox.height, bbox.depth * 0.5) + + # 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) + + 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) + + # we need this for the projection calculation + hulls4 = numpy.ones((hulls.shape[0], 4)) + hulls4[:, :-1] = hulls + #position = Vector(10, 10, 10) + # projected_position = camera.project(position) + + preview_pass.setCamera(camera) + preview_pass.setSize(render_width, render_height) # texture size + preview_pass.render() + pixel_output = preview_pass.getOutput() + + print("Calculating image coordinates...") + view = camera.getWorldTransformation().getInverse() + min_x, max_x, min_y, max_y = render_width, 0, render_height, 0 + for hull_coords in hulls4: + projected_position = view.getData().dot(hull_coords) + projected_position2 = projection_matrix.getData().dot(projected_position) + #xx, yy = camera.project(Vector(data = hull_coords)) + # xx, yy range from -1 to 1 + xx = projected_position2[0] / projected_position2[2] / 2.0 + yy = projected_position2[1] / projected_position2[2] / 2.0 + # x, y 0..render_width/height + x = int(render_width / 2 + xx * render_width / 2) + y = int(render_height / 2 + yy * render_height / 2) + min_x = min(x, min_x) + max_x = max(x, max_x) + min_y = min(y, min_y) + max_y = max(y, max_y) + print(min_x, max_x, min_y, max_y) + + # print("looping all pixels in python...") + # min_x_, max_x_, min_y_, max_y_ = render_width, 0, render_height, 0 + # for y in range(int(render_height)): + # for x in range(int(render_width)): + # color = pixel_output.pixelColor(x, y) + # if color.alpha() > 0: + # min_x_ = min(x, min_x_) + # max_x_ = max(x, max_x_) + # min_y_ = min(y, min_y_) + # max_y_ = max(y, max_y_) + # print(min_x_, max_x_, min_y_, max_y_) + + # make it a square + if max_x - min_x >= max_y - min_y: + # make y bigger + min_y, max_y = int((max_y + min_y) / 2 - (max_x - min_x) / 2), int((max_y + min_y) / 2 + (max_x - min_x) / 2) + else: + # make x bigger + min_x, max_x = int((max_x + min_x) / 2 - (max_y - min_y) / 2), int((max_x + min_x) / 2 + (max_y - min_y) / 2) + copy_pixel_output = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y) + + # Scale it to the correct height + image = copy_pixel_output.scaledToHeight(height, QtCore.Qt.SmoothTransformation) + # Then chop of the width + cropped_image = image.copy(image.width() // 2 - width // 2, 0, width, height) + + return cropped_image diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 09ed1e126d..1726818100 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os.path @@ -37,10 +37,6 @@ class ThreeMFReader(MeshReader): super().__init__() self._supported_extensions = [".3mf"] self._root = None - self._namespaces = { - "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", - "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10" - } self._base_name = "" self._unit = None self._object_count = 0 # Used to name objects as there is no node name yet. diff --git a/plugins/UCPWriter/UCPWriter.py b/plugins/UCPWriter/UCPWriter.py deleted file mode 100644 index cd858b912a..0000000000 --- a/plugins/UCPWriter/UCPWriter.py +++ /dev/null @@ -1,68 +0,0 @@ -import zipfile - -from io import StringIO - -from UM.Resources import Resources -from UM.Mesh.MeshWriter import MeshWriter -from UM.Logger import Logger -from UM.PluginRegistry import PluginRegistry - -MYPY = False -try: - if not MYPY: - import xml.etree.cElementTree as ET -except ImportError: - Logger.log("w", "Unable to load cElementTree, switching to slower version") - import xml.etree.ElementTree as ET - - -class UCPWriter(MeshWriter): - def __init__(self): - super().__init__() - self._namespaces = { - "content-types": "http://schemas.openxmlformats.org/package/2006/content-types", - "relationships": "http://schemas.openxmlformats.org/package/2006/relationships", - } - - def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): - self._archive = None # Reset archive - archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED) - - gcode_file = zipfile.ZipInfo("3D/model.gcode") - gcode_file.compress_type = zipfile.ZIP_DEFLATED - - # Create content types file - content_types_file = zipfile.ZipInfo("[Content_Types].xml") - content_types_file.compress_type = zipfile.ZIP_DEFLATED - content_types = ET.Element("Types", xmlns=self._namespaces["content-types"]) - - rels_type = ET.SubElement(content_types, "Default", Extension="rels", - ContentType="application/vnd.openxmlformats-package.relationships+xml") - gcode_type = ET.SubElement(content_types, "Default", Extension="gcode", - ContentType="text/x-gcode") - image_type = ET.SubElement(content_types, "Default", Extension="png", - ContentType="image/png") - - # Create _rels/.rels file - relations_file = zipfile.ZipInfo("_rels/.rels") - relations_file.compress_type = zipfile.ZIP_DEFLATED - relations_element = ET.Element("Relationships", xmlns=self._namespaces["relationships"]) - - thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target="/Metadata/thumbnail.png", Id="rel0", - Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") - - model_relation_element = ET.SubElement(relations_element, "Relationship", Target="/3D/model.gcode", - Id="rel1", - Type="http://schemas.ultimaker.org/package/2018/relationships/gcode") - - gcode_string = StringIO() - - PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_string, None) - - archive.write(Resources.getPath(Resources.Images, "cura-icon.png"), "Metadata/thumbnail.png") - - archive.writestr(gcode_file, gcode_string.getvalue()) - archive.writestr(content_types_file, b' \n' + ET.tostring(content_types)) - archive.writestr(relations_file, b' \n' + ET.tostring(relations_element)) - - archive.close() diff --git a/plugins/UCPWriter/__init__.py b/plugins/UCPWriter/__init__.py deleted file mode 100644 index 24a4856c34..0000000000 --- a/plugins/UCPWriter/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Uranium is released under the terms of the LGPLv3 or higher. - -from . import UCPWriter - -from UM.i18n import i18nCatalog - -i18n_catalog = i18nCatalog("cura") - -def getMetaData(): - return { - "mesh_writer": { - "output": [ - { - "mime_type": "application/x-ucp", - "mode": UCPWriter.UCPWriter.OutputMode.BinaryMode, - "extension": "UCP", - "description": i18n_catalog.i18nc("@item:inlistbox", "UCP File (WIP)") - } - ] - } - } - -def register(app): - return { "mesh_writer": UCPWriter.UCPWriter() } diff --git a/plugins/UCPWriter/plugin.json b/plugins/UCPWriter/plugin.json deleted file mode 100644 index d1e3ce3d1c..0000000000 --- a/plugins/UCPWriter/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "UCP Writer", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "description": "Provides support for writing UCP files.", - "api": 4, - "i18n-catalog": "cura" -} diff --git a/plugins/UFPWriter/UFPWriter.py b/plugins/UFPWriter/UFPWriter.py new file mode 100644 index 0000000000..0f49a30403 --- /dev/null +++ b/plugins/UFPWriter/UFPWriter.py @@ -0,0 +1,56 @@ +#Copyright (c) 2018 Ultimaker B.V. +#Cura is released under the terms of the LGPLv3 or higher. + +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 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) + + #Store the g-code from the scene. + archive.addContentType(extension = "gcode", mime_type = "text/x-gcode") + gcode_textio = StringIO() #We have to convert the g-code into bytes. + PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None) + gcode = archive.getStream("/3D/model.gcode") + gcode.write(gcode_textio.getvalue().encode("UTF-8")) + archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") + + #Store the thumbnail. + 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 diff --git a/plugins/UFPWriter/__init__.py b/plugins/UFPWriter/__init__.py new file mode 100644 index 0000000000..77f8e81222 --- /dev/null +++ b/plugins/UFPWriter/__init__.py @@ -0,0 +1,25 @@ +#Copyright (c) 2018 Ultimaker B.V. +#Cura is released under the terms of the LGPLv3 or higher. + +from . import UFPWriter +from UM.i18n import i18nCatalog #To translate the file format description. +from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag. + +i18n_catalog = i18nCatalog("cura") + +def getMetaData(): + return { + "mesh_writer": { + "output": [ + { + "mime_type": "application/x-ufp", + "mode": MeshWriter.OutputMode.BinaryMode, + "extension": "ufp", + "description": i18n_catalog.i18nc("@item:inlistbox", "Ultimaker Format Package") + } + ] + } + } + +def register(app): + return { "mesh_writer": UFPWriter.UFPWriter() } diff --git a/plugins/UFPWriter/kitten.png b/plugins/UFPWriter/kitten.png new file mode 100644 index 0000000000..44738f94f0 Binary files /dev/null and b/plugins/UFPWriter/kitten.png differ diff --git a/plugins/UFPWriter/plugin.json b/plugins/UFPWriter/plugin.json new file mode 100644 index 0000000000..7d10b89ad4 --- /dev/null +++ b/plugins/UFPWriter/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "UFP Writer", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "description": "Provides support for writing Ultimaker Format Packages.", + "api": 4, + "i18n-catalog": "cura" +} \ No newline at end of file