# Copyright © 2022 Kasper Arnklit Frandsen - MIT License # See `LICENSE.md` included in the source distribution for details. extends EditorNode3DGizmoPlugin const RiverManager = preload("./river_manager.gd") const RiverControls = preload("./gui/river_controls.gd") const HANDLES_PER_POINT = 5 const AXIS_CONSTRAINT_LENGTH = 4096 const AXIS_MAPPING := { RiverControls.CONSTRAINTS.AXIS_X: Vector3.RIGHT, RiverControls.CONSTRAINTS.AXIS_Y: Vector3.UP, RiverControls.CONSTRAINTS.AXIS_Z: Vector3.BACK } const PLANE_MAPPING := { RiverControls.CONSTRAINTS.PLANE_YZ: Vector3.RIGHT, RiverControls.CONSTRAINTS.PLANE_XZ: Vector3.UP, RiverControls.CONSTRAINTS.PLANE_XY: Vector3.BACK } var editor_plugin : EditorPlugin var _path_mat var _handle_lines_mat var _handle_base_transform # Ensure that the width handle can't end up inside the center handle # as then it is hard to separate them again. const MIN_DIST_TO_CENTER_HANDLE = 0.02 func _init() -> void: # Two materials for every handle type. # 1) Transparent handle that is always shown. # 2) Opaque handle that is only shown above terrain (when passing depth test) # Note that this impacts the point index of the handles. See table below. create_handle_material("handles_center") create_handle_material("handles_control_points") create_handle_material("handles_width") create_handle_material("handles_center_with_depth") create_handle_material("handles_control_points_with_depth") create_handle_material("handles_width_with_depth") var handles_center_mat = get_material("handles_center") var handles_center_mat_wd = get_material("handles_center_with_depth") var handles_control_points_mat = get_material("handles_control_points") var handles_control_points_mat_wd = get_material("handles_control_points_with_depth") var handles_width_mat = get_material("handles_width") var handles_width_mat_wd = get_material("handles_width_with_depth") handles_center_mat.set_albedo( Color(1.0, 1.0, 0.0, 0.25)) handles_center_mat_wd.set_albedo( Color(1.0, 1.0, 0.0, 1.0)) handles_control_points_mat.set_albedo( Color(1.0, 0.5, 0.0, 0.25)) handles_control_points_mat_wd.set_albedo(Color(1.0, 0.5, 0.0, 1.0)) handles_width_mat.set_albedo( Color(0.0, 1.0, 1.0, 0.25)) handles_width_mat_wd.set_albedo( Color(0.0, 1.0, 1.0, 1.0)) handles_center_mat.set_flag( StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, true) handles_center_mat_wd.set_flag( StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, false) handles_control_points_mat.set_flag( StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, true) handles_control_points_mat_wd.set_flag(StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, false) handles_width_mat.set_flag( StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, true) handles_width_mat_wd.set_flag( StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, false) var mat = StandardMaterial3D.new() mat.shading_mode = StandardMaterial3D.SHADING_MODE_UNSHADED mat.set_flag(StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, true) mat.set_albedo(Color(1.0, 1.0, 0.0)) mat.render_priority = 10 add_material("path", mat) add_material("handle_lines", mat) func _get_gizmo_name(): return "Waterways" func reset() -> void: _handle_base_transform = null func get_name() -> String: return "RiverInput" func _has_gizmo(spatial) -> bool: return spatial is RiverManager # TODO - figure out of this new "secondary" bool should be used func _get_handle_name(gizmo: EditorNode3DGizmo, index: int, secondary: bool) -> String: return "Handle " + str(index) # Handles are pushed to separate handle lists, one per material (using gizmo.add_handles). # A handle's "index" is given (by Godot) in order it was added to a gizmo. # Given that N = points in the curve: # - First we add the center ("actual") curve handles, therefore # the handle's index is the same as the curve point's index. # - Then we add the in and out points together. So the first curve point's IN handle # gets an index of N. The OUT handle gets N+1. # - Finally the left/right indices come last, and the first curve point's LEFT is N * 3 . # (3 because there are three rows before the left/right indices) # # Examples for N = 2, 3, 4: # curve points 2:0 1 3:0 1 2 4:0 1 2 3 # ------------------------------------------------------------------ # center 0 1 0 1 2 0 1 2 3 # in 2 4 3 5 7 4 6 8 10 # out 3 5 4 6 8 5 7 9 11 # left 6 8 9 11 13 12 14 16 18 # right 7 9 10 12 14 13 15 17 19 # # The following utility functions calculate to and from curve/handle indices. func _is_center_point(index: int, river_curve_point_count: int): var res = index < river_curve_point_count return res func _is_control_point_in(index: int, river_curve_point_count: int): if index < river_curve_point_count: return false if index >= river_curve_point_count * 3: return false var res = (index - river_curve_point_count) % 2 == 0 return res func _is_control_point_out(index: int, river_curve_point_count: int): if index < river_curve_point_count: return false if index >= river_curve_point_count * 3: return false var res = (index - river_curve_point_count) % 2 == 1 return res func _is_width_point_left(index: int, river_curve_point_count: int): if index < river_curve_point_count * 3: return false var res = (index - river_curve_point_count * 3) % 2 == 0 return res func _is_width_point_right(index: int, river_curve_point_count: int): if index < river_curve_point_count * 3: return false var res = (index - river_curve_point_count * 3) % 2 == 1 return res func _get_curve_index(index: int, point_count: int): if _is_center_point(index, point_count): return index if _is_control_point_in(index, point_count): return (index - point_count) / 2 if _is_control_point_out(index, point_count): return (index - point_count - 1) / 2 if _is_width_point_left(index, point_count) or _is_width_point_right(index, point_count): return (index - point_count * 3) / 2 func _get_point_index(curve_index: int, is_center: bool, is_cp_in: bool, is_cp_out: bool, is_width_left: bool, is_width_right: bool, point_count: int): if is_center: return curve_index if is_cp_in: return point_count + curve_index * 2 if is_cp_out: return point_count + 1 + curve_index * 2 if is_width_left: return point_count * 3 + curve_index * 2 if is_width_right: return point_count * 3 + 1 + curve_index * 2 # TODO - figure out of this new "secondary" bool should be used func _get_handle_value(gizmo: EditorNode3DGizmo, index: int, secondary: bool): var river : RiverManager = gizmo.get_node_3d() var point_count = river.curve.get_point_count() if _is_center_point(index, point_count): return river.curve.get_point_position(_get_curve_index(index, point_count)) if _is_control_point_in(index, point_count): return river.curve.get_point_in(_get_curve_index(index, point_count)) if _is_control_point_out(index, point_count): return river.curve.get_point_out(_get_curve_index(index, point_count)) if _is_width_point_left(index, point_count) or _is_width_point_right(index, point_count): return river.widths[_get_curve_index(index, point_count)] # Called when handle is moved # TODO - figure out of this new "secondary" bool should be used func _set_handle(gizmo: EditorNode3DGizmo, index: int, secondary: bool, camera: Camera3D, point: Vector2) -> void: var river : RiverManager = gizmo.get_node_3d() var space_state = river.get_world_3d().direct_space_state var global_transform : Transform3D = river.transform if river.is_inside_tree(): global_transform = river.get_global_transform() var global_inverse: Transform3D = global_transform.affine_inverse() var ray_from = camera.project_ray_origin(point) var ray_dir = camera.project_ray_normal(point) var old_pos : Vector3 var point_count = river.curve.get_point_count() var p_index = _get_curve_index(index, point_count) var base = river.curve.get_point_position(p_index) # Logic to move handles var is_center = _is_center_point(index, point_count) var is_cp_in = _is_control_point_in(index, point_count) var is_cp_out = _is_control_point_out(index, point_count) var is_width_left = _is_width_point_left(index, point_count) var is_width_right = _is_width_point_right(index, point_count) if is_center: old_pos = base if is_cp_in: old_pos = river.curve.get_point_in(p_index) + base if is_cp_out: old_pos = river.curve.get_point_out(p_index) + base if is_width_left: old_pos = base + river.curve.get_point_out(p_index).cross(Vector3.UP).normalized() * river.widths[p_index] if is_width_right: old_pos = base + river.curve.get_point_out(p_index).cross(Vector3.DOWN).normalized() * river.widths[p_index] var old_pos_global : Vector3 = river.to_global(old_pos) #print("_handle_base_transform is: ", _handle_base_transform) if _handle_base_transform == null: # This is the first set_handle() call since the last reset so we # use the current handle position as our _handle_base_transform var z := river.curve.get_point_out(p_index).normalized() var x := z.cross(Vector3.DOWN).normalized() var y := z.cross(x).normalized() _handle_base_transform = Transform3D( Basis(x, y, z) * global_transform.basis, old_pos_global ) # Point, in and out handles if is_center or is_cp_in or is_cp_out: var new_pos if editor_plugin.constraint == RiverControls.CONSTRAINTS.COLLIDERS: # TODO - make in / out handles snap to a plane based on the normal of # the raycast hit instead. var ray_params: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.new() ray_params.from = ray_from ray_params.to = ray_from + ray_dir * 4096 var result = space_state.intersect_ray(ray_params) if result: new_pos = result.position elif editor_plugin.constraint == RiverControls.CONSTRAINTS.NONE: var plane = Plane(old_pos_global, old_pos_global + camera.transform.basis.x, old_pos_global + camera.transform.basis.y) new_pos = plane.intersects_ray(ray_from, ray_dir) elif editor_plugin.constraint in AXIS_MAPPING: var axis: Vector3 = AXIS_MAPPING[editor_plugin.constraint] if editor_plugin.local_editing: axis = _handle_base_transform.basis * (axis) var axis_from = old_pos_global + (axis * AXIS_CONSTRAINT_LENGTH) var axis_to = old_pos_global - (axis * AXIS_CONSTRAINT_LENGTH) var ray_to = ray_from + (ray_dir * AXIS_CONSTRAINT_LENGTH) var result = Geometry3D.get_closest_points_between_segments(axis_from, axis_to, ray_from, ray_to) new_pos = result[0] elif editor_plugin.constraint in PLANE_MAPPING: var normal: Vector3 = PLANE_MAPPING[editor_plugin.constraint] if editor_plugin.local_editing: normal = _handle_base_transform.basis * (normal) var projected : Vector3 = old_pos_global.project(normal) var direction : Vector3 = sign(projected.dot(normal)) var distance : Vector3 = direction * projected.length() var plane := Plane(normal, distance) new_pos = plane.intersects_ray(ray_from, ray_dir) # Discard if no valid position was found if new_pos == null: return # TODO: implement rounding when control is pressed. # How do we round when in local axis/plane mode? var new_pos_local = river.to_local(new_pos) if is_center: river.set_curve_point_position(p_index, new_pos_local) if is_cp_in: river.set_curve_point_in(p_index, new_pos_local - base) river.set_curve_point_out(p_index, -(new_pos_local - base)) if is_cp_out: river.set_curve_point_out(p_index, new_pos_local - base) river.set_curve_point_in(p_index, -(new_pos_local - base)) # Widths handles if is_width_left or is_width_right: var p1 = base var p2 if is_width_left: p2 = river.curve.get_point_out(p_index).cross(Vector3.UP).normalized() * 4096 if is_width_right: p2 = river.curve.get_point_out(p_index).cross(Vector3.DOWN).normalized() * 4096 var g1 = global_inverse * (ray_from) var g2 = global_inverse * (ray_from + ray_dir * 4096) var geo_points = Geometry3D.get_closest_points_between_segments(p1, p2, g1, g2) var dir = geo_points[0].distance_to(base) - old_pos.distance_to(base) river.widths[p_index] += dir # Ensure width handles don't end up inside the center point river.widths[p_index] = max(river.widths[p_index], MIN_DIST_TO_CENTER_HANDLE) _redraw(gizmo) # Handle Undo / Redo of handle movements # TODO - figure out of this new "secondary" bool should be used func _commit_handle(gizmo: EditorNode3DGizmo, index: int, secondary: bool, restore, cancel: bool = false) -> void: var river : RiverManager = gizmo.get_node_3d() var point_count = river.curve.get_point_count() var ur = editor_plugin.get_undo_redo() ur.create_action("Change River Shape") var p_index = _get_curve_index(index, point_count) if _is_center_point(index, point_count): ur.add_do_method(river, "set_curve_point_position", p_index, river.curve.get_point_position(p_index)) ur.add_undo_method(river, "set_curve_point_position", p_index, restore) if _is_control_point_in(index, point_count): ur.add_do_method(river, "set_curve_point_in", p_index, river.curve.get_point_in(p_index)) ur.add_undo_method(river, "set_curve_point_in", p_index, restore) ur.add_do_method(river, "set_curve_point_out", p_index, river.curve.get_point_out(p_index)) ur.add_undo_method(river, "set_curve_point_out", p_index, -restore) if _is_control_point_out(index, point_count): ur.add_do_method(river, "set_curve_point_out", p_index, river.curve.get_point_out(p_index)) ur.add_undo_method(river, "set_curve_point_out", p_index, restore) ur.add_do_method(river, "set_curve_point_in", p_index, river.curve.get_point_in(p_index)) ur.add_undo_method(river, "set_curve_point_in", p_index, -restore) if _is_width_point_left(index, point_count) or _is_width_point_right(index, point_count): var river_widths_undo := river.widths.duplicate(true) river_widths_undo[p_index] = restore ur.add_do_property(river, "widths", river.widths) ur.add_undo_property(river, "widths", river_widths_undo) ur.add_do_method(river, "properties_changed") ur.add_do_method(river, "set_materials", "i_valid_flowmap", false) ur.add_do_property(river, "valid_flowmap", false) ur.add_do_method(river, "update_configuration_warnings") ur.add_undo_method(river, "properties_changed") ur.add_undo_method(river, "set_materials", "i_valid_flowmap", river.valid_flowmap) ur.add_undo_property(river, "valid_flowmap", river.valid_flowmap) ur.add_undo_method(river, "update_configuration_warnings") ur.commit_action() _redraw(gizmo) func _redraw(gizmo: EditorNode3DGizmo) -> void: # Work around for issue where using "get_material" doesn't return a # material when redraw is being called manually from _set_handle() # so I'm caching the materials instead if not _path_mat: _path_mat = get_material("path", gizmo) if not _handle_lines_mat: _handle_lines_mat = get_material("handle_lines", gizmo) gizmo.clear() var river := gizmo.get_node_3d() as RiverManager if not river.is_connected("river_changed", Callable(self, "_redraw")): river.river_changed.connect(_redraw.bind(gizmo)) _draw_path(gizmo, river.curve) _draw_handles(gizmo, river) func _draw_path(gizmo: EditorNode3DGizmo, curve : Curve3D) -> void: var path = PackedVector3Array() var baked_points = curve.get_baked_points() for i in baked_points.size() - 1: path.append(baked_points[i]) path.append(baked_points[i + 1]) gizmo.add_lines(path, _path_mat) func _draw_handles(gizmo: EditorNode3DGizmo, river : RiverManager) -> void: var lines = PackedVector3Array() var handles_center = PackedVector3Array() var handles_center_wd = PackedVector3Array() var handles_control_points = PackedVector3Array() var handles_control_points_wd = PackedVector3Array() var handles_width = PackedVector3Array() var handles_width_wd = PackedVector3Array() var point_count = river.curve.get_point_count() for i in point_count: var point_pos = river.curve.get_point_position(i) var point_pos_in = river.curve.get_point_in(i) + point_pos var point_pos_out = river.curve.get_point_out(i) + point_pos var point_width_pos_right = river.curve.get_point_position(i) + river.curve.get_point_out(i).cross(Vector3.UP).normalized() * river.widths[i] var point_width_pos_left = river.curve.get_point_position(i) + river.curve.get_point_out(i).cross(Vector3.DOWN).normalized() * river.widths[i] handles_center.push_back(point_pos) handles_control_points.push_back(point_pos_in) handles_control_points.push_back(point_pos_out) handles_width.push_back(point_width_pos_right) handles_width.push_back(point_width_pos_left) lines.push_back(point_pos) lines.push_back(point_pos_in) lines.push_back(point_pos) lines.push_back(point_pos_out) lines.push_back(point_pos) lines.push_back(point_width_pos_right) lines.push_back(point_pos) lines.push_back(point_width_pos_left) gizmo.add_lines(lines, _handle_lines_mat) # Add each handle twice, for both material types. # Needs to be grouped by material "type" since that's what influences the handle indices. gizmo.add_handles(handles_center, get_material("handles_center", gizmo), []) gizmo.add_handles(handles_control_points, get_material("handles_control_points", gizmo), []) gizmo.add_handles(handles_width, get_material("handles_width", gizmo), []) gizmo.add_handles(handles_center, get_material("handles_center_with_depth", gizmo), []) gizmo.add_handles(handles_control_points, get_material("handles_control_points_with_depth", gizmo), []) gizmo.add_handles(handles_width, get_material("handles_width_with_depth", gizmo), [])