Made changed from code review and updated MeshReader plugins to support changes made to Uranium branch MeshReaderDialog. This branch now must be paired with that Uranium branch.

This commit is contained in:
Kurt Loeffler 2015-12-22 21:40:46 -08:00 committed by Ghostkeeper
parent f5939df085
commit 447fdc8fbc
5 changed files with 161 additions and 147 deletions

View file

@ -24,7 +24,7 @@ import xml.etree.ElementTree as ET
class ThreeMFReader(MeshReader): class ThreeMFReader(MeshReader):
def __init__(self): def __init__(self):
super(ThreeMFReader, self).__init__() super(ThreeMFReader, self).__init__()
self._supported_extension = ".3mf" self._supported_extensions = [".3mf"]
self._namespaces = { self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
@ -33,102 +33,102 @@ class ThreeMFReader(MeshReader):
def read(self, file_name): def read(self, file_name):
result = None result = None
extension = os.path.splitext(file_name)[1]
if extension.lower() == self._supported_extension:
result = SceneNode()
# The base object of 3mf is a zipped archive.
archive = zipfile.ZipFile(file_name, "r")
try:
root = ET.parse(archive.open("3D/3dmodel.model"))
# There can be multiple objects, try to load all of them. result = SceneNode()
objects = root.findall("./3mf:resources/3mf:object", self._namespaces) # The base object of 3mf is a zipped archive.
if len(objects) == 0: archive = zipfile.ZipFile(file_name, "r")
Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) try:
return None root = ET.parse(archive.open("3D/3dmodel.model"))
for object in objects: # There can be multiple objects, try to load all of them.
mesh = MeshData() objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
node = SceneNode() if len(objects) == 0:
vertex_list = [] Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name)
#for vertex in object.mesh.vertices.vertex: return None
for vertex in object.findall(".//3mf:vertex", self._namespaces):
vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
Job.yieldThread()
triangles = object.findall(".//3mf:triangle", self._namespaces)
mesh.reserveFaceCount(len(triangles))
#for triangle in object.mesh.triangles.triangle:
for triangle in triangles:
v1 = int(triangle.get("v1"))
v2 = int(triangle.get("v2"))
v3 = int(triangle.get("v3"))
mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2])
Job.yieldThread()
#TODO: We currently do not check for normals and simply recalculate them.
mesh.calculateNormals()
node.setMeshData(mesh)
node.setSelectable(True)
transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(object.get("id")), self._namespaces)
if transformation:
transformation = transformation[0]
if transformation.get("transform"):
splitted_transformation = transformation.get("transform").split()
## Transformation is saved as:
## M00 M01 M02 0.0
## M10 M11 M12 0.0
## M20 M21 M22 0.0
## M30 M31 M32 1.0
## We switch the row & cols as that is how everyone else uses matrices!
temp_mat = Matrix()
# Rotation & Scale
temp_mat._data[0,0] = splitted_transformation[0]
temp_mat._data[1,0] = splitted_transformation[1]
temp_mat._data[2,0] = splitted_transformation[2]
temp_mat._data[0,1] = splitted_transformation[3]
temp_mat._data[1,1] = splitted_transformation[4]
temp_mat._data[2,1] = splitted_transformation[5]
temp_mat._data[0,2] = splitted_transformation[6]
temp_mat._data[1,2] = splitted_transformation[7]
temp_mat._data[2,2] = splitted_transformation[8]
# Translation
temp_mat._data[0,3] = splitted_transformation[9]
temp_mat._data[1,3] = splitted_transformation[10]
temp_mat._data[2,3] = splitted_transformation[11]
node.setPosition(Vector(temp_mat.at(0,3), temp_mat.at(1,3), temp_mat.at(2,3)))
temp_quaternion = Quaternion()
temp_quaternion.setByMatrix(temp_mat)
node.setOrientation(temp_quaternion)
# Magical scale extraction
scale = temp_mat.getTransposed().multiply(temp_mat)
scale_x = math.sqrt(scale.at(0,0))
scale_y = math.sqrt(scale.at(1,1))
scale_z = math.sqrt(scale.at(2,2))
node.setScale(Vector(scale_x,scale_y,scale_z))
# We use a different coordinate frame, so rotate.
#rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0))
#node.rotate(rotation)
result.addChild(node)
for object in objects:
mesh = MeshData()
node = SceneNode()
vertex_list = []
#for vertex in object.mesh.vertices.vertex:
for vertex in object.findall(".//3mf:vertex", self._namespaces):
vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
Job.yieldThread() Job.yieldThread()
#If there is more then one object, group them. triangles = object.findall(".//3mf:triangle", self._namespaces)
try:
if len(objects) > 1: mesh.reserveFaceCount(len(triangles))
group_decorator = GroupDecorator()
result.addDecorator(group_decorator) #for triangle in object.mesh.triangles.triangle:
except: for triangle in triangles:
pass v1 = int(triangle.get("v1"))
except Exception as e: v2 = int(triangle.get("v2"))
Logger.log("e" ,"exception occured in 3mf reader: %s" , e) v3 = int(triangle.get("v3"))
mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2])
Job.yieldThread()
#TODO: We currently do not check for normals and simply recalculate them.
mesh.calculateNormals()
node.setMeshData(mesh)
node.setSelectable(True)
transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(object.get("id")), self._namespaces)
if transformation:
transformation = transformation[0]
if transformation.get("transform"):
splitted_transformation = transformation.get("transform").split()
## Transformation is saved as:
## M00 M01 M02 0.0
## M10 M11 M12 0.0
## M20 M21 M22 0.0
## M30 M31 M32 1.0
## We switch the row & cols as that is how everyone else uses matrices!
temp_mat = Matrix()
# Rotation & Scale
temp_mat._data[0,0] = splitted_transformation[0]
temp_mat._data[1,0] = splitted_transformation[1]
temp_mat._data[2,0] = splitted_transformation[2]
temp_mat._data[0,1] = splitted_transformation[3]
temp_mat._data[1,1] = splitted_transformation[4]
temp_mat._data[2,1] = splitted_transformation[5]
temp_mat._data[0,2] = splitted_transformation[6]
temp_mat._data[1,2] = splitted_transformation[7]
temp_mat._data[2,2] = splitted_transformation[8]
# Translation
temp_mat._data[0,3] = splitted_transformation[9]
temp_mat._data[1,3] = splitted_transformation[10]
temp_mat._data[2,3] = splitted_transformation[11]
node.setPosition(Vector(temp_mat.at(0,3), temp_mat.at(1,3), temp_mat.at(2,3)))
temp_quaternion = Quaternion()
temp_quaternion.setByMatrix(temp_mat)
node.setOrientation(temp_quaternion)
# Magical scale extraction
scale = temp_mat.getTransposed().multiply(temp_mat)
scale_x = math.sqrt(scale.at(0,0))
scale_y = math.sqrt(scale.at(1,1))
scale_z = math.sqrt(scale.at(2,2))
node.setScale(Vector(scale_x,scale_y,scale_z))
# We use a different coordinate frame, so rotate.
#rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0))
#node.rotate(rotation)
result.addChild(node)
Job.yieldThread()
#If there is more then one object, group them.
try:
if len(objects) > 1:
group_decorator = GroupDecorator()
result.addDecorator(group_decorator)
except:
pass
except Exception as e:
Logger.log("e" ,"exception occured in 3mf reader: %s" , e)
return result return result

