def test_toMatrix(self): q1 = Quaternion() q1.setByAngleAxis(math.pi / 2, Vector.Unit_Z) m1 = q1.toMatrix() m2 = Matrix() m2.setByRotationAxis(math.pi / 2, Vector.Unit_Z) self.assertTrue(Float.fuzzyCompare(m1.at(0, 0), m2.at(0, 0), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(0, 1), m2.at(0, 1), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(0, 2), m2.at(0, 2), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(0, 3), m2.at(0, 3), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(1, 0), m2.at(1, 0), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(1, 1), m2.at(1, 1), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(1, 2), m2.at(1, 2), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(1, 3), m2.at(1, 3), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(2, 0), m2.at(2, 0), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(2, 1), m2.at(2, 1), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(2, 2), m2.at(2, 2), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(2, 3), m2.at(2, 3), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(3, 0), m2.at(3, 0), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(3, 1), m2.at(3, 1), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(3, 2), m2.at(3, 2), 1e-6)) self.assertTrue(Float.fuzzyCompare(m1.at(3, 3), m2.at(3, 3), 1e-6))
def __init__(self, parent=None): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = "" self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self)
def setZ(self, Z): if float(Z) != self._Z_angle: self._angle = ((float(Z) % 360) - (self._Z_angle % 360)) % 360 self._Z_angle = float(Z) #rotation = Quaternion.fromAngleAxis(math.radians( self._angle ), Vector.Unit_Z) rotation = Quaternion() rotation.setByAngleAxis(math.radians(self._angle), Vector.Unit_Z) # 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.getPosition())) node._rotationZ = self._Z_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.1: self._angle_update_time = new_time # 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._angle = 0 self.propertyChanged.emit()
def run(self): for node in self._nodes: transformed_vertices = node.getMeshDataTransformed().getVertices() result = Tweak( transformed_vertices, extended_mode=self._extended_mode, verbose=False, progress_callback=self.updateProgress, min_volume=CuraApplication.getInstance().getPreferences( ).getValue("OrientationPlugin/min_volume")) [v, phi] = result.euler_parameter # Convert the new orientation into quaternion new_orientation = Quaternion.fromAngleAxis( phi, Vector(-v[0], -v[1], -v[2])) # Rotate the axis frame. rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1, 0, 0)) new_orientation = rotation * new_orientation # Ensure node gets the new orientation node.rotate(new_orientation, SceneNode.TransformSpace.World) Job.yieldThread()
def _updateTransformation(self) -> None: scale, shear, euler_angles, translation, mirror = self._transformation.decompose( ) self._position = translation self._scale = scale self._shear = shear self._mirror = mirror orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation( ).multiply(self._transformation, copy=True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose( ) self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix)
def test_getData(self): q = Quaternion(1, 2, 3, 4) data = q.getData() assert data[0] == 1 assert data[1] == 2 assert data[2] == 3 assert data[3] == 4
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_setByAxis(self): q = Quaternion() q.setByAngleAxis(math.pi / 2, Vector.Unit_Z) self.assertEqual(q.x, 0.0) self.assertEqual(q.y, 0.0) self.assertTrue(Float.fuzzyCompare(q.z, math.sqrt(2.0) / 2.0, 1e-6)) self.assertTrue(Float.fuzzyCompare(q.w, math.sqrt(2.0) / 2.0, 1e-6))
def _updateLocalTransformation(self): translation, euler_angle_matrix, scale, shear = self._transformation.decompose() self._position = translation self._scale = scale self._shear = shear orientation = Quaternion() orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation
def test_invert(self): q1 = Quaternion() q1.setByAngleAxis(math.pi, Vector.Unit_Z) q1.invert() q2 = Quaternion() q2.setByAngleAxis(math.pi, -Vector.Unit_Z) self.assertEqual(q1, q2)
def test_rotateVector(self): q1 = Quaternion() q1.setByAngleAxis(math.pi / 2, Vector.Unit_Z) v = Vector(0, 1, 0) v = q1.rotate(v) self.assertTrue(Float.fuzzyCompare(v.x, -1.0, 1e-6)) self.assertTrue(Float.fuzzyCompare(v.y, 0.0, 1e-6)) self.assertTrue(Float.fuzzyCompare(v.z, 0.0, 1e-6))
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "") -> None: super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: Optional[MeshData] # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBoundingBox be re-calculated? self._calculate_aabb = False # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = visible # type: bool self._name = name # type: str self._decorators = [] # type: List[SceneNodeDecorator] # Store custom settings to be compatible with Savitar SceneNode self._settings = {} #type: Dict[str, Any] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self)
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_fromMatrix(self): m = Matrix() m.setByRotationAxis(math.pi / 2, Vector.Unit_Z) q1 = Quaternion.fromMatrix(m) q2 = Quaternion() q2.setByAngleAxis(math.pi / 2, Vector.Unit_Z) self.assertTrue(Float.fuzzyCompare(q1.x, q2.x, 1e-6)) self.assertTrue(Float.fuzzyCompare(q1.y, q2.y, 1e-6)) self.assertTrue(Float.fuzzyCompare(q1.z, q2.z, 1e-6)) self.assertTrue(Float.fuzzyCompare(q1.w, q2.w, 1e-6))
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "") -> None: super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: Optional[MeshData] # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBoundingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = visible # type: bool self._name = name # type: str self._decorators = [] # type: List[SceneNodeDecorator] # Store custom settings to be compatible with Savitar SceneNode self._settings = {} # type: Dict[str, Any] ## Signals self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self)
def __init__(self, parent = None, **kwargs): super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: MeshData # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBounxingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = kwargs.get("visible", True) # type: bool self._name = kwargs.get("name", "") # type: str self._decorators = [] # type: List[SceneNodeDecorator] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self)
def test_multiply(self): q1 = Quaternion() q1.setByAngleAxis(math.pi / 2, Vector.Unit_Z) q2 = Quaternion() q2.setByAngleAxis(math.pi / 2, Vector.Unit_Z) q3 = q1 * q2 q4 = Quaternion() q4.setByAngleAxis(math.pi, Vector.Unit_Z) self.assertEqual(q3, q4)
def resetAll(self): nodes = [] for node in DepthFirstIterator( self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue #Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue #Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: # Ensure that the object is above the build platform move_distance = node.getBoundingBox().center.y if move_distance <= 0: move_distance = -node.getBoundingBox().bottom op.addOperation( SetTransformOperation(node, Vector(0, move_distance, 0), Quaternion(), Vector(1, 1, 1))) op.push()
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 _updateTransformation(self): self._transformation = Matrix.fromPositionOrientationScale( self._position, self._orientation, self._scale) if self._parent: parent_orientation = self._parent._getDerivedOrientation() if self._inherit_orientation: self._derived_orientation = parent_orientation * self._orientation else: self._derived_orientation = self._orientation # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() parent_scale = self._parent._getDerivedScale() if self._inherit_scale: self._derived_scale = parent_scale.scale(self._scale) else: self._derived_scale = self._scale self._derived_position = parent_orientation.rotate( parent_scale.scale(self._position)) self._derived_position += self._parent._getDerivedPosition() self._world_transformation = Matrix.fromPositionOrientationScale( self._derived_position, self._derived_orientation, self._derived_scale) else: self._derived_position = self._position self._derived_orientation = self._orientation self._derived_scale = self._scale self._world_transformation = self._transformation
def resetRotation(self): for node in Selection.getAllSelectedObjects(): node.setMirror(Vector(1, 1, 1)) Selection.applyOperation(SetTransformOperation, None, Quaternion(), None)
def resetRotation(self): for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.setMirror(Vector(1, 1, 1)) Selection.applyOperation(SetTransformOperation, None, Quaternion(), None)
def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged()
def resetRotation(self): """Reset the orientation of the mesh(es) to their original orientation(s)""" for node in self._getSelectedObjectsWithoutSelectedAncestors(): node.setMirror(Vector(1, 1, 1)) Selection.applyOperation(SetTransformOperation, None, Quaternion(), None)
def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local) -> None: """Set the local orientation of this scene node. :param orientation: :type{Quaternion} The new orientation of this scene node. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or orientation == self._orientation: return if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).invert() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix = Matrix() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged()
def resetAll(self): Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator( self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: # Ensure that the object is above the build platform node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) center_y = 0 if node.callDecoration("isGroup"): center_y = node.getWorldPosition().y - node.getBoundingBox( ).bottom else: center_y = node.getMeshData().getCenterPosition().y op.addOperation( SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push()
def nodePostProcessing(self, node): # TODO: Investigate how the status is on SolidWorks 2018 (now beta) if self._revision_major >= 24: # Known problem under SolidWorks 2016 until 2017: Exported models are rotated by -90 degrees. This rotates it back! rotation = Quaternion.fromAngleAxis(math.radians(90), Vector.Unit_X) node.rotate(rotation) return node
def _onSelectedFaceChanged(self): self._handle.setEnabled(not Selection.getFaceSelectMode()) selected_face = Selection.getSelectedFace() if not Selection.getSelectedFace() or not (Selection.hasSelection() and Selection.getFaceSelectMode()): return original_node, face_id = selected_face meshdata = original_node.getMeshDataTransformed() if not meshdata or face_id < 0: return rotation_point, face_normal = meshdata.getFacePlane(face_id) rotation_point_vector = Vector(rotation_point[0], rotation_point[1], rotation_point[2]) face_normal_vector = Vector(face_normal[0], face_normal[1], face_normal[2]) rotation_quaternion = Quaternion.rotationTo(face_normal_vector.normalized(), Vector(0.0, -1.0, 0.0)) operation = GroupedOperation() current_node = None # type: Optional[SceneNode] for node in Selection.getAllSelectedObjects(): current_node = node parent_node = current_node.getParent() while parent_node and parent_node.callDecoration("isGroup"): current_node = parent_node parent_node = current_node.getParent() if current_node is None: return rotate_operation = RotateOperation(current_node, rotation_quaternion, rotation_point_vector) operation.addOperation(rotate_operation) operation.push()
def test_create(self): q = Quaternion() self.assertEqual(q.x, 0.0) self.assertEqual(q.y, 0.0) self.assertEqual(q.z, 0.0) self.assertEqual(q.w, 1.0)
def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: scene_root = Application.getInstance().getController().getScene().getRoot() found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) not_fit_count = 0 grouped_operation = GroupedOperation() for node, node_item in zip(nodes_to_arrange, node_items): if add_new_nodes_in_scene: grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) if node_item.binId() == 0: # We found a spot for it rotation_matrix = Matrix() rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, node_item.translation().y() / factor))) else: # We didn't find a spot grouped_operation.addOperation( TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) not_fit_count += 1 return grouped_operation, not_fit_count
def __init__(self, parent = None): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = "" if parent: parent.addChild(self)
def lookAt(self, target, up=Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) if self._parent: self._orientation = self._parent._getDerivedOrientation() * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged()
def _getDerivedOrientation(self): if not self._derived_orientation: self._updateTransformation() # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() return self._derived_orientation
def __init__(self, parent = None, **kwargs): super().__init__() # Call super to make multiple inheritance work. self._children = [] self._mesh_data = None # Local transformation (from parent to local) self._transformation = Matrix() # Convenience "components" of the transformation self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._shear = Vector(0.0, 0.0, 0.0) self._mirror = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() # World transformation (from root to local) self._world_transformation = Matrix() # Convenience "components" of the world_transformation self._derived_position = Vector() self._derived_orientation = Quaternion() self._derived_scale = Vector() self._parent = parent self._enabled = True # Can this SceneNode be modified in any way? self._selectable = False # Can this SceneNode be selected in any way? self._calculate_aabb = True # Should the AxisAlignedBounxingBox be re-calculated? self._aabb = None # The AxisAligned bounding box. self._original_aabb = None # The AxisAligned bounding box, without transformations. self._bounding_box_mesh = None self._visible = kwargs.get("visible", True) self._name = kwargs.get("name", "") self._decorators = [] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self)
def lookAt(self, target, up=Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) if self._parent: self._orientation = self._parent._getDerivedOrientation( ) * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged()
def updateFromODE(self): self._update_from_ode = True self.getNode().setPosition(Helpers.fromODE(self._body.getPosition()), SceneNode.TransformSpace.World) body_orientation = self._body.getQuaternion() self.getNode().setOrientation( Quaternion(body_orientation[3], body_orientation[0], body_orientation[1], body_orientation[2])) self._update_from_ode = False
def _updateTransformation(self): scale, shear, euler_angles, translation, mirror = self._transformation.decompose() self._position = translation self._scale = scale self._shear = shear self._mirror = mirror orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation().multiply(self._transformation, copy = True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose() self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix)
def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged()
def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * (self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale = self._scale, angles = euler_angles, translate = self._position, shear = self._shear) self._transformation = new_transform_matrix self._transformChanged()
def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) self.setOrientation(Quaternion.fromMatrix(m))
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent = None): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = "" if parent: parent.addChild(self) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): transformed_mesh = deepcopy(self._mesh_data) transformed_mesh.transform(self.getWorldTransformation()) return transformed_mesh ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self.meshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self.meshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._orientation = self._orientation * rotation elif transform_space == SceneNode.TransformSpace.Parent: self._orientation = rotation * self._orientation elif transform_space == SceneNode.TransformSpace.World: self._orientation = self._orientation * self._getDerivedOrientation().getInverse() * rotation * self._getDerivedOrientation() else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._orientation.normalize() self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. def setOrientation(self, orientation): if not self._enabled or orientation == self._orientation: return self._orientation = orientation self._orientation.normalize() self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._scale = self._scale.scale(scale) elif transform_space == SceneNode.TransformSpace.Parent: raise NotImplementedError() elif transform_space == SceneNode.TransformSpace.World: raise NotImplementedError() else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. def setScale(self, scale): if not self._enabled or scale == self._scale: return self._scale = scale self._transformChanged() ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): if not self._derived_position: self._updateTransformation() return deepcopy(self._derived_position) ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._position += self._orientation.rotate(translation) elif transform_space == SceneNode.TransformSpace.Parent: self._position += translation elif transform_space == SceneNode.TransformSpace.World: if self._parent: self._position += (1.0 / self._parent._getDerivedScale()).scale(self._parent._getDerivedOrientation().getInverse().rotate(translation)) else: self._position += translation self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. def setPosition(self, position): if not self._enabled or position == self._position: return self._position = position self._transformChanged() ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) if self._parent: self._orientation = self._parent._getDerivedOrientation() * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged() ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate ## private: def _getDerivedPosition(self): if not self._derived_position: self._updateTransformation() return self._derived_position def _getDerivedOrientation(self): if not self._derived_orientation: self._updateTransformation() return self._derived_orientation def _getDerivedScale(self): if not self._derived_scale: self._updateTransformation() return self._derived_scale def _transformChanged(self): self._resetAABB() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): self._transformation = Matrix.fromPositionOrientationScale(self._position, self._orientation, self._scale) if self._parent: parent_orientation = self._parent._getDerivedOrientation() if self._inherit_orientation: self._derived_orientation = parent_orientation * self._orientation else: self._derived_orientation = self._orientation parent_scale = self._parent._getDerivedScale() if self._inherit_scale: self._derived_scale = parent_scale.scale(self._scale) else: self._derived_scale = self._scale self._derived_position = parent_orientation.rotate(parent_scale.scale(self._position)) self._derived_position += self._parent._getDerivedPosition() self._world_transformation = Matrix.fromPositionOrientationScale(self._derived_position, self._derived_orientation, self._derived_scale) else: self._derived_position = self._position self._derived_orientation = self._orientation self._derived_scale = self._scale self._world_transformation = self._transformation def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
def test_slerp(self): q1 = Quaternion() q1.setByAngleAxis(0, Vector.Unit_Z) q2 = Quaternion() q2.setByAngleAxis(math.pi / 2, Vector.Unit_Z) c = Quaternion(0.0, 0.0, 0.0, 1.0) self.assertEqual(c, Quaternion.slerp(q1, q2, 0.0)) c = Quaternion(0.0, 0.0, 0.19509033858776093, 0.9807853102684021) self.assertEqual(c, Quaternion.slerp(q1, q2, 0.25)) c = Quaternion(0.0, 0.0, 0.38268348574638367, 0.9238795638084412) self.assertEqual(c, Quaternion.slerp(q1, q2, 0.5)) c = Quaternion(0.0, 0.0, 0.5555703043937683, 0.8314696550369263) self.assertEqual(c, Quaternion.slerp(q1, q2, 0.75)) c = Quaternion(0.0, 0.0, 0.7071068286895752, 0.7071068286895752) self.assertEqual(c, Quaternion.slerp(q1, q2, 1.0))
class SceneNode(): class TransformSpace: Local = 1 Parent = 2 World = 3 ## Construct a scene node. # \param parent The parent of this node (if any). Only a root node should have None as a parent. # \param kwargs Keyword arguments. # Possible keywords: # - visible \type{bool} Is the SceneNode (and thus, all it's children) visible? Defaults to True # - name \type{string} Name of the SceneNode. Defaults to empty string. def __init__(self, parent = None, **kwargs): super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: MeshData # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBounxingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = kwargs.get("visible", True) # type: bool self._name = kwargs.get("name", "") # type: str self._decorators = [] # type: List[SceneNodeDecorator] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) copy._name = deepcopy(self._name, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy ## Set the center position of this node. # This is used to modify it's mesh data (and it's children) in such a way that they are centered. # In most cases this means that we use the center of mass as center (which most objects don't use) def setCenterPosition(self, center: Vector): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m).set(center_position=center) for child in self._children: child.setCenterPosition(center) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self) -> Optional["SceneNode"]: return self._parent def getMirror(self) -> Vector: return self._mirror ## Get the MeshData of the bounding box # \returns \type{MeshData} Bounding box mesh. def getBoundingBoxMesh(self) -> Optional[MeshData]: return self._bounding_box_mesh ## (re)Calculate the bounding box mesh. def calculateBoundingBoxMesh(self): aabb = self.getBoundingBox() if aabb: bounding_box_mesh = MeshBuilder() rtf = aabb.maximum lbb = aabb.minimum bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh = bounding_box_mesh.build() ## Handler for the ParentChanged signal # \param node Node from which this event was triggered. def _onParentChanged(self, node: Optional["SceneNode"]): for child in self.getChildren(): child.parentChanged.emit(self) ## Signal for when a \type{SceneNodeDecorator} is added / removed. decoratorsChanged = Signal() ## Add a SceneNodeDecorator to this SceneNode. # \param \type{SceneNodeDecorator} decorator The decorator to add. def addDecorator(self, decorator: SceneNodeDecorator): if type(decorator) in [type(dec) for dec in self._decorators]: Logger.log("w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator)) return try: decorator.setNode(self) except AttributeError: Logger.logException("e", "Unable to add decorator.") return self._decorators.append(decorator) self.decoratorsChanged.emit(self) ## Get all SceneNodeDecorators that decorate this SceneNode. # \return list of all SceneNodeDecorators. def getDecorators(self) -> List[SceneNodeDecorator]: return self._decorators ## Get SceneNodeDecorators by type. # \param dec_type type of decorator to return. def getDecorator(self, dec_type) -> Optional[SceneNodeDecorator]: for decorator in self._decorators: if type(decorator) == dec_type: return decorator ## Remove all decorators def removeDecorators(self): for decorator in self._decorators: decorator.clear() self._decorators = [] self.decoratorsChanged.emit(self) ## Remove decorator by type. # \param dec_type type of the decorator to remove. def removeDecorator(self, dec_type: SceneNodeDecorator): for decorator in self._decorators: if type(decorator) == dec_type: decorator.clear() self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break ## Call a decoration of this SceneNode. # SceneNodeDecorators add Decorations, which are callable functions. # \param \type{string} function The function to be called. # \param *args # \param **kwargs def callDecoration(self, function: str, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None ## Does this SceneNode have a certain Decoration (as defined by a Decorator) # \param \type{string} function the function to check for. def hasDecoration(self, function: str) -> bool: for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self) -> str: return self._name def setName(self, name: str): self._name = name ## How many nodes is this node removed from the root? # \return |tupe{int} Steps from root (0 means it -is- the root). def getDepth(self) -> int: if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node: Optional["SceneNode"]): if self._parent: self._parent.removeChild(self) if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide whether to render or not. def isVisible(self) -> bool: if self._parent is not None and self._visible: return self._parent.isVisible() else: return self._visible ## Set the visibility of this SceneNode. def setVisible(self, visible: bool): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self) -> Optional[MeshData]: return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self) -> Optional[MeshData]: if self._mesh_data: return self._mesh_data.getTransformed(self.getWorldTransformation()) return self._mesh_data ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data: Optional[MeshData]): self._mesh_data = mesh_data self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node: "SceneNode"): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child: "SceneNode"): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self._resetAABB() self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self) -> List["SceneNode"]: return self._children def hasChildren(self) -> bool: return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self) -> List["SceneNode"]: children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self) -> Matrix: if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self) -> Matrix: if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) def setTransformation(self, transformation: Matrix): self._transformation = deepcopy(transformation) # Make a copy to ensure we never change the given transformation self._transformChanged() ## Get the local orientation value. def getOrientation(self) -> Quaternion: return deepcopy(self._orientation) def getWorldOrientation(self) -> Quaternion: return deepcopy(self._derived_orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * (self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale = self._scale, angles = euler_angles, translate = self._position, shear = self._shear) self._transformation = new_transform_matrix self._transformChanged() ## Get the local scaling value. def getScale(self) -> Vector: return self._scale def getWorldScale(self) -> Vector: return self._derived_scale ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale: Vector, transform_space: int = TransformSpace.Local): if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setScale(self, scale: Vector, transform_space: int = TransformSpace.Local): if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) ## Get the local position. def getPosition(self) -> Vector: return self._position ## Get the position of this scene node relative to the world. def getWorldPosition(self) -> Vector: return self._derived_position ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation: Vector, transform_space: int = TransformSpace.Local): if not self._enabled: return translation_matrix = Matrix() translation_matrix.setByTranslation(translation) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.World: world_transformation = deepcopy(self._world_transformation) self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(world_transformation) self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setPosition(self, position: Vector, transform_space: int = TransformSpace.Local): if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._derived_position, SceneNode.TransformSpace.World) ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) self.setOrientation(Quaternion.fromMatrix(m)) ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer) -> bool: return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self) -> bool: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable: bool): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self) -> bool: return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select: bool): self._selectable = select ## Get the bounding box of this node and its children. def getBoundingBox(self) -> Optional[AxisAlignedBox]: if not self._calculate_aabb: return None if self._aabb is None: self._calculateAABB() return self._aabb ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate: bool): self._calculate_aabb = calculate boundingBoxChanged = Signal() def getShear(self) -> Vector: return self._shear ## private: def _transformChanged(self): self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale, shear, euler_angles, translation, mirror = self._transformation.decompose() self._position = translation self._scale = scale self._shear = shear self._mirror = mirror orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation().multiply(self._transformation, copy = True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose() self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix) def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self.getParent(): self.getParent()._resetAABB() self.boundingBoxChanged.emit() def _calculateAABB(self): aabb = None if self._mesh_data: aabb = self._mesh_data.getExtents(self.getWorldTransformation()) else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) position = self.getWorldPosition() aabb = AxisAlignedBox(minimum = position, maximum = position) for child in self._children: if aabb is None: aabb = child.getBoundingBox() else: aabb = aabb + child.getBoundingBox() self._aabb = aabb
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
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent = None, name = ""): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._mirror = Vector(1.0, 1.0, 1.0) self._orientation = Quaternion() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = True self._name = name self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy def setCenterPosition(self, center): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m) self._mesh_data.setCenterPosition(center) for child in self._children: child.setCenterPosition(center) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getBoundingBoxMesh(self): return self._bounding_box_mesh def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB() def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self): return self._decorators def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) #self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): #transformed_mesh = deepcopy(self._mesh_data) #transformed_mesh.transform(self.getWorldTransformation()) return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._orientation = self._orientation * rotation elif transform_space == SceneNode.TransformSpace.Parent: self._orientation = rotation * self._orientation elif transform_space == SceneNode.TransformSpace.World: self._orientation = self._orientation * self._getDerivedOrientation().getInverse() * rotation * self._getDerivedOrientation() else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._orientation.normalize() self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. def setOrientation(self, orientation): if not self._enabled or orientation == self._orientation: return self._orientation = orientation self._orientation.normalize() self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._scale = self._scale.scale(scale) elif transform_space == SceneNode.TransformSpace.Parent: raise NotImplementedError() elif transform_space == SceneNode.TransformSpace.World: if self._parent: scale_change = Vector(1,1,1) - scale if scale_change.x < 0 or scale_change.y < 0 or scale_change.z < 0: direction = -1 else: direction = 1 # Hackish way to do this, but this seems to correctly scale the object. change_vector = self._scale.scale(self._getDerivedOrientation().getInverse().rotate(scale_change)) if change_vector.x < 0 and direction == 1: change_vector.setX(-change_vector.x) if change_vector.x > 0 and direction == -1: change_vector.setX(-change_vector.x) if change_vector.y < 0 and direction == 1: change_vector.setY(-change_vector.y) if change_vector.y > 0 and direction == -1: change_vector.setY(-change_vector.y) if change_vector.z < 0 and direction == 1: change_vector.setZ(-change_vector.z) if change_vector.z > 0 and direction == -1: change_vector.setZ(-change_vector.z) self._scale -= self._scale.scale(change_vector) else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. def setScale(self, scale): if not self._enabled or scale == self._scale: return self._scale = scale self._transformChanged() ## Get the local mirror values. # # \return The mirror values as vector of scaling values. def getMirror(self): return deepcopy(self._mirror) ## Mirror the scene object (and thus its children) in the given directions. # # \param mirror \type{Vector} A vector of three scale values that is used # to mirror the node. # \param transform_space The space relative to which to scale. Can be any # one of the constants in SceneNode::TransformSpace. def mirror(self, mirror, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._mirror *= mirror elif transform_space == SceneNode.TransformSpace.Parent: self._mirror *= mirror elif transform_space == SceneNode.TransformSpace.World: self._mirror *= mirror else: raise ValueError("Unknown transform space {0}".format(transform_space)) self._transformChanged() ## Set the local mirror values. # # \param mirror \type{Vector} The new mirror values as scale multipliers. def setMirror(self, mirror): if not self._enabled or mirror == self._mirror: return self._mirror = mirror self._transformChanged() ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): if not self._derived_position: self._updateTransformation() return deepcopy(self._derived_position) ## Translate the scene object (and thus its children) by given amount. # # \param translation \type{Vector} The amount to translate by. # \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. def translate(self, translation, transform_space = TransformSpace.Local): if not self._enabled: return if transform_space == SceneNode.TransformSpace.Local: self._position += self._orientation.rotate(translation) elif transform_space == SceneNode.TransformSpace.Parent: self._position += translation elif transform_space == SceneNode.TransformSpace.World: if self._parent: self._position += (1.0 / self._parent._getDerivedScale()).scale(self._parent._getDerivedOrientation().getInverse().rotate(translation)) else: self._position += translation self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. def setPosition(self, position): if not self._enabled or position == self._position: return self._position = position self._transformChanged() ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) if self._parent: self._orientation = self._parent._getDerivedOrientation() * Quaternion.fromMatrix(m) else: self._orientation = Quaternion.fromMatrix(m) self._transformChanged() ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _getDerivedPosition(self): if not self._derived_position: self._updateTransformation() return self._derived_position def _getDerivedOrientation(self): if not self._derived_orientation: self._updateTransformation() # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() return self._derived_orientation def _getDerivedScale(self): if not self._derived_scale: self._updateTransformation() return self._derived_scale def _transformChanged(self): self._resetAABB() self._transformation = None self._world_transformation = None self._derived_position = None self._derived_orientation = None self._derived_scale = None self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale_and_mirror = self._scale * self._mirror self._transformation = Matrix.fromPositionOrientationScale(self._position, self._orientation, scale_and_mirror) if self._parent: parent_orientation = self._parent._getDerivedOrientation() if self._inherit_orientation: self._derived_orientation = parent_orientation * self._orientation else: self._derived_orientation = self._orientation # Sometimes the derived orientation can be None. # I've not been able to locate the cause of this, but this prevents it being an issue. if not self._derived_orientation: self._derived_orientation = Quaternion() parent_scale = self._parent._getDerivedScale() if self._inherit_scale: self._derived_scale = parent_scale.scale(scale_and_mirror) else: self._derived_scale = scale_and_mirror self._derived_position = parent_orientation.rotate(parent_scale.scale(self._position)) self._derived_position += self._parent._getDerivedPosition() self._world_transformation = Matrix.fromPositionOrientationScale(self._derived_position, self._derived_orientation, self._derived_scale) else: self._derived_position = self._position self._derived_orientation = self._orientation self._derived_scale = scale_and_mirror self._world_transformation = self._transformation def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
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) 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