CURA-4425 first thumbnail in UFP file; updated CuraSceneModel and PreviewPass

This commit is contained in:
Jack Ha 2018-01-31 17:08:32 +01:00
parent 2fe9860bb9
commit c42f186812
4 changed files with 189 additions and 14 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,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()

74
cura/Snapshot.py Normal file
View file

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

View file

@ -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.
if self._snapshot:
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()
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