class_name Cel3DObject extends Spatial signal property_changed enum Type { BOX, SPHERE, CAPSULE, CYLINDER, PRISM, TORUS, PLANE, TEXT, DIR_LIGHT, SPOT_LIGHT, OMNI_LIGHT, IMPORTED } enum Gizmos { NONE, X_POS, Y_POS, Z_POS, X_ROT, Y_ROT, Z_ROT, X_SCALE, Y_SCALE, Z_SCALE } var cel var id := -1 var type: int = Type.BOX setget _set_type var selected := false var hovered := false var box_shape: BoxShape var camera: Camera var file_path := "" setget _set_file_path # Only useful for Type.IMPORTED var applying_gizmos: int = Gizmos.NONE var node3d_type: VisualInstance var dir_light_texture := preload("res://assets/graphics/gizmos/directional_light.svg") var spot_light_texture := preload("res://assets/graphics/gizmos/spot_light.svg") var omni_light_texture := preload("res://assets/graphics/gizmos/omni_light.svg") onready var gizmos_3d: Node2D = Global.canvas.gizmos_3d func _ready() -> void: camera = get_viewport().get_camera() var static_body := StaticBody.new() var collision_shape := CollisionShape.new() box_shape = BoxShape.new() box_shape.extents = scale collision_shape.shape = box_shape static_body.add_child(collision_shape) add_child(static_body) func find_cel() -> bool: var project = Global.current_project return cel == project.frames[project.current_frame].cels[project.current_layer] func serialize() -> Dictionary: var dict := { "id": id, "type": type, "transform": transform, "visible": visible, "file_path": file_path } if _is_mesh(): var mesh: Mesh = node3d_type.mesh match type: Type.BOX: dict["mesh_size"] = mesh.size Type.PLANE: dict["mesh_sizev2"] = mesh.size dict["mesh_center_offset"] = mesh.center_offset Type.PRISM: dict["mesh_size"] = mesh.size dict["mesh_left_to_right"] = mesh.left_to_right Type.SPHERE: dict["mesh_radius"] = mesh.radius dict["mesh_height"] = mesh.height dict["mesh_radial_segments"] = mesh.radial_segments dict["mesh_rings"] = mesh.rings dict["mesh_is_hemisphere"] = mesh.is_hemisphere Type.CAPSULE: dict["mesh_radius"] = mesh.radius dict["mesh_mid_height"] = mesh.mid_height dict["mesh_radial_segments"] = mesh.radial_segments dict["mesh_rings"] = mesh.rings Type.CYLINDER: dict["mesh_bottom_radius"] = mesh.bottom_radius dict["mesh_top_radius"] = mesh.top_radius dict["mesh_height"] = mesh.height dict["mesh_radial_segments"] = mesh.radial_segments dict["mesh_rings"] = mesh.rings Type.TEXT: dict["mesh_text"] = mesh.text dict["mesh_pixel_size"] = mesh.pixel_size dict["mesh_curve_step"] = mesh.curve_step dict["mesh_horizontal_alignment"] = mesh.horizontal_alignment else: dict["light_color"] = node3d_type.light_color dict["light_energy"] = node3d_type.light_energy dict["light_negative"] = node3d_type.light_negative dict["shadow_enabled"] = node3d_type.shadow_enabled dict["shadow_color"] = node3d_type.shadow_color match type: Type.OMNI_LIGHT: dict["omni_range"] = node3d_type.omni_range Type.SPOT_LIGHT: dict["spot_range"] = node3d_type.spot_range dict["spot_angle"] = node3d_type.spot_angle return dict func deserialize(dict: Dictionary) -> void: id = dict["id"] self.type = dict["type"] transform = dict["transform"] visible = dict["visible"] self.file_path = dict["file_path"] if _is_mesh(): var mesh: Mesh = node3d_type.mesh match type: Type.BOX: mesh.size = dict["mesh_size"] Type.PLANE: mesh.size = dict["mesh_sizev2"] mesh.center_offset = dict["mesh_center_offset"] Type.PRISM: mesh.size = dict["mesh_size"] mesh.left_to_right = dict["mesh_left_to_right"] Type.SPHERE: mesh.radius = dict["mesh_radius"] mesh.height = dict["mesh_height"] mesh.radial_segments = dict["mesh_radial_segments"] mesh.rings = dict["mesh_rings"] mesh.is_hemisphere = dict["mesh_is_hemisphere"] Type.CAPSULE: mesh.radius = dict["mesh_radius"] mesh.mid_height = dict["mesh_mid_height"] mesh.radial_segments = dict["mesh_radial_segments"] mesh.rings = dict["mesh_rings"] Type.CYLINDER: mesh.bottom_radius = dict["mesh_bottom_radius"] mesh.top_radius = dict["mesh_top_radius"] mesh.height = dict["mesh_height"] mesh.radial_segments = dict["mesh_radial_segments"] mesh.rings = dict["mesh_rings"] Type.TEXT: mesh.text = dict["mesh_text"] mesh.pixel_size = dict["mesh_pixel_size"] mesh.curve_step = dict["mesh_curve_step"] mesh.horizontal_alignment = dict["mesh_horizontal_alignment"] else: node3d_type.light_color = dict["light_color"] node3d_type.light_energy = dict["light_energy"] node3d_type.light_negative = dict["light_negative"] node3d_type.shadow_enabled = dict["shadow_enabled"] node3d_type.shadow_color = dict["shadow_color"] match type: Type.OMNI_LIGHT: node3d_type.omni_range = dict["omni_range"] Type.SPOT_LIGHT: node3d_type.spot_range = dict["spot_range"] node3d_type.spot_angle = dict["spot_angle"] change_property() func _is_mesh() -> bool: return node3d_type is MeshInstance func _set_type(value: int) -> void: type = value if is_instance_valid(node3d_type): node3d_type.queue_free() match type: Type.BOX: node3d_type = MeshInstance.new() node3d_type.mesh = CubeMesh.new() Type.SPHERE: node3d_type = MeshInstance.new() node3d_type.mesh = SphereMesh.new() Type.CAPSULE: node3d_type = MeshInstance.new() node3d_type.mesh = CapsuleMesh.new() Type.CYLINDER: node3d_type = MeshInstance.new() node3d_type.mesh = CylinderMesh.new() Type.PRISM: node3d_type = MeshInstance.new() node3d_type.mesh = PrismMesh.new() Type.PLANE: node3d_type = MeshInstance.new() node3d_type.mesh = PlaneMesh.new() Type.TEXT: node3d_type = MeshInstance.new() var mesh := TextMesh.new() mesh.font = Global.control.theme.default_font mesh.text = "Sample" node3d_type.mesh = mesh Type.DIR_LIGHT: node3d_type = DirectionalLight.new() gizmos_3d.add_always_visible(self, dir_light_texture) Type.SPOT_LIGHT: node3d_type = SpotLight.new() gizmos_3d.add_always_visible(self, spot_light_texture) Type.OMNI_LIGHT: node3d_type = OmniLight.new() gizmos_3d.add_always_visible(self, omni_light_texture) Type.IMPORTED: node3d_type = MeshInstance.new() var mesh: Mesh if not file_path.empty(): mesh = ObjParse.load_obj(file_path) node3d_type.mesh = mesh add_child(node3d_type) func _set_file_path(value: String) -> void: file_path = value if file_path.empty(): return if type == Type.IMPORTED: node3d_type.mesh = ObjParse.load_obj(file_path) func _notification(what: int) -> void: if what == NOTIFICATION_EXIT_TREE: unselect() gizmos_3d.remove_always_visible(self) func select() -> void: selected = true gizmos_3d.get_points(camera, self) func unselect() -> void: selected = false gizmos_3d.clear_points(self) func hover() -> void: if hovered: return hovered = true if selected: return gizmos_3d.get_points(camera, self) func unhover() -> void: if not hovered: return hovered = false if selected: return gizmos_3d.clear_points(self) func change_transform(a: Vector3, b: Vector3) -> void: var diff := a - b match applying_gizmos: Gizmos.X_POS: move_axis(diff, transform.basis.x) Gizmos.Y_POS: move_axis(diff, transform.basis.y) Gizmos.Z_POS: move_axis(diff, transform.basis.z) Gizmos.X_ROT: change_rotation(a, b, transform.basis.x) Gizmos.Y_ROT: change_rotation(a, b, transform.basis.y) Gizmos.Z_ROT: change_rotation(a, b, transform.basis.z) Gizmos.X_SCALE: change_scale(diff, transform.basis.x, Vector3.RIGHT) Gizmos.Y_SCALE: change_scale(diff, transform.basis.y, Vector3.UP) Gizmos.Z_SCALE: change_scale(diff, transform.basis.z, Vector3.BACK) _: move(diff) func move(position: Vector3) -> void: translation += position change_property() func move_axis(diff: Vector3, axis: Vector3) -> void: # Move the object in the direction it is facing, and restrict mouse movement in that axis var axis_v2 := Vector2(axis.x, axis.y).normalized() if axis_v2 == Vector2.ZERO: axis_v2 = Vector2(axis.y, axis.z).normalized() var diff_v2 := Vector2(diff.x, diff.y).normalized() translation += axis * axis_v2.dot(diff_v2) * diff.length() change_property() func change_rotation(a: Vector3, b: Vector3, axis: Vector3) -> void: var a_local := a - translation var a_local_v2 := Vector2(a_local.x, a_local.y) var b_local := b - translation var b_local_v2 := Vector2(b_local.x, b_local.y) var angle := b_local_v2.angle_to(a_local_v2) # Rotate the object around a basis axis, instead of a fixed axis, such as # Vector3.RIGHT, Vector3.UP or Vector3.BACK rotate(axis.normalized(), angle) rotation.x = wrapf(rotation.x, -PI, PI) rotation.y = wrapf(rotation.y, -PI, PI) rotation.z = wrapf(rotation.z, -PI, PI) change_property() func change_scale(diff: Vector3, axis: Vector3, dir: Vector3) -> void: # Scale the object in the direction it is facing, and restrict mouse movement in that axis var axis_v2 := Vector2(axis.x, axis.y).normalized() if axis_v2 == Vector2.ZERO: axis_v2 = Vector2(axis.y, axis.z).normalized() var diff_v2 := Vector2(diff.x, diff.y).normalized() scale += dir * axis_v2.dot(diff_v2) * diff.length() change_property() func change_property() -> void: if selected: select() else: # Check is needed in case this runs before _ready(), and thus onready variables if is_instance_valid(gizmos_3d): gizmos_3d.update() emit_signal("property_changed")