def process(self): # Based on https://github.com/daid/Cura/blob/SteamEngine/Cura/util/printableObject.py#L207 # Note: Y & Z axis are swapped transformed_vertices = self._node.getMeshDataTransformed().getVertices() min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None for v in transformed_vertices: diff = v - min_y_vertex length = math.sqrt(diff[0] * diff[0] + diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: continue dot = (diff[1] / length) if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: self._emitProgress(len(transformed_vertices)) return rad = math.atan2(dot_v[2], dot_v[0]) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Y), SceneNode.TransformSpace.Parent) rad = -math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Z), SceneNode.TransformSpace.Parent) transformed_vertices = self._node.getMeshDataTransformed().getVertices() min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None for v in transformed_vertices: diff = v - min_y_vertex length = math.sqrt(diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: continue dot = (diff[1] / length) if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: self._node.setOrientation(self._old_orientation) return if dot_v[2] < 0: rad = -math.asin(dot_min) else: rad = math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_X), SceneNode.TransformSpace.Parent) self._new_orientation = self._node.getOrientation()
def test_rotate(self): node = SceneNode() self.assertEqual(node.getOrientation(), Quaternion()) node.rotate(Quaternion.fromAngleAxis(math.pi / 4, Vector.Unit_Z)) self.assertEqual(node.getOrientation(), Quaternion.fromAngleAxis(math.pi / 4, Vector.Unit_Z)) node.rotate(Quaternion.fromAngleAxis(math.pi / 4, Vector.Unit_Z)) self.assertEqual(node.getOrientation(), Quaternion.fromAngleAxis(math.pi / 2, Vector.Unit_Z))
def test_rotate(self): node = SceneNode() self.assertEqual(node.getOrientation(), Quaternion()) node.rotate(Quaternion.fromAngleAxis(math.pi / 4, Vector.Unit_Z)) node_orientation = deepcopy(node.getOrientation()) node_orientation.normalize() #For fair comparison. self.assertEqual(node_orientation, Quaternion.fromAngleAxis(math.pi / 4, Vector.Unit_Z)) node.rotate(Quaternion.fromAngleAxis(math.pi / 4, Vector.Unit_Z)) node_orientation = deepcopy(node.getOrientation()) node_orientation.normalize() self.assertEqual(node_orientation, Quaternion.fromAngleAxis(math.pi / 2, Vector.Unit_Z))
def test_translateWorld(self): node1 = SceneNode() node2 = SceneNode(node1) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 0)) node1.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 10)) node2.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 20)) node1.rotate(Quaternion.fromAngleAxis(math.pi / 2, Vector.Unit_Y)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(10, 0, 10)) node2.translate(Vector(0, 0, 10)) # Local translation on Z with a parent rotated 90 degrees results in movement on X axis pos = node2.getWorldPosition() #Using fuzzyCompare due to accumulation of floating point error self.assertTrue(Float.fuzzyCompare(pos.x, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) self.assertTrue(Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) self.assertTrue(Float.fuzzyCompare(pos.z, 10, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) node2.translate(Vector(0, 0, 10), SceneNode.TransformSpace.World) # World translation on Z with a parent rotated 90 degrees results in movement on Z axis pos = node2.getWorldPosition() self.assertTrue(Float.fuzzyCompare(pos.x, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) self.assertTrue(Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) self.assertTrue(Float.fuzzyCompare(pos.z, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) node1.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(10, 0, 10)) pos = node2.getWorldPosition() self.assertTrue(Float.fuzzyCompare(pos.x, 30, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) self.assertTrue(Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) self.assertTrue(Float.fuzzyCompare(pos.z, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) node1.scale(Vector(2, 2, 2)) pos = node2.getWorldPosition() self.assertTrue(Float.fuzzyCompare(pos.x, 50, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) self.assertTrue(Float.fuzzyCompare(pos.y, 0, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) self.assertTrue(Float.fuzzyCompare(pos.z, 30, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) node2.translate(Vector(0, 0, 10)) pos = node2.getWorldPosition() self.assertTrue(Float.fuzzyCompare(pos.x, 70, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) self.assertTrue(Float.fuzzyCompare(pos.y, 0, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) self.assertTrue(Float.fuzzyCompare(pos.z, 30, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) # World space set position node1 = SceneNode() node2 = SceneNode(node1) node1.setPosition(Vector(15,15,15)) node2.setPosition(Vector(10,10,10)) self.assertEqual(node2.getWorldPosition(), Vector(25, 25, 25)) node2.setPosition(Vector(15,15,15), SceneNode.TransformSpace.World) self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 15)) self.assertEqual(node2.getPosition(), Vector(0,0,0)) node1.setPosition(Vector(15,15,15)) node2.setPosition(Vector(0,0,0)) node2.rotate(Quaternion.fromAngleAxis(-math.pi / 2, Vector.Unit_Y)) node2.translate(Vector(10,0,0)) self.assertEqual(node2.getWorldPosition(), Vector(15,15,25)) node2.setPosition(Vector(15,15,25), SceneNode.TransformSpace.World) self.assertEqual(node2.getWorldPosition(), Vector(15,15,25)) self.assertEqual(node2.getPosition(), Vector(0,0,10))
def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.MousePressEvent: if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return if ToolHandle.isAxis(id): self.setLockedAxis(id) handle_position = self._handle.getWorldPosition() if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) self.setDragStart(event.x, event.y) self._angle = 0 self.operationStarted.emit(self) if event.type == Event.MouseMoveEvent: if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalize() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return drag_end = (drag_position - handle_position).normalize() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) self._angle += direction * angle # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.01: self.propertyChanged.emit() self._angle_update_time = new_time Selection.applyOperation(RotateOperation, rotation) self.setDragStart(event.x, event.y) if event.type == Event.MouseReleaseEvent: if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._angle = None self.propertyChanged.emit() self.operationStopped.emit(self) return True
def process(self): # Based on https://github.com/daid/Cura/blob/SteamEngine/Cura/util/printableObject.py#L207 # Note: Y & Z axis are swapped #Transform mesh first to get the current positions of the vertices. transformed_vertices = None if not self._node.callDecoration("isGroup"): transformed_vertices = self._node.getMeshDataTransformed().getVertices() else: #For groups, get the vertices of all children and process them as a single mesh for child in self._node.getChildren(): if transformed_vertices is None: transformed_vertices = child.getMeshDataTransformed().getVertices() else: transformed_vertices = numpy.concatenate((transformed_vertices, child.getMeshDataTransformed().getVertices()), axis = 0) min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 #Minimum y-component of direction vector. dot_v = None #Find the second-lowest vertex. for v in transformed_vertices: diff = v - min_y_vertex #From this vertex to the lowest vertex. length = math.sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]) if length < 5: #Ignore lines smaller than half a centimetre. It's unreliable at such small distances. continue dot = (diff[1] / length) #Y-component of direction vector. if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: #Couldn't find any vertex further than 5mm from the lowest vertex. self._emitProgress(len(transformed_vertices)) return #Rotate the mesh such that the second-lowest vertex is just as low as the lowest vertex. rad = math.atan2(dot_v[2], dot_v[0]) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Y), SceneNode.TransformSpace.Parent) rad = -math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Z), SceneNode.TransformSpace.Parent) #Apply the transformation so we get new vertex coordinates. transformed_vertices = None if not self._node.callDecoration("isGroup"): transformed_vertices = self._node.getMeshDataTransformed().getVertices() else: #For groups, get the vertices of all children and process them as a single mesh for child in self._node.getChildren(): if transformed_vertices is None: transformed_vertices = child.getMeshDataTransformed().getVertices() else: transformed_vertices = numpy.concatenate((transformed_vertices, child.getMeshDataTransformed().getVertices()), axis = 0) min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None #Find the second-lowest vertex again. for v in transformed_vertices: diff = v - min_y_vertex #From this vertex to the lowest vertex. length = math.sqrt(diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: #Ignore lines smaller than half a centimetre. It's unreliable at such small distances. continue dot = (diff[1] / length) #Y-component of direction vector. if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: #Couldn't find any vertex further than 5mm from the lowest vertex. self._node.setOrientation(self._old_orientation) return #Rotate the mesh such that the second-lowest vertex gets the same height as the lowest vertex. if dot_v[2] < 0: rad = -math.asin(dot_min) else: rad = math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_X), SceneNode.TransformSpace.Parent) self._new_orientation = self._node.getOrientation() #Save the resulting orientation.
def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: # Snap is toggled when pressing the shift button self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: # Snap is "toggled back" when releasing the shift button self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): # Start a rotate operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return if ToolHandle.isAxis(id): self.setLockedAxis(id) handle_position = self._handle.getWorldPosition() # Save the current positions of the node, as we want to rotate around their current centres self._saved_node_positions = [] for node in Selection.getAllSelectedObjects(): self._saved_node_positions.append((node, node.getWorldPosition())) if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) self.setDragStart(event.x, event.y) self._angle = 0 self.operationStarted.emit(self) if event.type == Event.MouseMoveEvent: # Perform a rotate operation if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalized() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return drag_end = (drag_position - handle_position).normalized() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.1: self._angle_update_time = new_time self._angle += direction * angle self.propertyChanged.emit() # Rotate around the saved centeres of all selected nodes op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation(RotateOperation(node, rotation, rotate_around_point = position)) op.push() self.setDragStart(event.x, event.y) if event.type == Event.MouseReleaseEvent: # Finish a rotate operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._angle = None self.propertyChanged.emit() self.operationStopped.emit(self) return True
def read(self, file_name): 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. objects = root.findall("./3mf:resources/3mf:object", self._namespaces) 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")]) 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]) #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 S2 = temp_mat.getTransposed().multiply(temp_mat) scale_x = math.sqrt(S2.at(0,0)) scale_y = math.sqrt(S2.at(1,1)) scale_z = math.sqrt(S2.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) #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
def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: # Snap is toggled when pressing the shift button self.setRotationSnap(not self._snap_rotation) if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: # Snap is "toggled back" when releasing the shift button self.setRotationSnap(not self._snap_rotation) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled( ): # Start a rotate operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if self._handle.isAxis(id): self.setLockedAxis(id) else: # Not clicked on an axis: do nothing. return False handle_position = self._handle.getWorldPosition() # Save the current positions of the node, as we want to rotate around their current centres self._saved_node_positions = [] for node in self._getSelectedObjectsWithoutSelectedAncestors(): self._saved_node_positions.append((node, node.getPosition())) if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) else: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) self.setDragStart(event.x, event.y) self._rotating = False self._angle = 0 return True if event.type == Event.MouseMoveEvent: # Perform a rotate operation if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) if not self.getDragStart(): #May have set it to None. return False if not self._rotating: self._rotating = True self.operationStarted.emit(self) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalized() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return False drag_end = (drag_position - handle_position).normalized() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return False rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) else: direction = -1 # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.1: self._angle_update_time = new_time self._angle += direction * angle self.propertyChanged.emit() # Rotate around the saved centeres of all selected nodes if len(self._saved_node_positions) > 1: op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation( RotateOperation(node, rotation, rotate_around_point=position)) op.push() else: for node, position in self._saved_node_positions: RotateOperation(node, rotation, rotate_around_point=position).push() self.setDragStart(event.x, event.y) return True if event.type == Event.MouseReleaseEvent: # Finish a rotate operation if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(ToolHandle.NoAxis) self._angle = None self.propertyChanged.emit() if self._rotating: self.operationStopped.emit(self) return True
def process(self): """Computes some orientation to hopefully lay the object flat. No promises! This algorithm finds the lowest three vertices and lays them flat. This is a rather naive heuristic, but fast and practical. """ # Based on https://github.com/daid/Cura/blob/SteamEngine/Cura/util/printableObject.py#L207 # Note: Y & Z axis are swapped #Transform mesh first to get the current positions of the vertices. transformed_vertices = self._node.getMeshDataTransformedVertices() min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 #Minimum y-component of direction vector. dot_v = None #Find the second-lowest vertex. for v in transformed_vertices: diff = v - min_y_vertex #From this vertex to the lowest vertex. length = math.sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]) if length < 5: #Ignore lines smaller than half a centimetre. It's unreliable at such small distances. continue dot = (diff[1] / length) #Y-component of direction vector. if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: #Couldn't find any vertex further than 5mm from the lowest vertex. self._emitProgress(len(transformed_vertices)) return #Rotate the mesh such that the second-lowest vertex is just as low as the lowest vertex. rad = math.atan2(dot_v[2], dot_v[0]) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Y), SceneNode.TransformSpace.Parent) rad = -math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Z), SceneNode.TransformSpace.Parent) #Apply the transformation so we get new vertex coordinates. transformed_vertices = self._node.getMeshDataTransformedVertices() min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None #Find the second-lowest vertex again. for v in transformed_vertices: diff = v - min_y_vertex #From this vertex to the lowest vertex. length = math.sqrt(diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: #Ignore lines smaller than half a centimetre. It's unreliable at such small distances. continue dot = (diff[1] / length) #Y-component of direction vector. if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: #Couldn't find any vertex further than 5mm from the lowest vertex. self._node.setOrientation(self._old_orientation) return #Rotate the mesh such that the second-lowest vertex gets the same height as the lowest vertex. if dot_v[2] < 0: rad = -math.asin(dot_min) else: rad = math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_X), SceneNode.TransformSpace.Parent) self._new_orientation = self._node.getOrientation() #Save the resulting orientation.
def test_translateWorld(self): node1 = SceneNode() node2 = SceneNode(node1) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 0)) node1.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 10)) node2.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 20)) node1.rotate(Quaternion.fromAngleAxis(math.pi / 2, Vector.Unit_Y)) self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10)) self.assertEqual(node2.getWorldPosition(), Vector(10, 0, 10)) node2.translate(Vector(0, 0, 10)) # Local translation on Z with a parent rotated 90 degrees results in movement on X axis pos = node2.getWorldPosition() #Using fuzzyCompare due to accumulation of floating point error self.assertTrue( Float.fuzzyCompare(pos.x, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) self.assertTrue( Float.fuzzyCompare(pos.z, 10, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 10))) node2.translate(Vector(0, 0, 10), SceneNode.TransformSpace.World) # World translation on Z with a parent rotated 90 degrees results in movement on Z axis pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.z, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(20, 0, 20))) node1.translate(Vector(0, 0, 10)) self.assertEqual(node1.getWorldPosition(), Vector(10, 0, 10)) pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 30, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) self.assertTrue( Float.fuzzyCompare(pos.z, 20, 1e-5), "{0} does not equal {1}".format(pos, Vector(30, 0, 20))) node1.scale(Vector(2, 2, 2)) pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 50, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.z, 30, 1e-4), "{0} does not equal {1}".format(pos, Vector(50, 0, 30))) node2.translate(Vector(0, 0, 10)) pos = node2.getWorldPosition() self.assertTrue( Float.fuzzyCompare(pos.x, 70, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.y, 0, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) self.assertTrue( Float.fuzzyCompare(pos.z, 30, 1e-4), "{0} does not equal {1}".format(pos, Vector(70, 0, 30))) # World space set position node1 = SceneNode() node2 = SceneNode(node1) node1.setPosition(Vector(15, 15, 15)) node2.setPosition(Vector(10, 10, 10)) self.assertEqual(node2.getWorldPosition(), Vector(25, 25, 25)) #node2.setPosition(Vector(15,15,15), SceneNode.TransformSpace.World) #self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 15)) #self.assertEqual(node2.getPosition(), Vector(0,0,0)) node1.setPosition(Vector(15, 15, 15)) node2.setPosition(Vector(0, 0, 0)) node2.rotate(Quaternion.fromAngleAxis(-math.pi / 2, Vector.Unit_Y)) node2.translate(Vector(10, 0, 0)) self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 25)) node2.setPosition(Vector(15, 15, 25), SceneNode.TransformSpace.World) self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 25)) self.assertEqual(node2.getPosition(), Vector(0, 0, 10))
def process(self): # Based on https://github.com/daid/Cura/blob/SteamEngine/Cura/util/printableObject.py#L207 # Note: Y & Z axis are swapped #Transform mesh first to get the current positions of the vertices. transformed_vertices = None if not self._node.callDecoration("isGroup"): transformed_vertices = self._node.getMeshDataTransformed( ).getVertices() else: #For groups, get the vertices of all children and process them as a single mesh for child in self._node.getChildren(): if transformed_vertices is None: transformed_vertices = child.getMeshDataTransformed( ).getVertices() else: transformed_vertices = numpy.concatenate( (transformed_vertices, child.getMeshDataTransformed().getVertices()), axis=0) min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 #Minimum y-component of direction vector. dot_v = None #Find the second-lowest vertex. for v in transformed_vertices: diff = v - min_y_vertex #From this vertex to the lowest vertex. length = math.sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]) if length < 5: #Ignore lines smaller than half a centimetre. It's unreliable at such small distances. continue dot = (diff[1] / length) #Y-component of direction vector. if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: #Couldn't find any vertex further than 5mm from the lowest vertex. self._emitProgress(len(transformed_vertices)) return #Rotate the mesh such that the second-lowest vertex is just as low as the lowest vertex. rad = math.atan2(dot_v[2], dot_v[0]) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Y), SceneNode.TransformSpace.Parent) rad = -math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Z), SceneNode.TransformSpace.Parent) #Apply the transformation so we get new vertex coordinates. transformed_vertices = None if not self._node.callDecoration("isGroup"): transformed_vertices = self._node.getMeshDataTransformed( ).getVertices() else: #For groups, get the vertices of all children and process them as a single mesh for child in self._node.getChildren(): if transformed_vertices is None: transformed_vertices = child.getMeshDataTransformed( ).getVertices() else: transformed_vertices = numpy.concatenate( (transformed_vertices, child.getMeshDataTransformed().getVertices()), axis=0) min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None #Find the second-lowest vertex again. for v in transformed_vertices: diff = v - min_y_vertex #From this vertex to the lowest vertex. length = math.sqrt(diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: #Ignore lines smaller than half a centimetre. It's unreliable at such small distances. continue dot = (diff[1] / length) #Y-component of direction vector. if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: #Couldn't find any vertex further than 5mm from the lowest vertex. self._node.setOrientation(self._old_orientation) return #Rotate the mesh such that the second-lowest vertex gets the same height as the lowest vertex. if dot_v[2] < 0: rad = -math.asin(dot_min) else: rad = math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_X), SceneNode.TransformSpace.Parent) self._new_orientation = self._node.getOrientation( ) #Save the resulting orientation.
def process(self): # Based on https://github.com/daid/Cura/blob/SteamEngine/Cura/util/printableObject.py#L207 # Note: Y & Z axis are swapped transformed_vertices = self._node.getMeshDataTransformed().getVertices( ) min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None for v in transformed_vertices: diff = v - min_y_vertex length = math.sqrt(diff[0] * diff[0] + diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: continue dot = (diff[1] / length) if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: self._emitProgress(len(transformed_vertices)) return rad = math.atan2(dot_v[2], dot_v[0]) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Y), SceneNode.TransformSpace.Parent) rad = -math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_Z), SceneNode.TransformSpace.Parent) transformed_vertices = self._node.getMeshDataTransformed().getVertices( ) min_y_vertex = transformed_vertices[transformed_vertices.argmin(0)[1]] dot_min = 1.0 dot_v = None for v in transformed_vertices: diff = v - min_y_vertex length = math.sqrt(diff[2] * diff[2] + diff[1] * diff[1]) if length < 5: continue dot = (diff[1] / length) if dot_min > dot: dot_min = dot dot_v = diff self._emitProgress(1) if dot_v is None: self._node.setOrientation(self._old_orientation) return if dot_v[2] < 0: rad = -math.asin(dot_min) else: rad = math.asin(dot_min) self._node.rotate(Quaternion.fromAngleAxis(rad, Vector.Unit_X), SceneNode.TransformSpace.Parent) self._new_orientation = self._node.getOrientation()
def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: self._snap_rotation = (not self._snap_rotation) if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: self._snap_rotation = (not self._snap_rotation) if event.type == Event.MousePressEvent: if MouseEvent.LeftButton not in event.buttons: return False id = self._renderer.getIdAtCoordinate(event.x, event.y) if not id: return if ToolHandle.isAxis(id): self.setLockedAxis(id) handle_position = self._handle.getWorldPosition() if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) self.setDragStart(event.x, event.y) if event.type == Event.MouseMoveEvent: if not self.getDragPlane(): return False handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalize() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return drag_end = (drag_position - handle_position).normalize() angle = math.acos(drag_start.dot(drag_end)) if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot(drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) Selection.applyOperation(RotateOperation, rotation) self.setDragStart(event.x, event.y) self.updateHandlePosition() if event.type == Event.MouseReleaseEvent: if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) return True
def event(self, event): super().event(event) if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey: self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey: self._snap_rotation = (not self._snap_rotation) self.propertyChanged.emit() if event.type == Event.MousePressEvent: if MouseEvent.LeftButton not in event.buttons: return False id = self._renderer.getIdAtCoordinate(event.x, event.y) if not id: return if ToolHandle.isAxis(id): self.setLockedAxis(id) handle_position = self._handle.getWorldPosition() if id == ToolHandle.XAxis: self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x)) elif id == ToolHandle.YAxis: self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y)) elif self._locked_axis == ToolHandle.ZAxis: self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z)) self.setDragStart(event.x, event.y) self._angle = 0 self.operationStarted.emit(self) if event.type == Event.MouseMoveEvent: if not self.getDragPlane(): return False if not self.getDragStart(): self.setDragStart(event.x, event.y) handle_position = self._handle.getWorldPosition() drag_start = (self.getDragStart() - handle_position).normalize() drag_position = self.getDragPosition(event.x, event.y) if not drag_position: return drag_end = (drag_position - handle_position).normalize() try: angle = math.acos(drag_start.dot(drag_end)) except ValueError: angle = 0 if self._snap_rotation: angle = int(angle / self._snap_angle) * self._snap_angle if angle == 0: return rotation = None if self.getLockedAxis() == ToolHandle.XAxis: direction = 1 if Vector.Unit_X.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X) elif self.getLockedAxis() == ToolHandle.YAxis: direction = 1 if Vector.Unit_Y.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y) elif self.getLockedAxis() == ToolHandle.ZAxis: direction = 1 if Vector.Unit_Z.dot( drag_start.cross(drag_end)) > 0 else -1 rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z) self._angle += direction * angle # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.01: self.propertyChanged.emit() self._angle_update_time = new_time Selection.applyOperation(RotateOperation, rotation) self.setDragStart(event.x, event.y) if event.type == Event.MouseReleaseEvent: if self.getDragPlane(): self.setDragPlane(None) self.setLockedAxis(None) self._angle = None self.propertyChanged.emit() self.operationStopped.emit(self) return True