View file

@ -31,36 +31,36 @@ UM.Dialog
columns: 2 columns: 2
Text { Text {
text: catalog.i18nc("@action:label","Size") text: catalog.i18nc("@action:label","Size (mm)")
Layout.fillWidth:true Layout.fillWidth:true
} }
TextField { TextField {
id: size id: size
focus: true focus: true
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;} validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
text: qsTr("120") text: "120"
onTextChanged: { manager.onSizeChanged(text) } onTextChanged: { manager.onSizeChanged(text) }
} }
Text { Text {
text: catalog.i18nc("@action:label","Base Height") text: catalog.i18nc("@action:label","Base Height (mm)")
Layout.fillWidth:true Layout.fillWidth:true
} }
TextField { TextField {
id: base_height id: base_height
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;} validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
text: qsTr("2") text: "2"
onTextChanged: { manager.onBaseHeightChanged(text) } onTextChanged: { manager.onBaseHeightChanged(text) }
} }
Text { Text {
text: catalog.i18nc("@action:label","Peak Height") text: catalog.i18nc("@action:label","Peak Height (mm)")
Layout.fillWidth:true Layout.fillWidth:true
} }
TextField { TextField {
id: peak_height id: peak_height
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;} validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
text: qsTr("12") text: "12"
onTextChanged: { manager.onPeakHeightChanged(text) } onTextChanged: { manager.onPeakHeightChanged(text) }
} }
@ -68,11 +68,19 @@ UM.Dialog
text: catalog.i18nc("@action:label","Smoothing") text: catalog.i18nc("@action:label","Smoothing")
Layout.fillWidth:true Layout.fillWidth:true
} }
TextField { Rectangle {
id: smoothing width: 100
validator: IntValidator {bottom: 0; top: 100;} height: 20
text: qsTr("1") color: "transparent"
onTextChanged: { manager.onSmoothingChanged(text) }
Slider {
id: smoothing
maximumValue: 100.0
stepSize: 1.0
value: 1
width: 100
onValueChanged: { manager.onSmoothingChanged(value) }
}
} }
UM.I18nCatalog{id: catalog; name:"ultimaker"} UM.I18nCatalog{id: catalog; name:"ultimaker"}

View file

