@tool class_name ShorelineRibbonBaker extends Node @export var water_bodies: Array[ShorelineRibbonConfig] = [] @export_enum("Off", "Raw", "Simplified", "Smoothed") var debug_path_stage := 0 @export_tool_button("Rebuild Shoreline Ribbons") var rebuild_shorelines: Callable = rebuild_all func rebuild_all() -> Array[Dictionary]: var results: Array[Dictionary] = [] for configuration: ShorelineRibbonConfig in water_bodies: var result := _rebuild(configuration) if result.is_empty(): return [] results.append(result) _update_debug_display(results) return results func _rebuild(configuration: ShorelineRibbonConfig) -> Dictionary: if configuration == null: push_error("Shoreline ribbon configuration is missing.") return {} if configuration.water_type != WaterType.Type.SALT_WATER: return { "skipped": true, "water_type": configuration.water_type, "output_path": configuration.output_resource_path, } var source := get_node_or_null(configuration.terrain_source) as Node3D if source == null: push_error( "Shoreline terrain source was not found: %s" % configuration.terrain_source ) return {} if configuration.output_resource_path.is_empty(): push_error("Shoreline output resource path is empty.") return {} var faces := _terrain_faces(source) if faces.is_empty(): push_error("Configured shoreline terrain source exposes no triangles.") return {} var result := ShorelineRibbonGenerator.generate( faces, configuration.water_height, configuration.generation_bounds, configuration.water_reference, configuration.water_is_inside, configuration.simplification_tolerance, configuration.smoothing_iterations, configuration.resample_spacing, configuration.corner_rounding_distance, ) var output_directory := configuration.output_resource_path.get_base_dir() DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_directory)) var save_error := ResourceSaver.save( result["mesh"], configuration.output_resource_path ) if save_error != OK: push_error( "Failed to save %s: %s" % [configuration.output_resource_path, error_string(save_error)] ) return {} result["output_path"] = configuration.output_resource_path result["water_height"] = configuration.water_height result["water_type"] = configuration.water_type result["skipped"] = false return result func _update_debug_display(results: Array[Dictionary]) -> void: var previous := get_node_or_null("_ShorelinePathDebug") if previous != null: previous.queue_free() if not Engine.is_editor_hint() or debug_path_stage == 0: return var debug_mesh := ImmediateMesh.new() var debug_material := StandardMaterial3D.new() debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED debug_material.albedo_color = Color(1.0, 0.35, 0.65, 1.0) var stage_key: String = [ "", "debug_raw_paths", "debug_simplified_paths", "debug_smoothed_paths" ][debug_path_stage] for result: Dictionary in results: if result.get("skipped", false): continue var height := float(result["water_height"]) + 0.06 for path_data: Dictionary in result[stage_key]: var points: PackedVector2Array = path_data["points"] if points.size() < 2: continue debug_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP, debug_material) for point: Vector2 in points: debug_mesh.surface_add_vertex(Vector3(point.x, height, point.y)) if path_data["closed"]: debug_mesh.surface_add_vertex(Vector3(points[0].x, height, points[0].y)) debug_mesh.surface_end() var debug_instance := MeshInstance3D.new() debug_instance.name = "_ShorelinePathDebug" debug_instance.mesh = debug_mesh add_child(debug_instance) func _terrain_faces(source: Node3D) -> PackedVector3Array: var result := PackedVector3Array() if source is CollisionShape3D: var collision_shape := source as CollisionShape3D var concave_shape := collision_shape.shape as ConcavePolygonShape3D if concave_shape == null: return result for vertex: Vector3 in concave_shape.get_faces(): result.append(_to_map_space(source, vertex)) return result if source is MeshInstance3D: var mesh_instance := source as MeshInstance3D if mesh_instance.mesh == null: return result for surface: int in mesh_instance.mesh.get_surface_count(): var arrays := mesh_instance.mesh.surface_get_arrays(surface) var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array if indices.is_empty(): for vertex: Vector3 in vertices: result.append(_to_map_space(source, vertex)) else: for index: int in indices: result.append(_to_map_space(source, vertices[index])) return result func _to_map_space(source: Node3D, vertex: Vector3) -> Vector3: var map_root := get_parent() as Node3D if map_root == null: return source.to_global(vertex) return map_root.to_local(source.to_global(vertex))