Merge branch 'feature_ufp_writer'

This commit is contained in:
Diego Prado Gesto 2018-02-02 11:58:33 +01:00
commit 56382bc9b8
11 changed files with 276 additions and 112 deletions

View File

@ -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()

View File

@ -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()

124
cura/Snapshot.py Normal file
View File

@ -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

View File

@ -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.

View File

@ -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'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
archive.close()

View File

@ -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() }

View File

@ -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"
}

View File

@ -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

View File

@ -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() }

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View File

@ -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"
}