@ -1,5 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2013 David Braam
# Cura is released under the terms of the AGPLv3 or higher. # Cura is released under the terms of the AGPLv3 or higher.
import os import os
@ -21,28 +20,17 @@ class ImageReader(MeshReader):
super(ImageReader, self).__init__() super(ImageReader, self).__init__()
self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"] self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
self._ui = ImageReaderUI(self) self._ui = ImageReaderUI(self)
self._wait = False
self._canceled = False def preRead(self, file_name):
self._ui.showConfigUI()
self._ui.waitForUIToClose()
if self._ui.getCancelled():
return MeshReader.PreReadResult.cancelled
return MeshReader.PreReadResult.accepted
def read(self, file_name): def read(self, file_name):
extension = os.path.splitext(file_name)[1] return self._generateSceneNode(file_name, self._ui.size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512)
if extension.lower() in self._supported_extensions:
self._ui.showConfigUI()
self._wait = True
self._canceled = True
while self._wait:
pass
# this causes the config window to not repaint...
# Job.yieldThread()
result = None
if not self._canceled:
result = self._generateSceneNode(file_name, self._ui.size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512)
return result
return None
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size): def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size):
mesh = None mesh = None
@ -99,15 +87,20 @@ class ImageReader(MeshReader):
Job.yieldThread() Job.yieldThread()
for i in range(0, blur_iterations): for i in range(0, blur_iterations):
ii = blur_iterations-i copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode='edge')
copy = numpy.copy(height_data)
height_data += numpy.roll(copy, ii, axis=0) height_data += copy[1:-1, 2:]
height_data += numpy.roll(copy, -ii, axis=0) height_data += copy[1:-1, :-2]
height_data += numpy.roll(copy, ii, axis=1) height_data += copy[2:, 1:-1]
height_data += numpy.roll(copy, -ii, axis=1) height_data += copy[:-2, 1:-1]
height_data += copy[2:, 2:]
height_data += copy[:-2, 2:]
height_data += copy[2:, :-2]
height_data += copy[:-2, :-2]
height_data /= 9
height_data /= 5
Job.yieldThread() Job.yieldThread()
height_data *= scale_vector.y height_data *= scale_vector.y
@ -118,8 +111,9 @@ class ImageReader(MeshReader):
mesh.reserveFaceCount(total_face_count) mesh.reserveFaceCount(total_face_count)
# initialize to texel space vertex offsets # initialize to texel space vertex offsets.
heightmap_vertices = numpy.zeros(((width - 1) * (height - 1), 6, 3), dtype=numpy.float32) # 6 is for 6 vertices for each texel quad.
heightmap_vertices = numpy.zeros((width_minus_one * height_minus_one, 6, 3), dtype=numpy.float32)
heightmap_vertices = heightmap_vertices + numpy.array([[ heightmap_vertices = heightmap_vertices + numpy.array([[
[0, base_height, 0], [0, base_height, 0],
[0, base_height, texel_height], [0, base_height, texel_height],

View file

@ -1,8 +1,8 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2013 David Braam
# Cura is released under the terms of the AGPLv3 or higher. # Cura is released under the terms of the AGPLv3 or higher.
import os import os
import threading
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject
from PyQt5.QtQml import QQmlComponent, QQmlContext from PyQt5.QtQml import QQmlComponent, QQmlContext
@ -23,12 +23,27 @@ class ImageReaderUI(QObject):
self.image_reader = image_reader self.image_reader = image_reader
self._ui_view = None self._ui_view = None
self.show_config_ui_trigger.connect(self._actualShowConfigUI) self.show_config_ui_trigger.connect(self._actualShowConfigUI)
self.size = 120
# There are corresponding values for these fields in ConfigUI.qml.
# If you change the values here, consider updating ConfigUI.qml as well.
self.size = 120
self.base_height = 2 self.base_height = 2
self.peak_height = 12 self.peak_height = 12
self.smoothing = 1 self.smoothing = 1
self._ui_lock = threading.Lock()
self._cancelled = False
def getCancelled(self):
return self._cancelled
def waitForUIToClose(self):
self._ui_lock.acquire()
self._ui_lock.release()
def showConfigUI(self): def showConfigUI(self):
self._ui_lock.acquire()
self._cancelled = False
self.show_config_ui_trigger.emit() self.show_config_ui_trigger.emit()
def _actualShowConfigUI(self): def _actualShowConfigUI(self):
@ -49,15 +64,15 @@ class ImageReaderUI(QObject):
@pyqtSlot() @pyqtSlot()
def onOkButtonClicked(self): def onOkButtonClicked(self):
self.image_reader._canceled = False self._cancelled = False
self.image_reader._wait = False
self._ui_view.close() self._ui_view.close()
self._ui_lock.release()
@pyqtSlot() @pyqtSlot()
def onCancelButtonClicked(self): def onCancelButtonClicked(self):
self.image_reader._canceled = True self._cancelled = True
self.image_reader._wait = False
self._ui_view.close() self._ui_view.close()
self._ui_lock.release()
@pyqtSlot(str) @pyqtSlot(str)
def onSizeChanged(self, value): def onSizeChanged(self, value):
@ -80,9 +95,6 @@ class ImageReaderUI(QObject):
else: else:
self.peak_height = 0 self.peak_height = 0
@pyqtSlot(str) @pyqtSlot(float)
def onSmoothingChanged(self, value): def onSmoothingChanged(self, value):
if (len(value) > 0): self.smoothing = int(value)
self.smoothing = int(value)
else:
self.smoothing = 0

View file

@ -4,7 +4,7 @@
from . import ImageReader from . import ImageReader
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("uranium") i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {