diff options
81 files changed, 3191 insertions, 2481 deletions
diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index 3c0b544781..4a19d5bda0 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -185,7 +185,7 @@ License: MPL-2.0 Files: ./thirdparty/clipper2/ Comment: Clipper2 -Copyright: 2010-2013, Angus Johnson +Copyright: 2010-2023, Angus Johnson License: BSL-1.0 Files: ./thirdparty/cvtt/ diff --git a/core/io/resource.cpp b/core/io/resource.cpp index 7e8d0b43cd..5edb045760 100644 --- a/core/io/resource.cpp +++ b/core/io/resource.cpp @@ -130,6 +130,16 @@ String Resource::generate_scene_unique_id() { } void Resource::set_scene_unique_id(const String &p_id) { + bool is_valid = true; + for (int i = 0; i < p_id.length(); i++) { + if (!is_ascii_identifier_char(p_id[i])) { + is_valid = false; + scene_unique_id = Resource::generate_scene_unique_id(); + break; + } + } + + ERR_FAIL_COND_MSG(!is_valid, "The scene unique ID must contain only letters, numbers, and underscores."); scene_unique_id = p_id; } @@ -425,7 +435,7 @@ RID Resource::get_rid() const { #ifdef TOOLS_ENABLED -uint32_t Resource::hash_edited_version() const { +uint32_t Resource::hash_edited_version_for_preview() const { uint32_t hash = hash_murmur3_one_32(get_edited_version()); List<PropertyInfo> plist; @@ -435,7 +445,7 @@ uint32_t Resource::hash_edited_version() const { if (E.usage & PROPERTY_USAGE_STORAGE && E.type == Variant::OBJECT && E.hint == PROPERTY_HINT_RESOURCE_TYPE) { Ref<Resource> res = get(E.name); if (res.is_valid()) { - hash = hash_murmur3_one_32(res->hash_edited_version(), hash); + hash = hash_murmur3_one_32(res->hash_edited_version_for_preview(), hash); } } } @@ -532,6 +542,10 @@ void Resource::_bind_methods() { ClassDB::bind_method(D_METHOD("get_local_scene"), &Resource::get_local_scene); ClassDB::bind_method(D_METHOD("setup_local_to_scene"), &Resource::setup_local_to_scene); + ClassDB::bind_static_method("Resource", D_METHOD("generate_scene_unique_id"), &Resource::generate_scene_unique_id); + ClassDB::bind_method(D_METHOD("set_scene_unique_id", "id"), &Resource::set_scene_unique_id); + ClassDB::bind_method(D_METHOD("get_scene_unique_id"), &Resource::get_scene_unique_id); + ClassDB::bind_method(D_METHOD("emit_changed"), &Resource::emit_changed); ClassDB::bind_method(D_METHOD("duplicate", "subresources"), &Resource::duplicate, DEFVAL(false)); @@ -542,6 +556,7 @@ void Resource::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "resource_local_to_scene"), "set_local_to_scene", "is_local_to_scene"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "set_path", "get_path"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_name"), "set_name", "get_name"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "resource_scene_unique_id", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_scene_unique_id", "get_scene_unique_id"); MethodInfo get_rid_bind("_get_rid"); get_rid_bind.return_val.type = Variant::RID; diff --git a/core/io/resource.h b/core/io/resource.h index f0f686af57..cc8a0d4387 100644 --- a/core/io/resource.h +++ b/core/io/resource.h @@ -125,7 +125,7 @@ public: #ifdef TOOLS_ENABLED - uint32_t hash_edited_version() const; + virtual uint32_t hash_edited_version_for_preview() const; virtual void set_last_modified_time(uint64_t p_time) { last_modified_time = p_time; } uint64_t get_last_modified_time() const { return last_modified_time; } diff --git a/core/io/stream_peer_gzip.cpp b/core/io/stream_peer_gzip.cpp index 4daa71a22a..514bcf59b8 100644 --- a/core/io/stream_peer_gzip.cpp +++ b/core/io/stream_peer_gzip.cpp @@ -76,6 +76,7 @@ Error StreamPeerGZIP::start_decompression(bool p_is_deflate, int buffer_size) { Error StreamPeerGZIP::_start(bool p_compress, bool p_is_deflate, int buffer_size) { ERR_FAIL_COND_V(ctx != nullptr, ERR_ALREADY_IN_USE); + ERR_FAIL_COND_V_MSG(buffer_size <= 0, ERR_INVALID_PARAMETER, "Invalid buffer size. It should be a positive integer."); clear(); compressing = p_compress; rb.resize(nearest_shift(buffer_size - 1)); diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 923c22e871..1c69c48786 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -883,6 +883,9 @@ <member name="project_manager/default_renderer" type="String" setter="" getter=""> The renderer type that will be checked off by default when creating a new project. Accepted strings are "forward_plus", "mobile" or "gl_compatibility". </member> + <member name="project_manager/directory_naming_convention" type="int" setter="" getter=""> + Directory naming convention for the project manager. Options are "No convention" (project name is directory name), "kebab-case" (default), "snake_case", "camelCase", "PascalCase", or "Title Case". + </member> <member name="project_manager/sorting_order" type="int" setter="" getter=""> The sorting order to use in the project manager. When changing the sorting order in the project manager, this setting is set permanently in the editor settings. </member> diff --git a/doc/classes/NavigationMeshSourceGeometryData2D.xml b/doc/classes/NavigationMeshSourceGeometryData2D.xml index 3f6fcc733a..9c05248eff 100644 --- a/doc/classes/NavigationMeshSourceGeometryData2D.xml +++ b/doc/classes/NavigationMeshSourceGeometryData2D.xml @@ -47,6 +47,13 @@ Returns [code]true[/code] when parsed source geometry data exists. </description> </method> + <method name="merge"> + <return type="void" /> + <param index="0" name="other_geometry" type="NavigationMeshSourceGeometryData2D" /> + <description> + Adds the geometry data of another [NavigationMeshSourceGeometryData2D] to the navigation mesh baking data. + </description> + </method> <method name="set_obstruction_outlines"> <return type="void" /> <param index="0" name="obstruction_outlines" type="PackedVector2Array[]" /> diff --git a/doc/classes/NavigationMeshSourceGeometryData3D.xml b/doc/classes/NavigationMeshSourceGeometryData3D.xml index ffa8163eaa..a3dcd4d209 100644 --- a/doc/classes/NavigationMeshSourceGeometryData3D.xml +++ b/doc/classes/NavigationMeshSourceGeometryData3D.xml @@ -57,6 +57,13 @@ Returns [code]true[/code] when parsed source geometry data exists. </description> </method> + <method name="merge"> + <return type="void" /> + <param index="0" name="other_geometry" type="NavigationMeshSourceGeometryData3D" /> + <description> + Adds the geometry data of another [NavigationMeshSourceGeometryData3D] to the navigation mesh baking data. + </description> + </method> <method name="set_indices"> <return type="void" /> <param index="0" name="indices" type="PackedInt32Array" /> diff --git a/doc/classes/Resource.xml b/doc/classes/Resource.xml index 85372288e7..cec936ac3e 100644 --- a/doc/classes/Resource.xml +++ b/doc/classes/Resource.xml @@ -59,6 +59,12 @@ [/codeblock] </description> </method> + <method name="generate_scene_unique_id" qualifiers="static"> + <return type="String" /> + <description> + Generates a unique identifier for a resource to be contained inside a [PackedScene], based on the current date, time, and a random value. The returned string is only composed of letters ([code]a[/code] to [code]y[/code]) and numbers ([code]0[/code] to [code]8[/code]). See also [member resource_scene_unique_id]. + </description> + </method> <method name="get_local_scene" qualifiers="const"> <return type="Node" /> <description> @@ -98,6 +104,12 @@ The unique path to this resource. If it has been saved to disk, the value will be its filepath. If the resource is exclusively contained within a scene, the value will be the [PackedScene]'s filepath, followed by a unique identifier. [b]Note:[/b] Setting this property manually may fail if a resource with the same path has already been previously loaded. If necessary, use [method take_over_path]. </member> + <member name="resource_scene_unique_id" type="String" setter="set_scene_unique_id" getter="get_scene_unique_id"> + An unique identifier relative to the this resource's scene. If left empty, the ID is automatically generated when this resource is saved inside a [PackedScene]. If the resource is not inside a scene, this property is empty by default. + [b]Note:[/b] When the [PackedScene] is saved, if multiple resources in the same scene use the same ID, only the earliest resource in the scene hierarchy keeps the original ID. The other resources are assigned new IDs from [method generate_scene_unique_id]. + [b]Note:[/b] Setting this property does not emit the [signal changed] signal. + [b]Warning:[/b] When setting, the ID must only consist of letters, numbers, and underscores. Otherwise, it will fail and default to a randomly generated ID. + </member> </members> <signals> <signal name="changed"> diff --git a/doc/classes/XRInterface.xml b/doc/classes/XRInterface.xml index 631bceec49..175caca598 100644 --- a/doc/classes/XRInterface.xml +++ b/doc/classes/XRInterface.xml @@ -122,17 +122,17 @@ [param mode] is the environment blend mode starting with the next frame. [b]Note:[/b] Not all runtimes support all environment blend modes, so it is important to check this at startup. For example: [codeblock] - func _ready(): - var xr_interface: XRInterface = XRServer.find_interface("OpenXR") - if xr_interface and xr_interface.is_initialized(): - var vp: Viewport = get_viewport() - vp.use_xr = true - var acceptable_modes = [ XRInterface.XR_ENV_BLEND_MODE_OPAQUE, XRInterface.XR_ENV_BLEND_MODE_ADDITIVE ] - var modes = xr_interface.get_supported_environment_blend_modes() - for mode in acceptable_modes: - if mode in modes: - xr_interface.set_environment_blend_mode(mode) - break + func _ready(): + var xr_interface: XRInterface = XRServer.find_interface("OpenXR") + if xr_interface and xr_interface.is_initialized(): + var vp: Viewport = get_viewport() + vp.use_xr = true + var acceptable_modes = [XRInterface.XR_ENV_BLEND_MODE_OPAQUE, XRInterface.XR_ENV_BLEND_MODE_ADDITIVE] + var modes = xr_interface.get_supported_environment_blend_modes() + for mode in acceptable_modes: + if mode in modes: + xr_interface.set_environment_blend_mode(mode) + break [/codeblock] </description> </method> diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py index f711038fdf..d871535a66 100755 --- a/doc/tools/make_rst.py +++ b/doc/tools/make_rst.py @@ -437,7 +437,7 @@ class State: class TagState: - def __init__(self, raw: str, name: str, arguments: List[str], closing: bool) -> None: + def __init__(self, raw: str, name: str, arguments: str, closing: bool) -> None: self.raw = raw self.name = name @@ -1762,7 +1762,7 @@ def is_in_tagset(tag_text: str, tagset: List[str]) -> bool: # Tag with arguments. if tag_text.startswith(tag + " "): return True - # Tag with arguments, special case for [url]. + # Tag with arguments, special case for [url], [color], and [font]. if tag_text.startswith(tag + "="): return True @@ -1771,17 +1771,22 @@ def is_in_tagset(tag_text: str, tagset: List[str]) -> bool: def get_tag_and_args(tag_text: str) -> TagState: tag_name = tag_text - arguments: List[str] = [] + arguments: str = "" + delim_pos = -1 + + space_pos = tag_text.find(" ") + if space_pos >= 0: + delim_pos = space_pos + + # Special case for [url], [color], and [font]. assign_pos = tag_text.find("=") - if assign_pos >= 0: - tag_name = tag_text[:assign_pos] - arguments = [tag_text[assign_pos + 1 :].strip()] - else: - space_pos = tag_text.find(" ") - if space_pos >= 0: - tag_name = tag_text[:space_pos] - arguments = [tag_text[space_pos + 1 :].strip()] + if assign_pos >= 0 and (delim_pos < 0 or assign_pos < delim_pos): + delim_pos = assign_pos + + if delim_pos >= 0: + tag_name = tag_text[:delim_pos] + arguments = tag_text[delim_pos + 1 :].strip() closing = False if tag_name.startswith("/"): @@ -1969,11 +1974,14 @@ def format_text_block( state, ) - tag_text = "\n::\n" + if "lang=text" in tag_state.arguments.split(" "): + tag_text = "\n.. code::\n" + else: + tag_text = "\n::\n" inside_code = True inside_code_tag = tag_state.name - ignore_code_warnings = "skip-lint" in tag_state.arguments + ignore_code_warnings = "skip-lint" in tag_state.arguments.split(" ") elif is_in_tagset(tag_state.name, ["code"]): tag_text = "``" @@ -1981,7 +1989,7 @@ def format_text_block( inside_code = True inside_code_tag = "code" - ignore_code_warnings = "skip-lint" in tag_state.arguments + ignore_code_warnings = "skip-lint" in tag_state.arguments.split(" ") escape_pre = True if not ignore_code_warnings: @@ -2078,7 +2086,7 @@ def format_text_block( # Cross-references to items in this or other class documentation pages. elif is_in_tagset(tag_state.name, RESERVED_CROSSLINK_TAGS): - link_target: str = tag_state.arguments[0] if len(tag_state.arguments) > 0 else "" + link_target: str = tag_state.arguments if link_target == "": print_error( @@ -2238,7 +2246,7 @@ def format_text_block( # Formatting directives. elif is_in_tagset(tag_state.name, ["url"]): - url_target = tag_state.arguments[0] if len(tag_state.arguments) > 0 else "" + url_target = tag_state.arguments if url_target == "": print_error( @@ -2439,7 +2447,7 @@ def format_codeblock( opening_formatted = tag_state.name if len(tag_state.arguments) > 0: - opening_formatted += " " + " ".join(tag_state.arguments) + opening_formatted += " " + tag_state.arguments code_text = post_text[len(f"[{opening_formatted}]") : end_pos] post_text = post_text[end_pos:] diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index f9af86e19b..09edc12112 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -4075,7 +4075,7 @@ RasterizerSceneGLES3::RasterizerSceneGLES3() { scene_globals.default_shader = material_storage->shader_allocate(); material_storage->shader_initialize(scene_globals.default_shader); material_storage->shader_set_code(scene_globals.default_shader, R"( -// Default 3D material shader. +// Default 3D material shader (Compatibility). shader_type spatial; @@ -4100,11 +4100,11 @@ void fragment() { scene_globals.overdraw_shader = material_storage->shader_allocate(); material_storage->shader_initialize(scene_globals.overdraw_shader); material_storage->shader_set_code(scene_globals.overdraw_shader, R"( -// 3D editor Overdraw debug draw mode shader. +// 3D editor Overdraw debug draw mode shader (Compatibility). shader_type spatial; -render_mode blend_add, unshaded; +render_mode blend_add, unshaded, fog_disabled; void fragment() { ALBEDO = vec3(0.4, 0.8, 0.8); diff --git a/drivers/gles3/shaders/particles.glsl b/drivers/gles3/shaders/particles.glsl index 64ef26b075..d95f7f4309 100644 --- a/drivers/gles3/shaders/particles.glsl +++ b/drivers/gles3/shaders/particles.glsl @@ -112,22 +112,22 @@ layout(location = 4) in highp vec4 xform_2; layout(location = 5) in highp vec4 xform_3; #endif #ifdef USERDATA1_USED -layout(location = 6) in highp vec4 userdata1; +in highp vec4 userdata1; #endif #ifdef USERDATA2_USED -layout(location = 7) in highp vec4 userdata2; +in highp vec4 userdata2; #endif #ifdef USERDATA3_USED -layout(location = 8) in highp vec4 userdata3; +in highp vec4 userdata3; #endif #ifdef USERDATA4_USED -layout(location = 9) in highp vec4 userdata4; +in highp vec4 userdata4; #endif #ifdef USERDATA5_USED -layout(location = 10) in highp vec4 userdata5; +in highp vec4 userdata5; #endif #ifdef USERDATA6_USED -layout(location = 11) in highp vec4 userdata6; +in highp vec4 userdata6; #endif out highp vec4 out_color; //tfb: @@ -219,6 +219,24 @@ void main() { #endif xform = transpose(xform); flags = floatBitsToUint(velocity_flags.w); +#ifdef USERDATA1_USED + out_userdata1 = userdata1; +#endif +#ifdef USERDATA2_USED + out_userdata2 = userdata2; +#endif +#ifdef USERDATA3_USED + out_userdata3 = userdata3; +#endif +#ifdef USERDATA4_USED + out_userdata4 = userdata4; +#endif +#ifdef USERDATA5_USED + out_userdata5 = userdata5; +#endif +#ifdef USERDATA6_USED + out_userdata6 = userdata6; +#endif } //clear started flag if set diff --git a/drivers/gles3/storage/particles_storage.cpp b/drivers/gles3/storage/particles_storage.cpp index 4d563ab28b..b72b4eaf8d 100644 --- a/drivers/gles3/storage/particles_storage.cpp +++ b/drivers/gles3/storage/particles_storage.cpp @@ -728,8 +728,10 @@ void ParticlesStorage::_particles_process(Particles *p_particles, double p_delta ParticlesShaderGLES3::ShaderVariant variant = ParticlesShaderGLES3::MODE_DEFAULT; uint32_t specialization = 0; - for (uint32_t i = 0; i < p_particles->userdata_count; i++) { - specialization |= (1 << i); + for (uint32_t i = 0; i < PARTICLES_MAX_USERDATAS; i++) { + if (m->shader_data->userdatas_used[i]) { + specialization |= ParticlesShaderGLES3::USERDATA1_USED << i; + } } if (p_particles->mode == RS::ParticlesMode::PARTICLES_MODE_3D) { diff --git a/drivers/wasapi/audio_driver_wasapi.cpp b/drivers/wasapi/audio_driver_wasapi.cpp index 64f2d1f203..8ea1f52d15 100644 --- a/drivers/wasapi/audio_driver_wasapi.cpp +++ b/drivers/wasapi/audio_driver_wasapi.cpp @@ -737,12 +737,17 @@ void AudioDriverWASAPI::thread_func(void *p_udata) { ad->start_counting_ticks(); if (avail_frames > 0 && ad->audio_output.audio_client) { + UINT32 buffer_size; UINT32 cur_frames; bool invalidated = false; - HRESULT hr = ad->audio_output.audio_client->GetCurrentPadding(&cur_frames); + HRESULT hr = ad->audio_output.audio_client->GetBufferSize(&buffer_size); + if (hr != S_OK) { + ERR_PRINT("WASAPI: GetBufferSize error"); + } + hr = ad->audio_output.audio_client->GetCurrentPadding(&cur_frames); if (hr == S_OK) { // Check how much frames are available on the WASAPI buffer - UINT32 write_frames = MIN(ad->buffer_frames - cur_frames, avail_frames); + UINT32 write_frames = MIN(buffer_size - cur_frames, avail_frames); if (write_frames > 0) { BYTE *buffer = nullptr; hr = ad->audio_output.render_client->GetBuffer(write_frames, &buffer); diff --git a/editor/doc_tools.cpp b/editor/doc_tools.cpp index ee3e54e9df..52b0d485cf 100644 --- a/editor/doc_tools.cpp +++ b/editor/doc_tools.cpp @@ -477,7 +477,7 @@ void DocTools::generate(BitField<GenerateFlags> p_flags) { } if (properties_from_instance) { - if (E.name == "resource_local_to_scene" || E.name == "resource_name" || E.name == "resource_path" || E.name == "script") { + if (E.name == "resource_local_to_scene" || E.name == "resource_name" || E.name == "resource_path" || E.name == "script" || E.name == "resource_scene_unique_id") { // Don't include spurious properties from Object property list. continue; } diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index 3e11b4a5ff..83e71292a3 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -1333,24 +1333,28 @@ void EditorFileSystem::_save_filesystem_cache(EditorFileSystemDirectory *p_dir, p_file->store_line("::" + p_dir->get_path() + "::" + String::num(p_dir->modified_time)); for (int i = 0; i < p_dir->files.size(); i++) { - if (!p_dir->files[i]->import_group_file.is_empty()) { - group_file_cache.insert(p_dir->files[i]->import_group_file); + const EditorFileSystemDirectory::FileInfo *file_info = p_dir->files[i]; + if (!file_info->import_group_file.is_empty()) { + group_file_cache.insert(file_info->import_group_file); } - String type = p_dir->files[i]->type; - if (p_dir->files[i]->resource_script_class) { - type += "/" + String(p_dir->files[i]->resource_script_class); - } - String s = p_dir->files[i]->file + "::" + type + "::" + itos(p_dir->files[i]->uid) + "::" + itos(p_dir->files[i]->modified_time) + "::" + itos(p_dir->files[i]->import_modified_time) + "::" + itos(p_dir->files[i]->import_valid) + "::" + p_dir->files[i]->import_group_file + "::" + p_dir->files[i]->script_class_name + "<>" + p_dir->files[i]->script_class_extends + "<>" + p_dir->files[i]->script_class_icon_path; - s += "::"; - for (int j = 0; j < p_dir->files[i]->deps.size(); j++) { - if (j > 0) { - s += "<>"; - } - s += p_dir->files[i]->deps[j]; + String type = file_info->type; + if (file_info->resource_script_class) { + type += "/" + String(file_info->resource_script_class); } - p_file->store_line(s); + PackedStringArray cache_string; + cache_string.append(file_info->file); + cache_string.append(type); + cache_string.append(itos(file_info->uid)); + cache_string.append(itos(file_info->modified_time)); + cache_string.append(itos(file_info->import_modified_time)); + cache_string.append(itos(file_info->import_valid)); + cache_string.append(file_info->import_group_file); + cache_string.append(String("<>").join({ file_info->script_class_name, file_info->script_class_extends, file_info->script_class_icon_path })); + cache_string.append(String("<>").join(file_info->deps)); + + p_file->store_line(String("::").join(cache_string)); } for (int i = 0; i < p_dir->subdirs.size(); i++) { diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index f94b0dba05..8c55c45190 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -47,6 +47,20 @@ #include "editor/themes/editor_scale.h" #include "scene/gui/line_edit.h" +#include "modules/modules_enabled.gen.h" // For gdscript, mono. + +// For syntax highlighting. +#ifdef MODULE_GDSCRIPT_ENABLED +#include "modules/gdscript/editor/gdscript_highlighter.h" +#include "modules/gdscript/gdscript.h" +#endif + +// For syntax highlighting. +#ifdef MODULE_MONO_ENABLED +#include "editor/plugins/script_editor_plugin.h" +#include "modules/mono/csharp_script.h" +#endif + #define CONTRIBUTE_URL vformat("%s/contributing/documentation/updating_the_class_reference.html", VERSION_DOCS_URL) #ifdef MODULE_MONO_ENABLED @@ -346,7 +360,7 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is link_t = link_t.trim_suffix("[]"); display_t = display_t.trim_suffix("[]"); - class_desc->push_meta("#Array"); // class + class_desc->push_meta("#Array", RichTextLabel::META_UNDERLINE_ON_HOVER); // class class_desc->add_text("Array"); class_desc->pop(); // meta class_desc->add_text("["); @@ -360,14 +374,14 @@ void EditorHelp::_add_type(const String &p_type, const String &p_enum, bool p_is } if (is_enum_type) { - class_desc->push_meta("$" + link_t); // enum + class_desc->push_meta("$" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // enum } else { - class_desc->push_meta("#" + link_t); // class + class_desc->push_meta("#" + link_t, RichTextLabel::META_UNDERLINE_ON_HOVER); // class } } class_desc->add_text(display_t); if (can_ref) { - class_desc->pop(); // Pushed meta above. + class_desc->pop(); // meta if (add_array) { class_desc->add_text("]"); } else if (is_bitfield) { @@ -489,7 +503,7 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview const bool is_documented = p_method.is_deprecated || p_method.is_experimental || !p_method.description.strip_edges().is_empty(); if (p_overview && is_documented) { - class_desc->push_meta("@method " + p_method.name); + class_desc->push_meta("@method " + p_method.name, RichTextLabel::META_UNDERLINE_ON_HOVER); } class_desc->push_color(theme_cache.headline_color); @@ -1196,7 +1210,7 @@ void EditorHelp::_update_doc() { class_desc->push_color(theme_cache.headline_color); if (describe) { - class_desc->push_meta("@member " + prop.name); + class_desc->push_meta("@member " + prop.name, RichTextLabel::META_UNDERLINE_ON_HOVER); } class_desc->add_text(prop.name); @@ -2298,18 +2312,27 @@ void EditorHelp::_help_callback(const String &p_topic) { } } -static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class = "") { - DocTools *doc = EditorHelp::get_doc_data(); - String base_path; +static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class) { + const DocTools *doc = EditorHelp::get_doc_data(); + + bool is_native = false; + { + const HashMap<String, DocData::ClassDoc>::ConstIterator E = doc->class_list.find(p_class); + if (E && !E->value.is_script_doc) { + is_native = true; + } + } + + const bool using_tab_indent = int(EDITOR_GET("text_editor/behavior/indent/type")) == 0; - Ref<Font> doc_font = p_owner_node->get_theme_font(SNAME("doc"), EditorStringName(EditorFonts)); - Ref<Font> doc_bold_font = p_owner_node->get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts)); - Ref<Font> doc_italic_font = p_owner_node->get_theme_font(SNAME("doc_italic"), EditorStringName(EditorFonts)); - Ref<Font> doc_code_font = p_owner_node->get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts)); - Ref<Font> doc_kbd_font = p_owner_node->get_theme_font(SNAME("doc_keyboard"), EditorStringName(EditorFonts)); + const Ref<Font> doc_font = p_owner_node->get_theme_font(SNAME("doc"), EditorStringName(EditorFonts)); + const Ref<Font> doc_bold_font = p_owner_node->get_theme_font(SNAME("doc_bold"), EditorStringName(EditorFonts)); + const Ref<Font> doc_italic_font = p_owner_node->get_theme_font(SNAME("doc_italic"), EditorStringName(EditorFonts)); + const Ref<Font> doc_code_font = p_owner_node->get_theme_font(SNAME("doc_source"), EditorStringName(EditorFonts)); + const Ref<Font> doc_kbd_font = p_owner_node->get_theme_font(SNAME("doc_keyboard"), EditorStringName(EditorFonts)); - int doc_code_font_size = p_owner_node->get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts)); - int doc_kbd_font_size = p_owner_node->get_theme_font_size(SNAME("doc_keyboard_size"), EditorStringName(EditorFonts)); + const int doc_code_font_size = p_owner_node->get_theme_font_size(SNAME("doc_source_size"), EditorStringName(EditorFonts)); + const int doc_kbd_font_size = p_owner_node->get_theme_font_size(SNAME("doc_keyboard_size"), EditorStringName(EditorFonts)); const Color type_color = p_owner_node->get_theme_color(SNAME("type_color"), SNAME("EditorHelp")); const Color code_color = p_owner_node->get_theme_color(SNAME("code_color"), SNAME("EditorHelp")); @@ -2330,7 +2353,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control // Select the correct code examples. switch ((int)EDITOR_GET("text_editor/help/class_reference_examples")) { case 0: // GDScript - bbcode = bbcode.replace("[gdscript", "[codeblock"); // Tag can have extra arguments. + bbcode = bbcode.replace("[gdscript", "[codeblock lang=gdscript"); // Tag can have extra arguments. bbcode = bbcode.replace("[/gdscript]", "[/codeblock]"); for (int pos = bbcode.find("[csharp"); pos != -1; pos = bbcode.find("[csharp")) { @@ -2347,7 +2370,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control } break; case 1: // C# - bbcode = bbcode.replace("[csharp", "[codeblock"); // Tag can have extra arguments. + bbcode = bbcode.replace("[csharp", "[codeblock lang=csharp"); // Tag can have extra arguments. bbcode = bbcode.replace("[/csharp]", "[/codeblock]"); for (int pos = bbcode.find("[gdscript"); pos != -1; pos = bbcode.find("[gdscript")) { @@ -2364,8 +2387,8 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control } break; case 2: // GDScript and C# - bbcode = bbcode.replace("[csharp", "[b]C#:[/b]\n[codeblock"); // Tag can have extra arguments. - bbcode = bbcode.replace("[gdscript", "[b]GDScript:[/b]\n[codeblock"); // Tag can have extra arguments. + bbcode = bbcode.replace("[csharp", "[b]C#:[/b]\n[codeblock lang=csharp"); // Tag can have extra arguments. + bbcode = bbcode.replace("[gdscript", "[b]GDScript:[/b]\n[codeblock lang=gdscript"); // Tag can have extra arguments. bbcode = bbcode.replace("[/csharp]", "[/codeblock]"); bbcode = bbcode.replace("[/gdscript]", "[/codeblock]"); @@ -2378,17 +2401,11 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control bbcode = bbcode.replace("[codeblocks]", ""); bbcode = bbcode.replace("[/codeblocks]", ""); - // Remove extra new lines around code blocks. - bbcode = bbcode.replace("[codeblock]\n", "[codeblock]"); - bbcode = bbcode.replace("[codeblock skip-lint]\n", "[codeblock skip-lint]"); // Extra argument to silence validation warnings. - bbcode = bbcode.replace("\n[/codeblock]", "[/codeblock]"); + // Remove `\n` here because `\n` is replaced by `\n\n` later. + // Will be compensated when parsing `[/codeblock]`. bbcode = bbcode.replace("[/codeblock]\n", "[/codeblock]"); List<String> tag_stack; - bool code_tag = false; - bool codeblock_tag = false; - const bool using_tab_indent = int(EDITOR_GET("text_editor/behavior/indent/type")) == 0; - StringBuilder codeblock_text; int pos = 0; while (pos < bbcode.length()) { @@ -2399,33 +2416,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control } if (brk_pos > pos) { - String text = bbcode.substr(pos, brk_pos - pos); - if (codeblock_tag && using_tab_indent) { - // Replace the code block's space indentation with tabs. - StringBuilder builder; - PackedStringArray text_lines = text.split("\n"); - for (const String &line : text_lines) { - String stripped_line = line.dedent(); - int space_count = line.length() - stripped_line.length(); - - if (builder.num_strings_appended() > 0) { - builder.append("\n"); - } - if (space_count > 0) { - builder.append(String("\t").repeat(MAX(space_count / 4, 1)) + stripped_line); - } else { - builder.append(line); - } - } - text = builder.as_string(); - } - if (!code_tag && !codeblock_tag) { - text = text.replace("\n", "\n\n"); - } - if (codeblock_tag) { - codeblock_text.append(text); - } - p_rt->add_text(text); + p_rt->add_text(bbcode.substr(pos, brk_pos - pos).replace("\n", "\n\n")); } if (brk_pos == bbcode.length()) { @@ -2435,16 +2426,11 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control int brk_end = bbcode.find_char(']', brk_pos + 1); if (brk_end == -1) { - String text = bbcode.substr(brk_pos, bbcode.length() - brk_pos); - if (!code_tag && !codeblock_tag) { - text = text.replace("\n", "\n\n"); - } - p_rt->add_text(text); - + p_rt->add_text(bbcode.substr(brk_pos, bbcode.length() - brk_pos).replace("\n", "\n\n")); break; } - String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1); + const String tag = bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1); if (tag.begins_with("/")) { bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1, tag.length()); @@ -2458,64 +2444,32 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control tag_stack.pop_front(); pos = brk_end + 1; if (tag != "/img") { - if (code_tag) { - p_rt->pop(); // color - p_rt->pop(); // background color - p_rt->pop(); // font size - } else if (codeblock_tag) { - p_rt->pop(); // color - p_rt->pop(); // cell - - // Copy codeblock button. - p_rt->push_cell(); - p_rt->set_cell_row_background_color(code_bg_color, Color(code_bg_color, 0.99)); - p_rt->set_cell_padding(Rect2(0, 10 * EDSCALE, 0, 10 * EDSCALE)); - p_rt->set_cell_size_override(Vector2(1, 1), Vector2(10, 10) * EDSCALE); - p_rt->push_meta("^" + codeblock_text.as_string(), RichTextLabel::META_UNDERLINE_ON_HOVER); - codeblock_text = StringBuilder(); - p_rt->add_image(p_owner_node->get_editor_theme_icon(SNAME("ActionCopy")), 24 * EDSCALE, 24 * EDSCALE, Color(link_property_color, 0.3), INLINE_ALIGNMENT_BOTTOM_TO, Rect2(), Variant(), false, TTR("Click to copy.")); - p_rt->pop(); // meta - p_rt->pop(); // cell - - p_rt->pop(); // table - p_rt->pop(); // font size - - if (pos < bbcode.length()) { - p_rt->add_newline(); - } - } - p_rt->pop(); // Pops font for codetags & codeblocks, anything else for other tags. - } - code_tag = false; - codeblock_tag = false; - - } else if (code_tag || codeblock_tag) { - p_rt->add_text("["); - pos = brk_pos + 1; - if (codeblock_tag) { - codeblock_text.append("["); + p_rt->pop(); } } else if (tag.begins_with("method ") || tag.begins_with("constructor ") || tag.begins_with("operator ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("annotation ") || tag.begins_with("theme_item ")) { const int tag_end = tag.find_char(' '); const String link_tag = tag.substr(0, tag_end); const String link_target = tag.substr(tag_end + 1, tag.length()).lstrip(" "); - // Use monospace font to make clickable references - // easier to distinguish from inline code and other text. - p_rt->push_font(doc_code_font); - p_rt->push_font_size(doc_code_font_size); - Color target_color = link_color; + RichTextLabel::MetaUnderline underline_mode = RichTextLabel::META_UNDERLINE_ON_HOVER; if (link_tag == "method" || link_tag == "constructor" || link_tag == "operator") { target_color = link_method_color; } else if (link_tag == "member" || link_tag == "signal" || link_tag == "theme_item") { target_color = link_property_color; } else if (link_tag == "annotation") { target_color = link_annotation_color; + } else { + // Better visibility for constants, enums, etc. + underline_mode = RichTextLabel::META_UNDERLINE_ALWAYS; } + // Use monospace font to make clickable references + // easier to distinguish from inline code and other text. + p_rt->push_font(doc_code_font); + p_rt->push_font_size(doc_code_font_size); p_rt->push_color(target_color); - p_rt->push_meta("@" + link_tag + " " + link_target); + p_rt->push_meta("@" + link_tag + " " + link_target, underline_mode); if (link_tag == "member" && ((!link_target.contains(".") && (p_class == "ProjectSettings" || p_class == "EditorSettings")) || @@ -2541,11 +2495,10 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control p_rt->pop(); // meta p_rt->pop(); // color - - p_rt->pop(); // font size + p_rt->pop(); // font_size p_rt->pop(); // font - pos = brk_end + 1; + pos = brk_end + 1; } else if (tag.begins_with("param ")) { const int tag_end = tag.find_char(' '); const String param_name = tag.substr(tag_end + 1, tag.length()).lstrip(" "); @@ -2553,41 +2506,40 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control // Use monospace font with translucent background color to make code easier to distinguish from other text. p_rt->push_font(doc_code_font); p_rt->push_font_size(doc_code_font_size); - p_rt->push_bgcolor(param_bg_color); p_rt->push_color(code_color); + p_rt->add_text(param_name); - p_rt->pop(); - p_rt->pop(); - p_rt->pop(); // font size + p_rt->pop(); // color + p_rt->pop(); // bgcolor + p_rt->pop(); // font_size p_rt->pop(); // font - pos = brk_end + 1; + pos = brk_end + 1; } else if (tag == p_class) { // Use a bold font when class reference tags are in their own page. p_rt->push_font(doc_bold_font); p_rt->add_text(tag); - p_rt->pop(); + p_rt->pop(); // font pos = brk_end + 1; - } else if (doc->class_list.has(tag)) { // Use a monospace font for class reference tags such as [Node2D] or [SceneTree]. p_rt->push_font(doc_code_font); p_rt->push_font_size(doc_code_font_size); p_rt->push_color(type_color); - p_rt->push_meta("#" + tag); + p_rt->push_meta("#" + tag, RichTextLabel::META_UNDERLINE_ON_HOVER); + p_rt->add_text(tag); - p_rt->pop(); - p_rt->pop(); - p_rt->pop(); // Font size - p_rt->pop(); // Font + p_rt->pop(); // meta + p_rt->pop(); // color + p_rt->pop(); // font_size + p_rt->pop(); // font pos = brk_end + 1; - } else if (tag == "b") { // Use bold font. p_rt->push_font(doc_bold_font); @@ -2601,42 +2553,134 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "code" || tag.begins_with("code ")) { + int end_pos = bbcode.find("[/code]", brk_end + 1); + if (end_pos < 0) { + end_pos = bbcode.length(); + } + // Use monospace font with darkened background color to make code easier to distinguish from other text. p_rt->push_font(doc_code_font); p_rt->push_font_size(doc_code_font_size); p_rt->push_bgcolor(code_bg_color); p_rt->push_color(code_color.lerp(p_owner_node->get_theme_color(SNAME("error_color"), EditorStringName(Editor)), 0.6)); - code_tag = true; - pos = brk_end + 1; - tag_stack.push_front("code"); + p_rt->add_text(bbcode.substr(brk_end + 1, end_pos - (brk_end + 1))); + + p_rt->pop(); // color + p_rt->pop(); // bgcolor + p_rt->pop(); // font_size + p_rt->pop(); // font + + pos = end_pos + 7; // `len("[/code]")`. } else if (tag == "codeblock" || tag.begins_with("codeblock ")) { + int end_pos = bbcode.find("[/codeblock]", brk_end + 1); + if (end_pos < 0) { + end_pos = bbcode.length(); + } + + const String codeblock_text = bbcode.substr(brk_end + 1, end_pos - (brk_end + 1)).strip_edges(); + + String codeblock_copy_text = codeblock_text; + if (using_tab_indent) { + // Replace the code block's space indentation with tabs. + StringBuilder builder; + PackedStringArray text_lines = codeblock_copy_text.split("\n"); + for (const String &line : text_lines) { + const String stripped_line = line.dedent(); + const int space_count = line.length() - stripped_line.length(); + + if (builder.num_strings_appended() > 0) { + builder.append("\n"); + } + if (space_count > 0) { + builder.append(String("\t").repeat(MAX(space_count / 4, 1)) + stripped_line); + } else { + builder.append(line); + } + } + codeblock_copy_text = builder.as_string(); + } + + String lang; + const PackedStringArray args = tag.trim_prefix("codeblock").split(" ", false); + for (int i = args.size() - 1; i >= 0; i--) { + if (args[i].begins_with("lang=")) { + lang = args[i].trim_prefix("lang="); + break; + } + } + // Use monospace font with darkened background color to make code easier to distinguish from other text. // Use a single-column table with cell row background color instead of `[bgcolor]`. // This makes the background color highlight cover the entire block, rather than individual lines. p_rt->push_font(doc_code_font); p_rt->push_font_size(doc_code_font_size); - p_rt->push_table(2); + p_rt->push_cell(); p_rt->set_cell_row_background_color(code_bg_color, Color(code_bg_color, 0.99)); p_rt->set_cell_padding(Rect2(10 * EDSCALE, 10 * EDSCALE, 10 * EDSCALE, 10 * EDSCALE)); p_rt->push_color(code_dark_color); - codeblock_tag = true; - pos = brk_end + 1; - tag_stack.push_front("codeblock"); + if (lang.is_empty() || lang == "gdscript") { +#ifdef MODULE_GDSCRIPT_ENABLED + EditorHelpHighlighter::get_singleton()->highlight(p_rt, EditorHelpHighlighter::LANGUAGE_GDSCRIPT, codeblock_text, is_native); +#else + p_rt->add_text(codeblock_text); +#endif + } else if (lang == "csharp") { +#ifdef MODULE_MONO_ENABLED + EditorHelpHighlighter::get_singleton()->highlight(p_rt, EditorHelpHighlighter::LANGUAGE_CSHARP, codeblock_text, is_native); +#else + p_rt->add_text(codeblock_text); +#endif + } else { + p_rt->add_text(codeblock_text); + } + + p_rt->pop(); // color + p_rt->pop(); // cell + + // Copy codeblock button. + p_rt->push_cell(); + p_rt->set_cell_row_background_color(code_bg_color, Color(code_bg_color, 0.99)); + p_rt->set_cell_padding(Rect2(0, 10 * EDSCALE, 0, 10 * EDSCALE)); + p_rt->set_cell_size_override(Vector2(1, 1), Vector2(10, 10) * EDSCALE); + p_rt->push_meta("^" + codeblock_copy_text, RichTextLabel::META_UNDERLINE_ON_HOVER); + p_rt->add_image(p_owner_node->get_editor_theme_icon(SNAME("ActionCopy")), 24 * EDSCALE, 24 * EDSCALE, Color(link_property_color, 0.3), INLINE_ALIGNMENT_BOTTOM_TO, Rect2(), Variant(), false, TTR("Click to copy.")); + p_rt->pop(); // meta + p_rt->pop(); // cell + + p_rt->pop(); // table + p_rt->pop(); // font_size + p_rt->pop(); // font + + pos = end_pos + 12; // `len("[/codeblock]")`. + + // Compensate for `\n` removed before the loop. + if (pos < bbcode.length()) { + p_rt->add_newline(); + } } else if (tag == "kbd") { + int end_pos = bbcode.find("[/kbd]", brk_end + 1); + if (end_pos < 0) { + end_pos = bbcode.length(); + } + // Use keyboard font with custom color and background color. p_rt->push_font(doc_kbd_font); p_rt->push_font_size(doc_kbd_font_size); p_rt->push_bgcolor(kbd_bg_color); p_rt->push_color(kbd_color); - code_tag = true; // Though not strictly a code tag, logic is similar. - pos = brk_end + 1; - tag_stack.push_front(tag); + p_rt->add_text(bbcode.substr(brk_end + 1, end_pos - (brk_end + 1))); + + p_rt->pop(); // color + p_rt->pop(); // bgcolor + p_rt->pop(); // font_size + p_rt->pop(); // font + pos = end_pos + 6; // `len("[/kbd]")`. } else if (tag == "center") { // Align to center. p_rt->push_paragraph(HORIZONTAL_ALIGNMENT_CENTER, Control::TEXT_DIRECTION_AUTO, ""); @@ -2712,9 +2756,9 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control if (end == -1) { end = bbcode.length(); } - String image = bbcode.substr(brk_end + 1, end - brk_end - 1); - p_rt->add_image(ResourceLoader::load(base_path.path_join(image), "Texture2D"), width, height, Color(1, 1, 1), INLINE_ALIGNMENT_CENTER, Rect2(), Variant(), false, String(), size_in_percent); + String image_path = bbcode.substr(brk_end + 1, end - brk_end - 1); + p_rt->add_image(ResourceLoader::load(image_path, "Texture2D"), width, height, Color(1, 1, 1), INLINE_ALIGNMENT_CENTER, Rect2(), Variant(), false, String(), size_in_percent); pos = end; tag_stack.push_front("img"); @@ -2725,11 +2769,9 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = brk_end + 1; tag_stack.push_front("color"); - } else if (tag.begins_with("font=")) { - String fnt = tag.substr(5, tag.length()); - - Ref<Font> font = ResourceLoader::load(base_path.path_join(fnt), "Font"); + String font_path = tag.substr(5, tag.length()); + Ref<Font> font = ResourceLoader::load(font_path, "Font"); if (font.is_valid()) { p_rt->push_font(font); } else { @@ -2738,12 +2780,18 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = brk_end + 1; tag_stack.push_front("font"); - } else { - p_rt->add_text("["); // ignore + p_rt->add_text("["); // Ignore. pos = brk_pos + 1; } } + + // Close unclosed tags. + for (const String &tag : tag_stack) { + if (tag != "img") { + p_rt->pop(); + } + } } void EditorHelp::_add_text(const String &p_bbcode) { @@ -2882,7 +2930,16 @@ void EditorHelp::_notification(int p_what) { } break; case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { - if (!EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/help")) { + bool need_update = false; + if (EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/help")) { + need_update = true; + } +#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) + if (!need_update && EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/theme/highlighting")) { + need_update = true; + } +#endif + if (!need_update) { break; } [[fallthrough]]; @@ -3076,10 +3133,11 @@ String EditorHelpBit::get_class_description(const StringName &p_class_name) cons } String description; - HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + + const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); if (E) { // Non-native class shouldn't be cached, nor translated. - bool is_native = ClassDB::class_exists(p_class_name); + const bool is_native = !E->value.is_script_doc; description = is_native ? DTR(E->value.brief_description) : E->value.brief_description; if (is_native) { @@ -3100,11 +3158,13 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c } String description; - // Non-native properties shouldn't be cached, nor translated. - bool is_native = ClassDB::class_exists(p_class_name); - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); + + const DocTools *dd = EditorHelp::get_doc_data(); + const HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); if (E) { + // Non-native properties shouldn't be cached, nor translated. + const bool is_native = !E->value.is_script_doc; + for (const DocData::PropertyDoc &property : E->value.properties) { String description_current = is_native ? DTR(property.description) : property.description; @@ -3112,7 +3172,7 @@ String EditorHelpBit::get_property_description(const StringName &p_class_name, c const String enum_name = class_enum.size() >= 2 ? class_enum[1] : ""; if (!enum_name.is_empty()) { // Classes can use enums from other classes, so check from which it came. - HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_enum[0]); + const HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_enum[0]); if (enum_class) { for (DocData::ConstantDoc val : enum_class->value.constants) { // Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector. @@ -3151,10 +3211,11 @@ String EditorHelpBit::get_method_description(const StringName &p_class_name, con } String description; - HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + + const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); if (E) { // Non-native methods shouldn't be cached, nor translated. - bool is_native = ClassDB::class_exists(p_class_name); + const bool is_native = !E->value.is_script_doc; for (const DocData::MethodDoc &method : E->value.methods) { String description_current = is_native ? DTR(method.description) : method.description; @@ -3182,10 +3243,11 @@ String EditorHelpBit::get_signal_description(const StringName &p_class_name, con } String description; - HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + + const HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); if (E) { // Non-native signals shouldn't be cached, nor translated. - bool is_native = ClassDB::class_exists(p_class_name); + const bool is_native = !E->value.is_script_doc; for (const DocData::MethodDoc &signal : E->value.signals) { String description_current = is_native ? DTR(signal.description) : signal.description; @@ -3213,12 +3275,13 @@ String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, } String description; + bool found = false; - DocTools *dd = EditorHelp::get_doc_data(); + const DocTools *dd = EditorHelp::get_doc_data(); HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); while (E) { // Non-native theme items shouldn't be cached, nor translated. - bool is_native = ClassDB::class_exists(p_class_name); + const bool is_native = !E->value.is_script_doc; for (const DocData::ThemeItemDoc &theme_item : E->value.theme_properties) { String description_current = is_native ? DTR(theme_item.description) : theme_item.description; @@ -3257,7 +3320,7 @@ void EditorHelpBit::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { rich_text->add_theme_color_override("selection_color", get_theme_color(SNAME("selection_color"), SNAME("EditorHelp"))); rich_text->clear(); - _add_text_to_rt(text, rich_text, this); + _add_text_to_rt(text, rich_text, this, doc_class_name); rich_text->reset_size(); // Force recalculating size after parsing bbcode. } break; } @@ -3266,7 +3329,7 @@ void EditorHelpBit::_notification(int p_what) { void EditorHelpBit::set_text(const String &p_text) { text = p_text; rich_text->clear(); - _add_text_to_rt(text, rich_text, this); + _add_text_to_rt(text, rich_text, this, doc_class_name); } EditorHelpBit::EditorHelpBit() { @@ -3304,6 +3367,8 @@ void EditorHelpTooltip::parse_tooltip(const String &p_text) { const String &property_name = slices[2]; const String &property_args = slices[3]; + doc_class_name = class_name; + String formatted_text; // Exclude internal properties, they are not documented. @@ -3357,6 +3422,170 @@ EditorHelpTooltip::EditorHelpTooltip(const String &p_text, const String &p_custo get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0)); } +#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) +/// EditorHelpHighlighter /// + +EditorHelpHighlighter *EditorHelpHighlighter::singleton = nullptr; + +void EditorHelpHighlighter::create_singleton() { + ERR_FAIL_COND(singleton != nullptr); + singleton = memnew(EditorHelpHighlighter); +} + +void EditorHelpHighlighter::free_singleton() { + ERR_FAIL_NULL(singleton); + memdelete(singleton); + singleton = nullptr; +} + +EditorHelpHighlighter *EditorHelpHighlighter::get_singleton() { + return singleton; +} + +EditorHelpHighlighter::HighlightData EditorHelpHighlighter::_get_highlight_data(Language p_language, const String &p_source, bool p_use_cache) { + switch (p_language) { + case LANGUAGE_GDSCRIPT: +#ifndef MODULE_GDSCRIPT_ENABLED + ERR_FAIL_V_MSG(HighlightData(), "GDScript module is disabled."); +#endif + break; + case LANGUAGE_CSHARP: +#ifndef MODULE_MONO_ENABLED + ERR_FAIL_V_MSG(HighlightData(), "Mono module is disabled."); +#endif + break; + default: + ERR_FAIL_V_MSG(HighlightData(), "Invalid parameter \"p_language\"."); + } + + if (p_use_cache) { + const HashMap<String, HighlightData>::ConstIterator E = highlight_data_caches[p_language].find(p_source); + if (E) { + return E->value; + } + } + + text_edits[p_language]->set_text(p_source); + scripts[p_language]->set_source_code(p_source); + highlighters[p_language]->_update_cache(); + + HighlightData result; + + int source_offset = 0; + int result_index = 0; + for (int i = 0; i < text_edits[p_language]->get_line_count(); i++) { + const Dictionary dict = highlighters[p_language]->_get_line_syntax_highlighting_impl(i); + + result.resize(result.size() + dict.size()); + + const Variant *key = nullptr; + int prev_column = -1; + while ((key = dict.next(key)) != nullptr) { + const int column = *key; + ERR_FAIL_COND_V(column <= prev_column, HighlightData()); + prev_column = column; + + const Color color = dict[*key].operator Dictionary().get("color", Color()); + + result.write[result_index] = { source_offset + column, color }; + result_index++; + } + + source_offset += text_edits[p_language]->get_line(i).length() + 1; // Plus newline. + } + + if (p_use_cache) { + highlight_data_caches[p_language][p_source] = result; + } + + return result; +} + +void EditorHelpHighlighter::highlight(RichTextLabel *p_rich_text_label, Language p_language, const String &p_source, bool p_use_cache) { + ERR_FAIL_NULL(p_rich_text_label); + + const HighlightData highlight_data = _get_highlight_data(p_language, p_source, p_use_cache); + + if (!highlight_data.is_empty()) { + for (int i = 1; i < highlight_data.size(); i++) { + const Pair<int, Color> &prev = highlight_data[i - 1]; + const Pair<int, Color> &curr = highlight_data[i]; + p_rich_text_label->push_color(prev.second); + p_rich_text_label->add_text(p_source.substr(prev.first, curr.first - prev.first)); + p_rich_text_label->pop(); // color + } + + const Pair<int, Color> &last = highlight_data[highlight_data.size() - 1]; + p_rich_text_label->push_color(last.second); + p_rich_text_label->add_text(p_source.substr(last.first)); + p_rich_text_label->pop(); // color + } +} + +void EditorHelpHighlighter::reset_cache() { + const Color text_color = EDITOR_GET("text_editor/theme/highlighting/text_color"); + +#ifdef MODULE_GDSCRIPT_ENABLED + highlight_data_caches[LANGUAGE_GDSCRIPT].clear(); + text_edits[LANGUAGE_GDSCRIPT]->add_theme_color_override("font_color", text_color); +#endif + +#ifdef MODULE_MONO_ENABLED + highlight_data_caches[LANGUAGE_CSHARP].clear(); + text_edits[LANGUAGE_CSHARP]->add_theme_color_override("font_color", text_color); +#endif +} + +EditorHelpHighlighter::EditorHelpHighlighter() { +#ifdef MODULE_GDSCRIPT_ENABLED + TextEdit *gdscript_text_edit = memnew(TextEdit); + + Ref<GDScript> gdscript; + gdscript.instantiate(); + + Ref<GDScriptSyntaxHighlighter> gdscript_highlighter; + gdscript_highlighter.instantiate(); + gdscript_highlighter->set_text_edit(gdscript_text_edit); + gdscript_highlighter->_set_edited_resource(gdscript); + + text_edits[LANGUAGE_GDSCRIPT] = gdscript_text_edit; + scripts[LANGUAGE_GDSCRIPT] = gdscript; + highlighters[LANGUAGE_GDSCRIPT] = gdscript_highlighter; +#endif + +#ifdef MODULE_MONO_ENABLED + TextEdit *csharp_text_edit = memnew(TextEdit); + + Ref<CSharpScript> csharp; + csharp.instantiate(); + + Ref<EditorStandardSyntaxHighlighter> csharp_highlighter; + csharp_highlighter.instantiate(); + csharp_highlighter->set_text_edit(csharp_text_edit); + csharp_highlighter->_set_edited_resource(csharp); + + text_edits[LANGUAGE_CSHARP] = csharp_text_edit; + scripts[LANGUAGE_CSHARP] = csharp; + highlighters[LANGUAGE_CSHARP] = csharp_highlighter; +#endif +} + +EditorHelpHighlighter::~EditorHelpHighlighter() { +#ifdef MODULE_GDSCRIPT_ENABLED + memdelete(text_edits[LANGUAGE_GDSCRIPT]); + scripts[LANGUAGE_GDSCRIPT].unref(); + highlighters[LANGUAGE_GDSCRIPT].unref(); +#endif + +#ifdef MODULE_MONO_ENABLED + memdelete(text_edits[LANGUAGE_CSHARP]); + scripts[LANGUAGE_CSHARP].unref(); + highlighters[LANGUAGE_CSHARP].unref(); +#endif +} + +#endif // defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) + /// FindBar /// FindBar::FindBar() { diff --git a/editor/editor_help.h b/editor/editor_help.h index 5018f6570d..f8686b964a 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -44,6 +44,8 @@ #include "scene/gui/text_edit.h" #include "scene/main/timer.h" +#include "modules/modules_enabled.gen.h" // For gdscript, mono. + class FindBar : public HBoxContainer { GDCLASS(FindBar, HBoxContainer); @@ -265,6 +267,7 @@ class EditorHelpBit : public MarginContainer { String text; protected: + String doc_class_name; String custom_description; static void _bind_methods(); @@ -297,4 +300,41 @@ public: EditorHelpTooltip(const String &p_text = String(), const String &p_custom_description = String()); }; +#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) +class EditorSyntaxHighlighter; + +class EditorHelpHighlighter { +public: + enum Language { + LANGUAGE_GDSCRIPT, + LANGUAGE_CSHARP, + LANGUAGE_MAX, + }; + +private: + using HighlightData = Vector<Pair<int, Color>>; + + static EditorHelpHighlighter *singleton; + + HashMap<String, HighlightData> highlight_data_caches[LANGUAGE_MAX]; + + TextEdit *text_edits[LANGUAGE_MAX]; + Ref<Script> scripts[LANGUAGE_MAX]; + Ref<EditorSyntaxHighlighter> highlighters[LANGUAGE_MAX]; + + HighlightData _get_highlight_data(Language p_language, const String &p_source, bool p_use_cache); + +public: + static void create_singleton(); + static void free_singleton(); + static EditorHelpHighlighter *get_singleton(); + + void highlight(RichTextLabel *p_rich_text_label, Language p_language, const String &p_source, bool p_use_cache); + void reset_cache(); + + EditorHelpHighlighter(); + virtual ~EditorHelpHighlighter(); +}; +#endif // defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) + #endif // EDITOR_HELP_H diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 83a7e115c9..a14c6e8462 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -2009,6 +2009,10 @@ Array EditorInspectorArray::_extract_properties_as_array(const List<PropertyInfo Array output; for (const PropertyInfo &pi : p_list) { + if (!(pi.usage & PROPERTY_USAGE_EDITOR)) { + continue; + } + if (pi.name.begins_with(array_element_prefix)) { String str = pi.name.trim_prefix(array_element_prefix); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index e1b69def14..b8d87cfe9f 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -165,6 +165,8 @@ #include <stdio.h> #include <stdlib.h> +#include "modules/modules_enabled.gen.h" // For gdscript, mono. + EditorNode *EditorNode::singleton = nullptr; static const String EDITOR_NODE_CONFIG_SECTION = "EditorNode"; @@ -576,6 +578,9 @@ void EditorNode::_notification(int p_what) { switch (p_what) { case NOTIFICATION_POSTINITIALIZE: { EditorHelp::generate_doc(); +#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) + EditorHelpHighlighter::create_singleton(); +#endif } break; case NOTIFICATION_PROCESS: { @@ -801,6 +806,12 @@ void EditorNode::_notification(int p_what) { _update_update_spinner(); _update_vsync_mode(); } + +#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) + if (EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/theme/highlighting")) { + EditorHelpHighlighter::get_singleton()->reset_cache(); + } +#endif } break; } } @@ -7420,6 +7431,9 @@ EditorNode::~EditorNode() { remove_print_handler(&print_handler); EditorHelp::cleanup_doc(); +#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) + EditorHelpHighlighter::free_singleton(); +#endif memdelete(editor_selection); memdelete(editor_plugins_over); memdelete(editor_plugins_force_over); diff --git a/editor/editor_resource_preview.cpp b/editor/editor_resource_preview.cpp index 94bf15ae66..ddf230dfdb 100644 --- a/editor/editor_resource_preview.cpp +++ b/editor/editor_resource_preview.cpp @@ -269,7 +269,7 @@ void EditorResourcePreview::_iterate() { if (item.resource.is_valid()) { Dictionary preview_metadata; _generate_preview(texture, small_texture, item, String(), preview_metadata); - _preview_ready(item.path, item.resource->hash_edited_version(), texture, small_texture, item.id, item.function, item.userdata, preview_metadata); + _preview_ready(item.path, item.resource->hash_edited_version_for_preview(), texture, small_texture, item.id, item.function, item.userdata, preview_metadata); return; } @@ -407,7 +407,7 @@ void EditorResourcePreview::queue_edited_resource_preview(const Ref<Resource> &p String path_id = "ID:" + itos(p_res->get_instance_id()); - if (cache.has(path_id) && cache[path_id].last_hash == p_res->hash_edited_version()) { + if (cache.has(path_id) && cache[path_id].last_hash == p_res->hash_edited_version_for_preview()) { cache[path_id].order = order++; p_receiver->call(p_receiver_func, path_id, cache[path_id].preview, cache[path_id].small_preview, p_userdata); return; diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index d0b633b136..29652c04b5 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -838,6 +838,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { // TRANSLATORS: Project Manager here refers to the tool used to create/manage Godot projects. EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "project_manager/sorting_order", 0, "Last Edited,Name,Path") + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "project_manager/directory_naming_convention", 1, "No convention,kebab-case,snake_case,camelCase,PascalCase,Title Case") #if defined(WEB_ENABLED) // Web platform only supports `gl_compatibility`. diff --git a/editor/find_in_files.cpp b/editor/find_in_files.cpp index 6d690cf31b..965cb39df3 100644 --- a/editor/find_in_files.cpp +++ b/editor/find_in_files.cpp @@ -398,8 +398,24 @@ FindInFilesDialog::FindInFilesDialog() { } void FindInFilesDialog::set_search_text(const String &text) { - _search_text_line_edit->set_text(text); - _on_search_text_modified(text); + if (_mode == SEARCH_MODE) { + if (!text.is_empty()) { + _search_text_line_edit->set_text(text); + _on_search_text_modified(text); + } + callable_mp((Control *)_search_text_line_edit, &Control::grab_focus).call_deferred(); + _search_text_line_edit->select_all(); + } else if (_mode == REPLACE_MODE) { + if (!text.is_empty()) { + _search_text_line_edit->set_text(text); + callable_mp((Control *)_replace_text_line_edit, &Control::grab_focus).call_deferred(); + _replace_text_line_edit->select_all(); + _on_search_text_modified(text); + } else { + callable_mp((Control *)_search_text_line_edit, &Control::grab_focus).call_deferred(); + _search_text_line_edit->select_all(); + } + } } void FindInFilesDialog::set_replace_text(const String &text) { @@ -464,9 +480,6 @@ void FindInFilesDialog::_notification(int p_what) { switch (p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { if (is_visible()) { - // Doesn't work more than once if not deferred... - callable_mp((Control *)_search_text_line_edit, &Control::grab_focus).call_deferred(); - _search_text_line_edit->select_all(); // Extensions might have changed in the meantime, we clean them and instance them again. for (int i = 0; i < _filters_container->get_child_count(); i++) { _filters_container->get_child(i)->queue_free(); diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index c74d9fc3d4..248678662c 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -103,15 +103,12 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i undo_redo->commit_action(); } else if (p_id == BUTTON_LOCK) { undo_redo->create_action(TTR("Unlock Node")); - - if (n->is_class("CanvasItem") || n->is_class("Node3D")) { - undo_redo->add_do_method(n, "remove_meta", "_edit_lock_"); - undo_redo->add_undo_method(n, "set_meta", "_edit_lock_", true); - undo_redo->add_do_method(this, "_update_tree"); - undo_redo->add_undo_method(this, "_update_tree"); - undo_redo->add_do_method(this, "emit_signal", "node_changed"); - undo_redo->add_undo_method(this, "emit_signal", "node_changed"); - } + undo_redo->add_do_method(n, "remove_meta", "_edit_lock_"); + undo_redo->add_undo_method(n, "set_meta", "_edit_lock_", true); + undo_redo->add_do_method(this, "_update_tree"); + undo_redo->add_undo_method(this, "_update_tree"); + undo_redo->add_do_method(this, "emit_signal", "node_changed"); + undo_redo->add_undo_method(this, "emit_signal", "node_changed"); undo_redo->commit_action(); } else if (p_id == BUTTON_PIN) { if (n->is_class("AnimationMixer")) { @@ -400,60 +397,28 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { item->set_button_color(0, item->get_button_count(0) - 1, button_color); } - if (p_node->is_class("CanvasItem")) { - if (p_node->has_meta("_edit_lock_")) { - item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it.")); - } - - if (p_node->has_meta("_edit_group_")) { - item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable.")); - } - - bool v = p_node->call("is_visible"); - if (v) { + if (p_node->has_method("is_visible") && p_node->has_method("set_visible") && p_node->has_signal("visibility_changed")) { + bool is_visible = p_node->call("is_visible"); + if (is_visible) { item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility")); } else { item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility")); } - - if (!p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) { - p_node->connect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed).bind(p_node)); + const Callable vis_changed = callable_mp(this, &SceneTreeEditor::_node_visibility_changed); + if (!p_node->is_connected("visibility_changed", vis_changed)) { + p_node->connect("visibility_changed", vis_changed.bind(p_node)); } - _update_visibility_color(p_node, item); - } else if (p_node->is_class("CanvasLayer") || p_node->is_class("Window")) { - bool v = p_node->call("is_visible"); - if (v) { - item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility")); - } else { - item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility")); - } - - if (!p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) { - p_node->connect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed).bind(p_node)); - } - } else if (p_node->is_class("Node3D")) { - if (p_node->has_meta("_edit_lock_")) { - item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it.")); - } - - if (p_node->has_meta("_edit_group_")) { - item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable.")); - } - - bool v = p_node->call("is_visible"); - if (v) { - item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility")); - } else { - item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility")); - } + } - if (!p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) { - p_node->connect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed).bind(p_node)); - } + if (p_node->has_meta("_edit_lock_")) { + item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it.")); + } + if (p_node->has_meta("_edit_group_")) { + item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable.")); + } - _update_visibility_color(p_node, item); - } else if (p_node->is_class("AnimationMixer")) { + if (p_node->is_class("AnimationMixer")) { bool is_pinned = AnimationPlayerEditor::get_singleton()->get_editing_node() == p_node && AnimationPlayerEditor::get_singleton()->is_pinned(); if (is_pinned) { @@ -564,11 +529,11 @@ void SceneTreeEditor::_node_visibility_changed(Node *p_node) { bool node_visible = false; - if (p_node->is_class("CanvasItem") || p_node->is_class("CanvasLayer") || p_node->is_class("Window")) { - node_visible = p_node->call("is_visible"); - CanvasItemEditor::get_singleton()->get_viewport_control()->queue_redraw(); - } else if (p_node->is_class("Node3D")) { + if (p_node->has_method("is_visible")) { node_visible = p_node->call("is_visible"); + if (p_node->is_class("CanvasItem") || p_node->is_class("CanvasLayer") || p_node->is_class("Window")) { + CanvasItemEditor::get_singleton()->get_viewport_control()->queue_redraw(); + } } if (node_visible) { @@ -581,7 +546,7 @@ void SceneTreeEditor::_node_visibility_changed(Node *p_node) { } void SceneTreeEditor::_update_visibility_color(Node *p_node, TreeItem *p_item) { - if (p_node->is_class("CanvasItem") || p_node->is_class("Node3D")) { + if (p_node->has_method("is_visible_in_tree")) { Color color(1, 1, 1, 1); bool visible_on_screen = p_node->call("is_visible_in_tree"); if (!visible_on_screen) { @@ -615,7 +580,7 @@ void SceneTreeEditor::_node_removed(Node *p_node) { p_node->disconnect("script_changed", callable_mp(this, &SceneTreeEditor::_node_script_changed)); } - if (p_node->is_class("Node3D") || p_node->is_class("CanvasItem") || p_node->is_class("CanvasLayer") || p_node->is_class("Window")) { + if (p_node->has_signal("visibility_changed")) { if (p_node->is_connected("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) { p_node->disconnect("visibility_changed", callable_mp(this, &SceneTreeEditor::_node_visibility_changed)); } diff --git a/editor/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp index f0de608cf5..721eccdfdd 100644 --- a/editor/import/3d/scene_import_settings.cpp +++ b/editor/import/3d/scene_import_settings.cpp @@ -443,24 +443,27 @@ void SceneImportSettingsDialog::_update_view_gizmos() { return; } for (const KeyValue<String, NodeData> &e : node_map) { + // Skip import nodes that aren't MeshInstance3D. + const MeshInstance3D *mesh_node = Object::cast_to<MeshInstance3D>(e.value.node); + if (mesh_node == nullptr || mesh_node->get_mesh().is_null()) { + continue; + } + + // Determine if the mesh collider should be visible. bool show_collider_view = false; if (e.value.settings.has(SNAME("generate/physics"))) { show_collider_view = e.value.settings[SNAME("generate/physics")]; } - MeshInstance3D *mesh_node = Object::cast_to<MeshInstance3D>(e.value.node); - if (mesh_node == nullptr || mesh_node->get_mesh().is_null()) { - // Nothing to do. - continue; - } - + // Get the collider_view MeshInstance3D. TypedArray<Node> descendants = mesh_node->find_children("collider_view", "MeshInstance3D"); - CRASH_COND_MSG(descendants.is_empty(), "This is unreachable, since the collider view is always created even when the collision is not used! If this is triggered there is a bug on the function `_fill_scene`."); + MeshInstance3D *collider_view = Object::cast_to<MeshInstance3D>(descendants[0].operator Object *()); - MeshInstance3D *collider_view = static_cast<MeshInstance3D *>(descendants[0].operator Object *()); - collider_view->set_visible(show_collider_view); - if (generate_collider) { + // Regenerate the physics collider for this MeshInstance3D if either: + // - A regeneration is requested for the selected import node. + // - The collider is being made visible. + if ((generate_collider && e.key == selected_id) || (show_collider_view && !collider_view->is_visible())) { // This collider_view doesn't have a mesh so we need to generate a new one. Ref<ImporterMesh> mesh; mesh.instantiate(); @@ -524,6 +527,9 @@ void SceneImportSettingsDialog::_update_view_gizmos() { collider_view->set_mesh(collider_view_mesh); collider_view->set_transform(transform); } + + // Set the collider visibility. + collider_view->set_visible(show_collider_view); } generate_collider = false; diff --git a/editor/plugins/abstract_polygon_2d_editor.cpp b/editor/plugins/abstract_polygon_2d_editor.cpp index e7a1d2735e..dd52375c10 100644 --- a/editor/plugins/abstract_polygon_2d_editor.cpp +++ b/editor/plugins/abstract_polygon_2d_editor.cpp @@ -291,6 +291,7 @@ bool AbstractPolygon2DEditor::forward_gui_input(const Ref<InputEvent> &p_event) const PosVertex closest = closest_point(gpoint); if (closest.valid()) { + original_mouse_pos = gpoint; pre_move_edit = _get_polygon(closest.polygon); edited_point = PosVertex(closest, xform.affine_inverse().xform(closest.pos)); selected_point = closest; @@ -327,15 +328,15 @@ bool AbstractPolygon2DEditor::forward_gui_input(const Ref<InputEvent> &p_event) } } else { if (edited_point.valid()) { - //apply - - Vector<Vector2> vertices = _get_polygon(edited_point.polygon); - ERR_FAIL_INDEX_V(edited_point.vertex, vertices.size(), false); - vertices.write[edited_point.vertex] = edited_point.pos - _get_offset(edited_point.polygon); - - undo_redo->create_action(TTR("Edit Polygon")); - _action_set_polygon(edited_point.polygon, pre_move_edit, vertices); - _commit_action(); + if (original_mouse_pos != gpoint) { + Vector<Vector2> vertices = _get_polygon(edited_point.polygon); + ERR_FAIL_INDEX_V(edited_point.vertex, vertices.size(), false); + vertices.write[edited_point.vertex] = edited_point.pos - _get_offset(edited_point.polygon); + + undo_redo->create_action(TTR("Edit Polygon")); + _action_set_polygon(edited_point.polygon, pre_move_edit, vertices); + _commit_action(); + } edited_point = PosVertex(); return true; diff --git a/editor/plugins/abstract_polygon_2d_editor.h b/editor/plugins/abstract_polygon_2d_editor.h index 25b2e2603e..70cccebf40 100644 --- a/editor/plugins/abstract_polygon_2d_editor.h +++ b/editor/plugins/abstract_polygon_2d_editor.h @@ -79,6 +79,7 @@ class AbstractPolygon2DEditor : public HBoxContainer { Vertex hover_point; // point under mouse cursor Vertex selected_point; // currently selected PosVertex edge_point; // adding an edge point? + Vector2 original_mouse_pos; Vector<Vector2> pre_move_edit; Vector<Vector2> wip; diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 2ab53effed..894eef74ec 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -3911,16 +3911,19 @@ void CanvasItemEditor::_update_editor_settings() { warped_panning = bool(EDITOR_GET("editors/panning/warped_mouse_panning")); } +void CanvasItemEditor::_project_settings_changed() { + EditorNode::get_singleton()->get_scene_root()->set_snap_controls_to_pixels(GLOBAL_GET("gui/common/snap_controls_to_pixels")); +} + void CanvasItemEditor::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { EditorRunBar::get_singleton()->connect("play_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(true)); EditorRunBar::get_singleton()->connect("stop_pressed", callable_mp(this, &CanvasItemEditor::_update_override_camera_button).bind(false)); + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &CanvasItemEditor::_project_settings_changed)); } break; case NOTIFICATION_PROCESS: { - EditorNode::get_singleton()->get_scene_root()->set_snap_controls_to_pixels(GLOBAL_GET("gui/common/snap_controls_to_pixels")); - int nb_having_pivot = 0; // Update the viewport if the canvas_item changes @@ -5695,8 +5698,11 @@ void CanvasItemEditorViewport::_create_preview(const Vector<String> &files) cons } void CanvasItemEditorViewport::_remove_preview() { - if (preview_node->get_parent()) { + if (!canvas_item_editor->message.is_empty()) { canvas_item_editor->message = ""; + canvas_item_editor->update_viewport(); + } + if (preview_node->get_parent()) { for (int i = preview_node->get_child_count() - 1; i >= 0; i--) { Node *node = preview_node->get_child(i); node->queue_free(); @@ -5709,7 +5715,7 @@ void CanvasItemEditorViewport::_remove_preview() { } } -bool CanvasItemEditorViewport::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) { +bool CanvasItemEditorViewport::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const { if (p_desired_node->get_scene_file_path() == p_target_scene_path) { return true; } @@ -5853,7 +5859,7 @@ void CanvasItemEditorViewport::_perform_drop_data() { return; } - Vector<String> error_files; + PackedStringArray error_files; EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action_for_history(TTR("Create Node"), EditorNode::get_editor_data().get_current_edited_scene_history_id()); @@ -5872,12 +5878,12 @@ void CanvasItemEditorViewport::_perform_drop_data() { // Without root node act the same as "Load Inherited Scene" Error err = EditorNode::get_singleton()->load_scene(path, false, true); if (err != OK) { - error_files.push_back(path); + error_files.push_back(path.get_file()); } } else { bool success = _create_instance(target_node, path, drop_pos); if (!success) { - error_files.push_back(path); + error_files.push_back(path.get_file()); } } } else { @@ -5893,12 +5899,7 @@ void CanvasItemEditorViewport::_perform_drop_data() { undo_redo->commit_action(); if (error_files.size() > 0) { - String files_str; - for (int i = 0; i < error_files.size(); i++) { - files_str += error_files[i].get_file().get_basename() + ","; - } - files_str = files_str.substr(0, files_str.length() - 1); - accept->set_text(vformat(TTR("Error instantiating scene from %s"), files_str.get_data())); + accept->set_text(vformat(TTR("Error instantiating scene from %s."), String(", ").join(error_files))); accept->popup_centered(); } } @@ -5912,6 +5913,8 @@ bool CanvasItemEditorViewport::can_drop_data(const Point2 &p_point, const Varian Vector<String> files = d["files"]; bool can_instantiate = false; + bool is_cyclical_dep = false; + String error_file; // Check if at least one of the dragged files is a texture or scene. for (int i = 0; i < files.size(); i++) { @@ -5929,12 +5932,25 @@ bool CanvasItemEditorViewport::can_drop_data(const Point2 &p_point, const Varian if (!instantiated_scene) { continue; } + + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + if (_cyclical_dependency_exists(edited_scene->get_scene_file_path(), instantiated_scene)) { + memdelete(instantiated_scene); + can_instantiate = false; + is_cyclical_dep = true; + error_file = files[i].get_file(); + break; + } memdelete(instantiated_scene); } can_instantiate = true; - break; } } + if (is_cyclical_dep) { + canvas_item_editor->message = vformat(TTR("Can't instantiate: %s."), vformat(TTR("Circular dependency found at %s"), error_file)); + canvas_item_editor->update_viewport(); + return false; + } if (can_instantiate) { if (!preview_node->get_parent()) { // create preview only once _create_preview(files); diff --git a/editor/plugins/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h index 723dbc7f59..fd6622bad6 100644 --- a/editor/plugins/canvas_item_editor_plugin.h +++ b/editor/plugins/canvas_item_editor_plugin.h @@ -483,6 +483,8 @@ private: void _focus_selection(int p_op); void _reset_drag(); + void _project_settings_changed(); + SnapTarget snap_target[2]; Transform2D snap_transform; void _snap_if_closer_float( @@ -645,7 +647,7 @@ class CanvasItemEditorViewport : public Control { void _create_preview(const Vector<String> &files) const; void _remove_preview(); - bool _cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node); + bool _cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const; bool _only_packed_scenes_selected() const; void _create_nodes(Node *parent, Node *child, String &path, const Point2 &p_point); bool _create_instance(Node *parent, String &path, const Point2 &p_point); diff --git a/editor/plugins/cast_2d_editor_plugin.cpp b/editor/plugins/cast_2d_editor_plugin.cpp index 64db19d85e..c9d7ff8e08 100644 --- a/editor/plugins/cast_2d_editor_plugin.cpp +++ b/editor/plugins/cast_2d_editor_plugin.cpp @@ -65,10 +65,13 @@ bool Cast2DEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) { Vector2 target_position = node->get("target_position"); + Vector2 gpoint = mb->get_position(); + if (mb->is_pressed()) { - if (xform.xform(target_position).distance_to(mb->get_position()) < 8) { + if (xform.xform(target_position).distance_to(gpoint) < 8) { pressed = true; original_target_position = target_position; + original_mouse_pos = gpoint; return true; } else { @@ -77,16 +80,17 @@ bool Cast2DEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_event) { return false; } } else if (pressed) { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Set Target Position")); - undo_redo->add_do_property(node, "target_position", target_position); - undo_redo->add_do_method(canvas_item_editor, "update_viewport"); - undo_redo->add_undo_property(node, "target_position", original_target_position); - undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); - undo_redo->commit_action(); + if (original_mouse_pos != gpoint) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Set Target Position")); + undo_redo->add_do_property(node, "target_position", target_position); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_property(node, "target_position", original_target_position); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + } pressed = false; - return true; } } diff --git a/editor/plugins/cast_2d_editor_plugin.h b/editor/plugins/cast_2d_editor_plugin.h index ad48d6a524..36b302f311 100644 --- a/editor/plugins/cast_2d_editor_plugin.h +++ b/editor/plugins/cast_2d_editor_plugin.h @@ -44,6 +44,7 @@ class Cast2DEditor : public Control { bool pressed = false; Point2 original_target_position; + Vector2 original_mouse_pos; protected: void _notification(int p_what); diff --git a/editor/plugins/collision_shape_2d_editor_plugin.cpp b/editor/plugins/collision_shape_2d_editor_plugin.cpp index 12dde46193..258396ff31 100644 --- a/editor/plugins/collision_shape_2d_editor_plugin.cpp +++ b/editor/plugins/collision_shape_2d_editor_plugin.cpp @@ -326,6 +326,7 @@ bool CollisionShape2DEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_e return false; } + original_mouse_pos = gpoint; original_point = handles[edit_handle]; original = get_handle_value(edit_handle); original_transform = node->get_global_transform(); @@ -336,7 +337,9 @@ bool CollisionShape2DEditor::forward_canvas_gui_input(const Ref<InputEvent> &p_e } else { if (pressed) { - commit_handle(edit_handle, original); + if (original_mouse_pos != gpoint) { + commit_handle(edit_handle, original); + } edit_handle = -1; pressed = false; diff --git a/editor/plugins/collision_shape_2d_editor_plugin.h b/editor/plugins/collision_shape_2d_editor_plugin.h index 0e454799d6..19b2de3821 100644 --- a/editor/plugins/collision_shape_2d_editor_plugin.h +++ b/editor/plugins/collision_shape_2d_editor_plugin.h @@ -74,6 +74,7 @@ class CollisionShape2DEditor : public Control { Transform2D original_transform; Vector2 original_point; Point2 last_point; + Vector2 original_mouse_pos; Ref<Shape2D> current_shape; diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index f876d9ebd1..f38d42f681 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -1888,7 +1888,9 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { surface->queue_redraw(); } else { if (_edit.gizmo.is_valid()) { - _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, false); + if (_edit.original_mouse_pos != _edit.mouse_pos) { + _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, false); + } _edit.gizmo = Ref<EditorNode3DGizmo>(); break; } @@ -1922,7 +1924,9 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { se->gizmo->commit_subgizmos(ids, restore, false); } else { - commit_transform(); + if (_edit.original_mouse_pos != _edit.mouse_pos) { + commit_transform(); + } } _edit.mode = TRANSFORM_NONE; set_message(""); @@ -4326,7 +4330,7 @@ void Node3DEditorViewport::_remove_preview_material() { spatial_editor->set_preview_material_surface(-1); } -bool Node3DEditorViewport::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) { +bool Node3DEditorViewport::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const { if (p_desired_node->get_scene_file_path() == p_target_scene_path) { return true; } @@ -4437,7 +4441,7 @@ void Node3DEditorViewport::_perform_drop_data() { _remove_preview_node(); - Vector<String> error_files; + PackedStringArray error_files; undo_redo->create_action(TTR("Create Node"), UndoRedo::MERGE_DISABLE, target_node); undo_redo->add_do_method(editor_selection, "clear"); @@ -4453,7 +4457,7 @@ void Node3DEditorViewport::_perform_drop_data() { if (mesh != nullptr || scene != nullptr) { bool success = _create_instance(target_node, path, drop_pos); if (!success) { - error_files.push_back(path); + error_files.push_back(path.get_file()); } } } @@ -4461,12 +4465,7 @@ void Node3DEditorViewport::_perform_drop_data() { undo_redo->commit_action(); if (error_files.size() > 0) { - String files_str; - for (int i = 0; i < error_files.size(); i++) { - files_str += error_files[i].get_file().get_basename() + ","; - } - files_str = files_str.substr(0, files_str.length() - 1); - accept->set_text(vformat(TTR("Error instantiating scene from %s"), files_str.get_data())); + accept->set_text(vformat(TTR("Error instantiating scene from %s."), String(", ").join(error_files))); accept->popup_centered(); } } @@ -4475,12 +4474,17 @@ bool Node3DEditorViewport::can_drop_data_fw(const Point2 &p_point, const Variant preview_node_viewport_pos = p_point; bool can_instantiate = false; + bool is_cyclical_dep = false; + String error_file; if (!preview_node->is_inside_tree() && spatial_editor->get_preview_material().is_null()) { Dictionary d = p_data; if (d.has("type") && (String(d["type"]) == "files")) { Vector<String> files = d["files"]; + // Track whether a type other than PackedScene is valid to stop checking them and only + // continue to check if the rest of the scenes are valid (don't have cyclic dependencies). + bool is_other_valid = false; // Check if at least one of the dragged files is a mesh, material, texture or scene. for (int i = 0; i < files.size(); i++) { bool is_scene = ClassDB::is_parent_class(ResourceLoader::get_resource_type(files[i]), "PackedScene"); @@ -4502,30 +4506,40 @@ bool Node3DEditorViewport::can_drop_data_fw(const Point2 &p_point, const Variant if (!instantiated_scene) { continue; } + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + if (_cyclical_dependency_exists(edited_scene->get_scene_file_path(), instantiated_scene)) { + memdelete(instantiated_scene); + can_instantiate = false; + is_cyclical_dep = true; + error_file = files[i].get_file(); + break; + } memdelete(instantiated_scene); - } else if (mat.is_valid()) { + } else if (!is_other_valid && mat.is_valid()) { Ref<BaseMaterial3D> base_mat = res; Ref<ShaderMaterial> shader_mat = res; if (base_mat.is_null() && !shader_mat.is_null()) { - break; + continue; } spatial_editor->set_preview_material(mat); - break; - } else if (mesh.is_valid()) { + is_other_valid = true; + continue; + } else if (!is_other_valid && mesh.is_valid()) { // Let the mesh pass. - } else if (tex.is_valid()) { + is_other_valid = true; + } else if (!is_other_valid && tex.is_valid()) { Ref<StandardMaterial3D> new_mat = memnew(StandardMaterial3D); new_mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, tex); spatial_editor->set_preview_material(new_mat); - break; + is_other_valid = true; + continue; } else { continue; } can_instantiate = true; - break; } } if (can_instantiate) { @@ -4539,6 +4553,11 @@ bool Node3DEditorViewport::can_drop_data_fw(const Point2 &p_point, const Variant } } + if (is_cyclical_dep) { + set_message(vformat(TTR("Can't instantiate: %s."), vformat(TTR("Circular dependency found at %s"), error_file))); + return false; + } + if (can_instantiate) { update_preview_node = true; return true; diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index 13b51289a9..091432b35a 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -444,7 +444,7 @@ private: bool _apply_preview_material(ObjectID p_target, const Point2 &p_point) const; void _reset_preview_material() const; void _remove_preview_material(); - bool _cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node); + bool _cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const; bool _create_instance(Node *parent, String &path, const Point2 &p_point); void _perform_drop_data(); diff --git a/editor/plugins/packed_scene_translation_parser_plugin.cpp b/editor/plugins/packed_scene_translation_parser_plugin.cpp index 04d4c6d779..86df57c469 100644 --- a/editor/plugins/packed_scene_translation_parser_plugin.cpp +++ b/editor/plugins/packed_scene_translation_parser_plugin.cpp @@ -58,6 +58,15 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, String node_type = state->get_node_type(i); String parent_path = state->get_node_path(i, true); + // Handle instanced scenes. + if (node_type.is_empty()) { + Ref<PackedScene> instance = state->get_node_instance(i); + if (instance.is_valid()) { + Ref<SceneState> _state = instance->get_state(); + node_type = _state->get_node_type(0); + } + } + // Find the `auto_translate_mode` property. bool auto_translating = true; bool auto_translate_mode_found = false; @@ -118,7 +127,8 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, for (int j = 0; j < state->get_node_property_count(i); j++) { String property_name = state->get_node_property_name(i, j); - if (!lookup_properties.has(property_name) || (exception_list.has(node_type) && exception_list[node_type].has(property_name))) { + + if (!match_property(property_name, node_type)) { continue; } @@ -134,15 +144,6 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, parsed_strings.append_array(temp); r_ids_ctx_plural->append_array(ids_context_plural); } - } else if ((node_type == "MenuButton" || node_type == "OptionButton") && property_name == "items") { - Vector<String> str_values = property_value; - int incr_value = node_type == "MenuButton" ? PopupMenu::ITEM_PROPERTY_SIZE : OptionButton::ITEM_PROPERTY_SIZE; - for (int k = 0; k < str_values.size(); k += incr_value) { - String desc = str_values[k].get_slice(";", 1).strip_edges(); - if (!desc.is_empty()) { - parsed_strings.push_back(desc); - } - } } else if (node_type == "FileDialog" && property_name == "filters") { // Extract FileDialog's filters property with values in format "*.png ; PNG Images","*.gd ; GDScript Files". Vector<String> str_values = property_value; @@ -167,14 +168,32 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, return OK; } +bool PackedSceneEditorTranslationParserPlugin::match_property(const String &p_property_name, const String &p_node_type) { + for (const KeyValue<String, Vector<String>> &exception : exception_list) { + const String &exception_node_type = exception.key; + if (ClassDB::is_parent_class(p_node_type, exception_node_type)) { + const Vector<String> &exception_properties = exception.value; + for (const String &exception_property : exception_properties) { + if (p_property_name.match(exception_property)) { + return false; + } + } + } + } + for (const String &lookup_property : lookup_properties) { + if (p_property_name.match(lookup_property)) { + return true; + } + } + return false; +} + PackedSceneEditorTranslationParserPlugin::PackedSceneEditorTranslationParserPlugin() { // Scene Node's properties containing strings that will be fetched for translation. lookup_properties.insert("text"); - lookup_properties.insert("tooltip_text"); - lookup_properties.insert("placeholder_text"); - lookup_properties.insert("items"); + lookup_properties.insert("*_text"); + lookup_properties.insert("popup/*/text"); lookup_properties.insert("title"); - lookup_properties.insert("dialog_text"); lookup_properties.insert("filters"); lookup_properties.insert("script"); diff --git a/editor/plugins/packed_scene_translation_parser_plugin.h b/editor/plugins/packed_scene_translation_parser_plugin.h index d1a92ded32..0a5cc1c2a6 100644 --- a/editor/plugins/packed_scene_translation_parser_plugin.h +++ b/editor/plugins/packed_scene_translation_parser_plugin.h @@ -43,6 +43,7 @@ class PackedSceneEditorTranslationParserPlugin : public EditorTranslationParserP public: virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override; + bool match_property(const String &p_property_name, const String &p_node_type); virtual void get_recognized_extensions(List<String> *r_extensions) const override; PackedSceneEditorTranslationParserPlugin(); diff --git a/editor/plugins/path_2d_editor_plugin.cpp b/editor/plugins/path_2d_editor_plugin.cpp index a772dba6ae..cc9114db39 100644 --- a/editor/plugins/path_2d_editor_plugin.cpp +++ b/editor/plugins/path_2d_editor_plugin.cpp @@ -86,6 +86,8 @@ bool Path2DEditor::forward_gui_input(const Ref<InputEvent> &p_event) { if (mb->is_pressed() && action == ACTION_NONE) { Ref<Curve2D> curve = node->get_curve(); + original_mouse_pos = gpoint; + for (int i = 0; i < curve->get_point_count(); i++) { real_t dist_to_p = gpoint.distance_to(xform.xform(curve->get_point_position(i))); real_t dist_to_p_out = gpoint.distance_to(xform.xform(curve->get_point_position(i) + curve->get_point_out(i))); @@ -224,45 +226,48 @@ bool Path2DEditor::forward_gui_input(const Ref<InputEvent> &p_event) { case ACTION_MOVING_POINT: case ACTION_MOVING_NEW_POINT: { - if (action == ACTION_MOVING_POINT) { - undo_redo->create_action(TTR("Move Point in Curve")); - undo_redo->add_undo_method(curve.ptr(), "set_point_position", action_point, moving_from); + if (original_mouse_pos != gpoint) { + if (action == ACTION_MOVING_POINT) { + undo_redo->create_action(TTR("Move Point in Curve")); + undo_redo->add_undo_method(curve.ptr(), "set_point_position", action_point, moving_from); + } + undo_redo->add_do_method(curve.ptr(), "set_point_position", action_point, cpoint); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(false); } - undo_redo->add_do_method(curve.ptr(), "set_point_position", action_point, cpoint); - undo_redo->add_do_method(canvas_item_editor, "update_viewport"); - undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); - undo_redo->commit_action(false); - } break; case ACTION_MOVING_IN: { - undo_redo->create_action(TTR("Move In-Control in Curve")); - undo_redo->add_do_method(curve.ptr(), "set_point_in", action_point, new_pos); - undo_redo->add_undo_method(curve.ptr(), "set_point_in", action_point, moving_from); - - if (mirror_handle_angle) { - undo_redo->add_do_method(curve.ptr(), "set_point_out", action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_out_length)); - undo_redo->add_undo_method(curve.ptr(), "set_point_out", action_point, mirror_handle_length ? -moving_from : (-moving_from.normalized() * orig_out_length)); + if (original_mouse_pos != gpoint) { + undo_redo->create_action(TTR("Move In-Control in Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_in", action_point, new_pos); + undo_redo->add_undo_method(curve.ptr(), "set_point_in", action_point, moving_from); + + if (mirror_handle_angle) { + undo_redo->add_do_method(curve.ptr(), "set_point_out", action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_out_length)); + undo_redo->add_undo_method(curve.ptr(), "set_point_out", action_point, mirror_handle_length ? -moving_from : (-moving_from.normalized() * orig_out_length)); + } + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); } - undo_redo->add_do_method(canvas_item_editor, "update_viewport"); - undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); - undo_redo->commit_action(); - } break; case ACTION_MOVING_OUT: { - undo_redo->create_action(TTR("Move Out-Control in Curve")); - undo_redo->add_do_method(curve.ptr(), "set_point_out", action_point, new_pos); - undo_redo->add_undo_method(curve.ptr(), "set_point_out", action_point, moving_from); - - if (mirror_handle_angle) { - undo_redo->add_do_method(curve.ptr(), "set_point_in", action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_in_length)); - undo_redo->add_undo_method(curve.ptr(), "set_point_in", action_point, mirror_handle_length ? -moving_from : (-moving_from.normalized() * orig_in_length)); + if (original_mouse_pos != gpoint) { + undo_redo->create_action(TTR("Move Out-Control in Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_out", action_point, new_pos); + undo_redo->add_undo_method(curve.ptr(), "set_point_out", action_point, moving_from); + + if (mirror_handle_angle) { + undo_redo->add_do_method(curve.ptr(), "set_point_in", action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_in_length)); + undo_redo->add_undo_method(curve.ptr(), "set_point_in", action_point, mirror_handle_length ? -moving_from : (-moving_from.normalized() * orig_in_length)); + } + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); } - undo_redo->add_do_method(canvas_item_editor, "update_viewport"); - undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); - undo_redo->commit_action(); - } break; } diff --git a/editor/plugins/path_2d_editor_plugin.h b/editor/plugins/path_2d_editor_plugin.h index af9f307cc8..8efd651494 100644 --- a/editor/plugins/path_2d_editor_plugin.h +++ b/editor/plugins/path_2d_editor_plugin.h @@ -92,6 +92,7 @@ class Path2DEditor : public HBoxContainer { float orig_in_length = 0.0f; float orig_out_length = 0.0f; Vector2 edge_point; + Vector2 original_mouse_pos; void _mode_selected(int p_mode); void _handle_option_pressed(int p_option); diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 02e2a9f487..b6a4a14117 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -482,6 +482,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool if (p_just_update) { Link &link = links[p_id]; + link.visual_node = vsnode.ptr(); link.graph_element = node; link.preview_box = nullptr; link.preview_pos = -1; diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 4187bf5a32..b951e9453e 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -618,14 +618,15 @@ void ProjectManager::_new_project() { } void ProjectManager::_rename_project() { - const HashSet<String> &selected_list = project_list->get_selected_project_keys(); + const Vector<ProjectList::Item> &selected_list = project_list->get_selected_projects(); if (selected_list.size() == 0) { return; } - for (const String &E : selected_list) { - project_dialog->set_project_path(E); + for (const ProjectList::Item &E : selected_list) { + project_dialog->set_project_name(E.project_name); + project_dialog->set_project_path(E.path); project_dialog->set_mode(ProjectDialog::MODE_RENAME); project_dialog->show_dialog(); } diff --git a/editor/project_manager/project_dialog.cpp b/editor/project_manager/project_dialog.cpp index 3af9509bbe..350bb5bb9f 100644 --- a/editor/project_manager/project_dialog.cpp +++ b/editor/project_manager/project_dialog.cpp @@ -41,349 +41,382 @@ #include "editor/themes/editor_icons.h" #include "editor/themes/editor_scale.h" #include "scene/gui/check_box.h" +#include "scene/gui/check_button.h" #include "scene/gui/line_edit.h" #include "scene/gui/option_button.h" #include "scene/gui/separator.h" #include "scene/gui/texture_rect.h" -void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) { +void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType p_input_type) { msg->set_text(p_msg); - Ref<Texture2D> current_path_icon = status_rect->get_texture(); - Ref<Texture2D> current_install_icon = install_status_rect->get_texture(); - Ref<Texture2D> new_icon; + get_ok_button()->set_disabled(p_type == MESSAGE_ERROR); + Ref<Texture2D> new_icon; switch (p_type) { case MESSAGE_ERROR: { msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor))); - msg->set_modulate(Color(1, 1, 1, 1)); new_icon = get_editor_theme_icon(SNAME("StatusError")); - } break; case MESSAGE_WARNING: { msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); - msg->set_modulate(Color(1, 1, 1, 1)); new_icon = get_editor_theme_icon(SNAME("StatusWarning")); - } break; case MESSAGE_SUCCESS: { - msg->remove_theme_color_override("font_color"); - msg->set_modulate(Color(1, 1, 1, 0)); + msg->add_theme_color_override("font_color", get_theme_color(SNAME("success_color"), EditorStringName(Editor))); new_icon = get_editor_theme_icon(SNAME("StatusSuccess")); - } break; } - if (current_path_icon != new_icon && input_type == PROJECT_PATH) { - status_rect->set_texture(new_icon); - } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) { + if (p_input_type == PROJECT_PATH) { + project_status_rect->set_texture(new_icon); + } else if (p_input_type == INSTALL_PATH) { install_status_rect->set_texture(new_icon); } } static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) { - return p_path.ends_with(".zip") && p_d->file_exists(p_path); + return p_path.get_extension() == "zip" && p_d->file_exists(p_path); } -String ProjectDialog::_test_path() { +void ProjectDialog::_validate_path() { + _set_message("", MESSAGE_SUCCESS, PROJECT_PATH); + _set_message("", MESSAGE_SUCCESS, INSTALL_PATH); + + if (project_name->get_text().strip_edges().is_empty()) { + _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR); + return; + } + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - const String base_path = project_path->get_text(); - String valid_path, valid_install_path; - bool is_zip = false; - if (d->change_dir(base_path) == OK) { - valid_path = base_path; - } else if (is_zip_file(d, base_path)) { - valid_path = base_path; - is_zip = true; - } else if (d->change_dir(base_path.strip_edges()) == OK) { - valid_path = base_path.strip_edges(); - } else if (is_zip_file(d, base_path.strip_edges())) { - valid_path = base_path.strip_edges(); - is_zip = true; - } - - if (valid_path.is_empty()) { - _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - return ""; - } - - if (mode == MODE_IMPORT && is_zip) { - if (d->change_dir(install_path->get_text()) == OK) { - valid_install_path = install_path->get_text(); - } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) { - valid_install_path = install_path->get_text().strip_edges(); + String path = project_path->get_text().simplify_path(); + + String target_path = path; + InputType target_path_input_type = PROJECT_PATH; + + if (mode == MODE_IMPORT) { + if (path.get_file().strip_edges() == "project.godot") { + path = path.get_base_dir(); + project_path->set_text(path); } - if (valid_install_path.is_empty()) { - _set_message(TTR("The install path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH); - get_ok_button()->set_disabled(true); - return ""; + if (is_zip_file(d, path)) { + zip_path = path; + } else if (is_zip_file(d, path.strip_edges())) { + zip_path = path.strip_edges(); + } else { + zip_path = ""; } - } - if (mode == MODE_IMPORT || mode == MODE_RENAME) { - if (!d->file_exists("project.godot")) { - if (is_zip) { - Ref<FileAccess> io_fa; - zlib_filefunc_def io = zipio_create_io(&io_fa); + if (!zip_path.is_empty()) { + target_path = install_path->get_text().simplify_path(); + target_path_input_type = INSTALL_PATH; - unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io); - if (!pkg) { - _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - unzClose(pkg); - return ""; - } + create_dir->show(); + install_path_container->show(); - int ret = unzGoToFirstFile(pkg); - while (ret == UNZ_OK) { - unz_file_info info; - char fname[16384]; - ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - if (ret != UNZ_OK) { - break; - } + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); - if (String::utf8(fname).ends_with("project.godot")) { - break; - } + unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io); + if (!pkg) { + _set_message(TTR("Invalid \".zip\" project file; it is not in ZIP format."), MESSAGE_ERROR); + unzClose(pkg); + return; + } - ret = unzGoToNextFile(pkg); - } + int ret = unzGoToFirstFile(pkg); + while (ret == UNZ_OK) { + unz_file_info info; + char fname[16384]; + ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info."); - if (ret == UNZ_END_OF_LIST_OF_FILE) { - _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - unzClose(pkg); - return ""; + String name = String::utf8(fname); + if (name.get_file() == "project.godot") { + break; // ret == UNZ_OK. } + ret = unzGoToNextFile(pkg); + } + + if (ret == UNZ_END_OF_LIST_OF_FILE) { + _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR); unzClose(pkg); + return; + } - // check if the specified install folder is empty, even though this is not an error, it is good to check here - d->list_dir_begin(); - is_folder_empty = true; - String n = d->get_next(); - while (!n.is_empty()) { - if (!n.begins_with(".")) { - // Allow `.`, `..` (reserved current/parent folder names) - // and hidden files/folders to be present. - // For instance, this lets users initialize a Git repository - // and still be able to create a project in the directory afterwards. - is_folder_empty = false; - break; - } - n = d->get_next(); - } - d->list_dir_end(); + unzClose(pkg); + } else if (d->dir_exists(path) && d->file_exists(path.path_join("project.godot"))) { + zip_path = ""; - if (!is_folder_empty) { - _set_message(TTR("Please choose an empty install folder."), MESSAGE_WARNING, INSTALL_PATH); - get_ok_button()->set_disabled(true); - return ""; - } + create_dir->hide(); + install_path_container->hide(); - } else { - _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR); - install_path_container->hide(); - get_ok_button()->set_disabled(true); - return ""; - } + _set_message(TTR("Valid project found at path."), MESSAGE_SUCCESS); + } else { + create_dir->hide(); + install_path_container->hide(); - } else if (is_zip) { - _set_message(TTR("The install directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH); - get_ok_button()->set_disabled(true); - return ""; + _set_message(TTR("Please choose a \"project.godot\", a directory with one, or a \".zip\" file."), MESSAGE_ERROR); + return; } + } - } else { - // Check if the specified folder is empty, even though this is not an error, it is good to check here. - d->list_dir_begin(); - is_folder_empty = true; - String n = d->get_next(); - while (!n.is_empty()) { - if (!n.begins_with(".")) { - // Allow `.`, `..` (reserved current/parent folder names) - // and hidden files/folders to be present. - // For instance, this lets users initialize a Git repository - // and still be able to create a project in the directory afterwards. - is_folder_empty = false; - break; - } - n = d->get_next(); - } - d->list_dir_end(); + if (target_path.is_empty() || target_path.is_relative_path()) { + _set_message(TTR("The path specified is invalid."), MESSAGE_ERROR, target_path_input_type); + return; + } - if (!is_folder_empty) { - if (valid_path == OS::get_singleton()->get_environment("HOME") || valid_path == OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS) || valid_path == OS::get_singleton()->get_executable_path().get_base_dir()) { - _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - return ""; - } + if (target_path.get_file() != OS::get_singleton()->get_safe_dir_name(target_path.get_file())) { + _set_message(TTR("The directory name specified contains invalid characters or trailing whitespace."), MESSAGE_ERROR, target_path_input_type); + return; + } - _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING); - get_ok_button()->set_disabled(false); - return valid_path; - } + String working_dir = d->get_current_dir(); + String executable_dir = OS::get_singleton()->get_executable_path().get_base_dir(); + if (target_path == working_dir || target_path == executable_dir) { + _set_message(TTR("Creating a project at the engine's working directory or executable directory is not allowed, as it would prevent the project manager from starting."), MESSAGE_ERROR, target_path_input_type); + return; } - _set_message(""); - _set_message("", MESSAGE_SUCCESS, INSTALL_PATH); - get_ok_button()->set_disabled(false); - return valid_path; -} + // TODO: The following 5 lines could be simplified if OS.get_user_home_dir() or SYSTEM_DIR_HOME is implemented. See: https://github.com/godotengine/godot-proposals/issues/4851. +#ifdef WINDOWS_ENABLED + String home_dir = OS::get_singleton()->get_environment("USERPROFILE"); +#else + String home_dir = OS::get_singleton()->get_environment("HOME"); +#endif + String documents_dir = OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS); + if (target_path == home_dir || target_path == documents_dir) { + _set_message(TTR("You cannot save a project at the selected path. Please create a subfolder or choose a new path."), MESSAGE_ERROR, target_path_input_type); + return; + } -void ProjectDialog::_update_path(const String &p_path) { - String sp = _test_path(); - if (!sp.is_empty()) { - // If the project name is empty or default, infer the project name from the selected folder name - if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) { - sp = sp.replace("\\", "/"); - int lidx = sp.rfind("/"); + is_folder_empty = true; + if (mode == MODE_NEW || mode == MODE_INSTALL || (mode == MODE_IMPORT && target_path_input_type == InputType::INSTALL_PATH)) { + if (create_dir->is_pressed()) { + if (!d->dir_exists(target_path.get_base_dir())) { + _set_message(TTR("The parent directory of the path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type); + return; + } - if (lidx != -1) { - sp = sp.substr(lidx + 1, sp.length()).capitalize(); + if (d->dir_exists(target_path)) { + // The path is not necessarily empty here, but we will update the message later if it isn't. + _set_message(TTR("The project folder already exists and is empty."), MESSAGE_SUCCESS, target_path_input_type); + } else { + _set_message(TTR("The project folder will be automatically created."), MESSAGE_SUCCESS, target_path_input_type); + } + } else { + if (!d->dir_exists(target_path)) { + _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR, target_path_input_type); + return; } - if (sp.is_empty() && mode == MODE_IMPORT) { - sp = TTR("Imported Project"); + + // The path is not necessarily empty here, but we will update the message later if it isn't. + _set_message(TTR("The project folder exists and is empty."), MESSAGE_SUCCESS, target_path_input_type); + } + + // Check if the directory is empty. Not an error, but we want to warn the user. + if (d->change_dir(target_path) == OK) { + d->list_dir_begin(); + String n = d->get_next(); + while (!n.is_empty()) { + if (n[0] != '.') { + // Allow `.`, `..` (reserved current/parent folder names) + // and hidden files/folders to be present. + // For instance, this lets users initialize a Git repository + // and still be able to create a project in the directory afterwards. + is_folder_empty = false; + break; + } + n = d->get_next(); } + d->list_dir_end(); - project_name->set_text(sp); - _text_changed(sp); + if (!is_folder_empty) { + _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING, target_path_input_type); + } } } +} - if (!created_folder_path.is_empty() && created_folder_path != p_path) { - _remove_created_folder(); +String ProjectDialog::_get_target_path() { + if (mode == MODE_NEW || mode == MODE_INSTALL) { + return project_path->get_text(); + } else if (mode == MODE_IMPORT) { + return install_path->get_text(); + } else { + ERR_FAIL_V(""); } } - -void ProjectDialog::_path_text_changed(const String &p_path) { - Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (mode == MODE_IMPORT && is_zip_file(d, p_path)) { - install_path->set_text(p_path.get_base_dir()); - install_path_container->show(); - } else if (mode == MODE_IMPORT && is_zip_file(d, p_path.strip_edges())) { - install_path->set_text(p_path.strip_edges().get_base_dir()); - install_path_container->show(); +void ProjectDialog::_set_target_path(const String &p_text) { + if (mode == MODE_NEW || mode == MODE_INSTALL) { + project_path->set_text(p_text); + } else if (mode == MODE_IMPORT) { + install_path->set_text(p_text); } else { - install_path_container->hide(); + ERR_FAIL(); + } +} + +void ProjectDialog::_update_target_auto_dir() { + String new_auto_dir; + if (mode == MODE_NEW || mode == MODE_INSTALL) { + new_auto_dir = project_name->get_text(); + } else if (mode == MODE_IMPORT) { + new_auto_dir = project_path->get_text().get_file().get_basename(); + } + int naming_convention = (int)EDITOR_GET("project_manager/directory_naming_convention"); + switch (naming_convention) { + case 0: // No convention + break; + case 1: // kebab-case + new_auto_dir = new_auto_dir.to_lower().replace(" ", "-"); + break; + case 2: // snake_case + new_auto_dir = new_auto_dir.to_snake_case(); + break; + case 3: // camelCase + new_auto_dir = new_auto_dir.to_camel_case(); + break; + case 4: // PascalCase + new_auto_dir = new_auto_dir.to_pascal_case(); + break; + case 5: // Title Case + new_auto_dir = new_auto_dir.capitalize(); + break; + default: + ERR_FAIL_MSG("Invalid directory naming convention."); + break; + } + new_auto_dir = OS::get_singleton()->get_safe_dir_name(new_auto_dir); + + if (create_dir->is_pressed()) { + String target_path = _get_target_path(); + + if (target_path.get_file() == auto_dir) { + // Update target dir name to new project name / ZIP name. + target_path = target_path.get_base_dir().path_join(new_auto_dir); + } + + _set_target_path(target_path); } - _update_path(p_path.simplify_path()); + auto_dir = new_auto_dir; } -void ProjectDialog::_file_selected(const String &p_path) { - // If not already shown. - show_dialog(); +void ProjectDialog::_create_dir_toggled(bool p_pressed) { + String target_path = _get_target_path(); - String p = p_path; - if (mode == MODE_IMPORT) { - if (p.ends_with("project.godot")) { - p = p.get_base_dir(); - install_path_container->hide(); - get_ok_button()->set_disabled(false); - } else if (p.ends_with(".zip")) { - install_path->set_text(p.get_base_dir()); - install_path_container->show(); - get_ok_button()->set_disabled(false); + if (create_dir->is_pressed()) { + // (Re-)append target dir name. + if (last_custom_target_dir.is_empty()) { + target_path = target_path.path_join(auto_dir); } else { - _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR); - get_ok_button()->set_disabled(true); - return; + target_path = target_path.path_join(last_custom_target_dir); + } + } else { + // Save and remove target dir name. + if (target_path.get_file() == auto_dir) { + last_custom_target_dir = ""; + } else { + last_custom_target_dir = target_path.get_file(); } + target_path = target_path.get_base_dir(); } - String sp = p.simplify_path(); - project_path->set_text(sp); - _update_path(sp); - if (p.ends_with(".zip")) { - callable_mp((Control *)install_path, &Control::grab_focus).call_deferred(); - } else { - callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred(); + _set_target_path(target_path); + _validate_path(); +} + +void ProjectDialog::_project_name_changed() { + if (mode == MODE_NEW || mode == MODE_INSTALL) { + _update_target_auto_dir(); } + + _validate_path(); } -void ProjectDialog::_path_selected(const String &p_path) { - // If not already shown. - show_dialog(); +void ProjectDialog::_project_path_changed() { + if (mode == MODE_IMPORT) { + _update_target_auto_dir(); + } - String sp = p_path.simplify_path(); - project_path->set_text(sp); - _update_path(sp); - callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred(); + _validate_path(); } -void ProjectDialog::_install_path_selected(const String &p_path) { - String sp = p_path.simplify_path(); - install_path->set_text(sp); - _update_path(sp); - callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred(); +void ProjectDialog::_install_path_changed() { + _validate_path(); } -void ProjectDialog::_browse_path() { - fdialog->set_current_dir(project_path->get_text()); +void ProjectDialog::_browse_project_path() { + if (mode == MODE_IMPORT && install_path->is_visible_in_tree()) { + // Select last ZIP file. + fdialog_project->set_current_path(project_path->get_text()); + } else if ((mode == MODE_NEW || mode == MODE_INSTALL) && create_dir->is_pressed()) { + // Select parent directory of project path. + fdialog_project->set_current_dir(project_path->get_text().get_base_dir()); + } else { + // Select project path. + fdialog_project->set_current_dir(project_path->get_text()); + } if (mode == MODE_IMPORT) { - fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY); - fdialog->clear_filters(); - fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project"))); - fdialog->add_filter("*.zip", TTR("ZIP File")); + fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY); + fdialog_project->clear_filters(); + fdialog_project->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project"))); + fdialog_project->add_filter("*.zip", TTR("ZIP File")); } else { - fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); + fdialog_project->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); } - fdialog->popup_file_dialog(); + fdialog_project->popup_file_dialog(); } void ProjectDialog::_browse_install_path() { - fdialog_install->set_current_dir(install_path->get_text()); + ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT."); + + if (create_dir->is_pressed()) { + // Select parent directory of install path. + fdialog_install->set_current_dir(install_path->get_text().get_base_dir()); + } else { + // Select install path. + fdialog_install->set_current_dir(install_path->get_text()); + } + fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR); fdialog_install->popup_file_dialog(); } -void ProjectDialog::_create_folder() { - const String project_name_no_edges = project_name->get_text().strip_edges(); - if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) { - _set_message(TTR("Invalid project name."), MESSAGE_WARNING); - return; +void ProjectDialog::_project_path_selected(const String &p_path) { + if (create_dir->is_pressed() && (mode == MODE_NEW || mode == MODE_INSTALL)) { + // Replace parent directory, but keep target dir name. + project_path->set_text(p_path.path_join(project_path->get_text().get_file())); + } else { + project_path->set_text(p_path); } - Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (d->change_dir(project_path->get_text()) == OK) { - if (!d->dir_exists(project_name_no_edges)) { - if (d->make_dir(project_name_no_edges) == OK) { - d->change_dir(project_name_no_edges); - String dir_str = d->get_current_dir(); - project_path->set_text(dir_str); - _update_path(dir_str); - created_folder_path = d->get_current_dir(); - create_dir->set_disabled(true); - } else { - dialog_error->set_text(TTR("Couldn't create folder.")); - dialog_error->popup_centered(); - } - } else { - dialog_error->set_text(TTR("There is already a folder in this path with the specified name.")); - dialog_error->popup_centered(); - } - } -} + _project_path_changed(); -void ProjectDialog::_text_changed(const String &p_text) { - if (mode != MODE_NEW) { - return; + if (install_path->is_visible_in_tree()) { + // ZIP is selected; focus install path. + install_path->grab_focus(); + } else { + get_ok_button()->grab_focus(); } +} - _test_path(); +void ProjectDialog::_install_path_selected(const String &p_path) { + ERR_FAIL_COND_MSG(mode != MODE_IMPORT, "Install path is only used for MODE_IMPORT."); - if (p_text.strip_edges().is_empty()) { - _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR); + if (create_dir->is_pressed()) { + // Replace parent directory, but keep target dir name. + install_path->set_text(p_path.path_join(install_path->get_text().get_file())); + } else { + install_path->set_text(p_path); } -} -void ProjectDialog::_nonempty_confirmation_ok_pressed() { - is_folder_empty = true; - ok_pressed(); + _install_path_changed(); + + get_ok_button()->grab_focus(); } void ProjectDialog::_renderer_selected() { @@ -417,233 +450,216 @@ void ProjectDialog::_renderer_selected() { } } -void ProjectDialog::_remove_created_folder() { - if (!created_folder_path.is_empty()) { - Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - d->remove(created_folder_path); - - create_dir->set_disabled(false); - created_folder_path = ""; - } +void ProjectDialog::_nonempty_confirmation_ok_pressed() { + is_folder_empty = true; + ok_pressed(); } void ProjectDialog::ok_pressed() { - String dir = project_path->get_text(); + // Before we create a project, check that the target folder is empty. + // If not, we need to ask the user if they're sure they want to do this. + if (!is_folder_empty) { + ConfirmationDialog *cd = memnew(ConfirmationDialog); + cd->set_title(TTR("Warning: This folder is not empty")); + cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?")); + cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed)); + get_parent()->add_child(cd); + cd->popup_centered(); + return; + } - if (mode == MODE_RENAME) { - String dir2 = _test_path(); - if (dir2.is_empty()) { - _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR); + String path = project_path->get_text(); + + if (mode == MODE_NEW) { + if (create_dir->is_pressed()) { + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (!d->dir_exists(path) && d->make_dir(path) != OK) { + _set_message(TTR("Couldn't create project directory, check permissions."), MESSAGE_ERROR); + return; + } + } + + PackedStringArray project_features = ProjectSettings::get_required_features(); + ProjectSettings::CustomMap initial_settings; + + // Be sure to change this code if/when renderers are changed. + // Default values are "forward_plus" for the main setting, "mobile" for the mobile override, + // and "gl_compatibility" for the web override. + String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method")); + initial_settings["rendering/renderer/rendering_method"] = renderer_type; + + EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type); + EditorSettings::get_singleton()->save(); + + if (renderer_type == "forward_plus") { + project_features.push_back("Forward Plus"); + } else if (renderer_type == "mobile") { + project_features.push_back("Mobile"); + } else if (renderer_type == "gl_compatibility") { + project_features.push_back("GL Compatibility"); + // Also change the default rendering method for the mobile override. + initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility"; + } else { + WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub."); + } + + project_features.sort(); + initial_settings["application/config/features"] = project_features; + initial_settings["application/config/name"] = project_name->get_text().strip_edges(); + initial_settings["application/config/icon"] = "res://icon.svg"; + + Error err = ProjectSettings::get_singleton()->save_custom(path.path_join("project.godot"), initial_settings, Vector<String>(), false); + if (err != OK) { + _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR); return; } - // Load project.godot as ConfigFile to set the new name. - ConfigFile cfg; - String project_godot = dir2.path_join("project.godot"); - Error err = cfg.load(project_godot); + // Store default project icon in SVG format. + Ref<FileAccess> fa_icon = FileAccess::open(path.path_join("icon.svg"), FileAccess::WRITE, &err); if (err != OK) { - _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR); - } else { - cfg.set_value("application", "config/name", project_name->get_text().strip_edges()); - err = cfg.save(project_godot); - if (err != OK) { - _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR); - } + _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR); + return; } + fa_icon->store_string(get_default_project_icon()); - hide(); - emit_signal(SNAME("projects_updated")); + EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), path); + } - } else { - if (mode == MODE_IMPORT) { - if (project_path->get_text().ends_with(".zip")) { - mode = MODE_INSTALL; - ok_pressed(); + // Two cases for importing a ZIP. + switch (mode) { + case MODE_IMPORT: { + if (zip_path.is_empty()) { + break; + } + path = install_path->get_text().simplify_path(); + [[fallthrough]]; + } + case MODE_INSTALL: { + ERR_FAIL_COND(zip_path.is_empty()); + + Ref<FileAccess> io_fa; + zlib_filefunc_def io = zipio_create_io(&io_fa); + + unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io); + if (!pkg) { + dialog_error->set_text(TTR("Error opening package file, not in ZIP format.")); + dialog_error->popup_centered(); return; } - } else { - if (mode == MODE_NEW) { - // Before we create a project, check that the target folder is empty. - // If not, we need to ask the user if they're sure they want to do this. - if (!is_folder_empty) { - ConfirmationDialog *cd = memnew(ConfirmationDialog); - cd->set_title(TTR("Warning: This folder is not empty")); - cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?")); - cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed)); - get_parent()->add_child(cd); - cd->popup_centered(); - cd->grab_focus(); - return; + // Find the first directory with a "project.godot". + String zip_root; + int ret = unzGoToFirstFile(pkg); + while (ret == UNZ_OK) { + unz_file_info info; + char fname[16384]; + unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info."); + + String name = String::utf8(fname); + if (name.get_file() == "project.godot") { + zip_root = name.get_base_dir(); + break; } - PackedStringArray project_features = ProjectSettings::get_required_features(); - ProjectSettings::CustomMap initial_settings; - - // Be sure to change this code if/when renderers are changed. - // Default values are "forward_plus" for the main setting, "mobile" for the mobile override, - // and "gl_compatibility" for the web override. - String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method")); - initial_settings["rendering/renderer/rendering_method"] = renderer_type; - - EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type); - EditorSettings::get_singleton()->save(); - - if (renderer_type == "forward_plus") { - project_features.push_back("Forward Plus"); - } else if (renderer_type == "mobile") { - project_features.push_back("Mobile"); - } else if (renderer_type == "gl_compatibility") { - project_features.push_back("GL Compatibility"); - // Also change the default rendering method for the mobile override. - initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility"; - } else { - WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub."); - } - - project_features.sort(); - initial_settings["application/config/features"] = project_features; - initial_settings["application/config/name"] = project_name->get_text().strip_edges(); - initial_settings["application/config/icon"] = "res://icon.svg"; - - if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) { - _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR); - } else { - // Store default project icon in SVG format. - Error err; - Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err); - fa_icon->store_string(get_default_project_icon()); - - if (err != OK) { - _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR); - } - EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir); - } - } else if (mode == MODE_INSTALL) { - if (project_path->get_text().ends_with(".zip")) { - dir = install_path->get_text(); - zip_path = project_path->get_text(); - } + ret = unzGoToNextFile(pkg); + } - Ref<FileAccess> io_fa; - zlib_filefunc_def io = zipio_create_io(&io_fa); + if (ret == UNZ_END_OF_LIST_OF_FILE) { + _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR); + unzClose(pkg); + return; + } - unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io); - if (!pkg) { - dialog_error->set_text(TTR("Error opening package file, not in ZIP format.")); - dialog_error->popup_centered(); + if (create_dir->is_pressed()) { + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (!d->dir_exists(path) && d->make_dir(path) != OK) { + _set_message(TTR("Couldn't create project directory, check permissions."), MESSAGE_ERROR); return; } + } - // Find the zip_root - String zip_root; - int ret = unzGoToFirstFile(pkg); - while (ret == UNZ_OK) { - unz_file_info info; - char fname[16384]; - unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - - String name = String::utf8(fname); - if (name.ends_with("project.godot")) { - zip_root = name.substr(0, name.rfind("project.godot")); - break; + ret = unzGoToFirstFile(pkg); + + Vector<String> failed_files; + while (ret == UNZ_OK) { + //get filename + unz_file_info info; + char fname[16384]; + ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); + ERR_FAIL_COND_MSG(ret != UNZ_OK, "Failed to get current file info."); + + String rel_path = String::utf8(fname).trim_prefix(zip_root); + if (rel_path.is_empty()) { // Root. + } else if (rel_path.ends_with("/")) { // Directory. + Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + da->make_dir(path.path_join(rel_path)); + } else { // File. + Vector<uint8_t> uncomp_data; + uncomp_data.resize(info.uncompressed_size); + + unzOpenCurrentFile(pkg); + ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size()); + ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path)); + unzCloseCurrentFile(pkg); + + Ref<FileAccess> f = FileAccess::open(path.path_join(rel_path), FileAccess::WRITE); + if (f.is_valid()) { + f->store_buffer(uncomp_data.ptr(), uncomp_data.size()); + } else { + failed_files.push_back(rel_path); } - - ret = unzGoToNextFile(pkg); } - ret = unzGoToFirstFile(pkg); + ret = unzGoToNextFile(pkg); + } - Vector<String> failed_files; + unzClose(pkg); - while (ret == UNZ_OK) { - //get filename - unz_file_info info; - char fname[16384]; - ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); - if (ret != UNZ_OK) { + if (failed_files.size()) { + String err_msg = TTR("The following files failed extraction from package:") + "\n\n"; + for (int i = 0; i < failed_files.size(); i++) { + if (i > 15) { + err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files."; break; } - - String path = String::utf8(fname); - - if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) { - // - } else if (path.ends_with("/")) { // a dir - path = path.substr(0, path.length() - 1); - String rel_path = path.substr(zip_root.length()); - - Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - da->make_dir(dir.path_join(rel_path)); - } else { - Vector<uint8_t> uncomp_data; - uncomp_data.resize(info.uncompressed_size); - String rel_path = path.substr(zip_root.length()); - - //read - unzOpenCurrentFile(pkg); - ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size()); - ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path)); - unzCloseCurrentFile(pkg); - - Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE); - if (f.is_valid()) { - f->store_buffer(uncomp_data.ptr(), uncomp_data.size()); - } else { - failed_files.push_back(rel_path); - } - } - - ret = unzGoToNextFile(pkg); + err_msg += failed_files[i] + "\n"; } - unzClose(pkg); - - if (failed_files.size()) { - String err_msg = TTR("The following files failed extraction from package:") + "\n\n"; - for (int i = 0; i < failed_files.size(); i++) { - if (i > 15) { - err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files."; - break; - } - err_msg += failed_files[i] + "\n"; - } - - dialog_error->set_text(err_msg); - dialog_error->popup_centered(); - - } else if (!project_path->get_text().ends_with(".zip")) { - dialog_error->set_text(TTR("Package installed successfully!")); - dialog_error->popup_centered(); - } + dialog_error->set_text(err_msg); + dialog_error->popup_centered(); + return; } - } - - dir = dir.replace("\\", "/"); - if (dir.ends_with("/")) { - dir = dir.substr(0, dir.length() - 1); - } - - hide(); - emit_signal(SNAME("project_created"), dir); + } break; + default: { + } break; } -} - -void ProjectDialog::cancel_pressed() { - _remove_created_folder(); - - project_path->clear(); - _update_path(""); - project_name->clear(); - _text_changed(""); - if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) { - msg->show(); + if (mode == MODE_RENAME || mode == MODE_INSTALL) { + // Load project.godot as ConfigFile to set the new name. + ConfigFile cfg; + String project_godot = path.path_join("project.godot"); + Error err = cfg.load(project_godot); + if (err != OK) { + dialog_error->set_text(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err)); + dialog_error->popup_centered(); + return; + } + cfg.set_value("application", "config/name", project_name->get_text().strip_edges()); + err = cfg.save(project_godot); + if (err != OK) { + dialog_error->set_text(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err)); + dialog_error->popup_centered(); + return; + } } - if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) { - msg->show(); + hide(); + if (mode == MODE_NEW || mode == MODE_IMPORT || mode == MODE_INSTALL) { + emit_signal(SNAME("project_created"), path); + } else if (mode == MODE_RENAME) { + emit_signal(SNAME("projects_updated")); } } @@ -659,6 +675,10 @@ void ProjectDialog::set_mode(Mode p_mode) { mode = p_mode; } +void ProjectDialog::set_project_name(const String &p_name) { + project_name->set_text(p_name); +} + void ProjectDialog::set_project_path(const String &p_path) { project_path->set_text(p_path); } @@ -666,120 +686,96 @@ void ProjectDialog::set_project_path(const String &p_path) { void ProjectDialog::ask_for_path_and_show() { // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog. show_dialog(); - _set_message(""); - - _browse_path(); + _browse_project_path(); } void ProjectDialog::show_dialog() { if (mode == MODE_RENAME) { + // Name and path are set in `ProjectManager::_rename_project`. project_path->set_editable(false); - browse->hide(); - install_browse->hide(); set_title(TTR("Rename Project")); set_ok_button_text(TTR("Rename")); + + create_dir->hide(); + project_status_rect->hide(); + project_browse->hide(); + name_container->show(); - status_rect->hide(); - msg->hide(); install_path_container->hide(); - install_status_rect->hide(); renderer_container->hide(); default_files_container->hide(); - get_ok_button()->set_disabled(false); - - // Fetch current name from project.godot to prefill the text input. - ConfigFile cfg; - String project_godot = project_path->get_text().path_join("project.godot"); - Error err = cfg.load(project_godot); - if (err != OK) { - _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR); - status_rect->show(); - msg->show(); - get_ok_button()->set_disabled(true); - } else { - String cur_name = cfg.get_value("application", "config/name", ""); - project_name->set_text(cur_name); - _text_changed(cur_name); - } callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(); - - create_dir->hide(); - + callable_mp(project_name, &LineEdit::select_all).call_deferred(); } else { - fav_dir = EDITOR_GET("filesystem/directories/default_project_path"); + String proj = TTR("New Game Project"); + project_name->set_text(proj); + project_path->set_editable(true); + + String fav_dir = EDITOR_GET("filesystem/directories/default_project_path"); if (!fav_dir.is_empty()) { project_path->set_text(fav_dir); - fdialog->set_current_dir(fav_dir); + install_path->set_text(fav_dir); + fdialog_project->set_current_dir(fav_dir); } else { Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); project_path->set_text(d->get_current_dir()); - fdialog->set_current_dir(d->get_current_dir()); - } - - if (project_name->get_text().is_empty()) { - String proj = TTR("New Game Project"); - project_name->set_text(proj); - _text_changed(proj); + install_path->set_text(d->get_current_dir()); + fdialog_project->set_current_dir(d->get_current_dir()); } - project_path->set_editable(true); - browse->set_disabled(false); - browse->show(); - install_browse->set_disabled(false); - install_browse->show(); create_dir->show(); - status_rect->show(); - install_status_rect->show(); - msg->show(); + project_status_rect->show(); + project_browse->show(); if (mode == MODE_IMPORT) { set_title(TTR("Import Existing Project")); set_ok_button_text(TTR("Import & Edit")); + name_container->hide(); install_path_container->hide(); renderer_container->hide(); default_files_container->hide(); - project_path->grab_focus(); + // Project path dialog is also opened; no need to change focus. } else if (mode == MODE_NEW) { set_title(TTR("Create New Project")); set_ok_button_text(TTR("Create & Edit")); + name_container->show(); install_path_container->hide(); renderer_container->show(); default_files_container->show(); + callable_mp((Control *)project_name, &Control::grab_focus).call_deferred(); callable_mp(project_name, &LineEdit::select_all).call_deferred(); - } else if (mode == MODE_INSTALL) { set_title(TTR("Install Project:") + " " + zip_title); set_ok_button_text(TTR("Install & Edit")); + project_name->set_text(zip_title); + name_container->show(); install_path_container->hide(); renderer_container->hide(); default_files_container->hide(); - project_path->grab_focus(); + + callable_mp((Control *)project_path, &Control::grab_focus).call_deferred(); } - _test_path(); + auto_dir = ""; + last_custom_target_dir = ""; + _update_target_auto_dir(); + if (create_dir->is_pressed()) { + // Append `auto_dir` to target path. + _create_dir_toggled(true); + } } - popup_centered(Size2(500, 0) * EDSCALE); -} - -void ProjectDialog::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_THEME_CHANGED: { - create_dir->set_icon(get_editor_theme_icon(SNAME("FolderCreate"))); - } break; + _validate_path(); - case NOTIFICATION_WM_CLOSE_REQUEST: { - _remove_created_folder(); - } break; - } + popup_centered(Size2(500, 0) * EDSCALE); } void ProjectDialog::_bind_methods() { @@ -798,27 +794,29 @@ ProjectDialog::ProjectDialog() { l->set_text(TTR("Project Name:")); name_container->add_child(l); - HBoxContainer *pnhb = memnew(HBoxContainer); - name_container->add_child(pnhb); - project_name = memnew(LineEdit); project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL); - pnhb->add_child(project_name); + name_container->add_child(project_name); - create_dir = memnew(Button); - pnhb->add_child(create_dir); - create_dir->set_text(TTR("Create Folder")); - create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder)); + project_path_container = memnew(VBoxContainer); + vb->add_child(project_path_container); - path_container = memnew(VBoxContainer); - vb->add_child(path_container); + HBoxContainer *pphb_label = memnew(HBoxContainer); + project_path_container->add_child(pphb_label); l = memnew(Label); l->set_text(TTR("Project Path:")); - path_container->add_child(l); + l->set_h_size_flags(Control::SIZE_EXPAND_FILL); + pphb_label->add_child(l); + + create_dir = memnew(CheckButton); + create_dir->set_text(TTR("Create Folder")); + create_dir->set_pressed(true); + pphb_label->add_child(create_dir); + create_dir->connect("toggled", callable_mp(this, &ProjectDialog::_create_dir_toggled)); HBoxContainer *pphb = memnew(HBoxContainer); - path_container->add_child(pphb); + project_path_container->add_child(pphb); project_path = memnew(LineEdit); project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -841,14 +839,14 @@ ProjectDialog::ProjectDialog() { iphb->add_child(install_path); // status icon - status_rect = memnew(TextureRect); - status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); - pphb->add_child(status_rect); + project_status_rect = memnew(TextureRect); + project_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); + pphb->add_child(project_status_rect); - browse = memnew(Button); - browse->set_text(TTR("Browse")); - browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path)); - pphb->add_child(browse); + project_browse = memnew(Button); + project_browse->set_text(TTR("Browse")); + project_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_project_path)); + pphb->add_child(project_browse); // install status icon install_status_rect = memnew(TextureRect); @@ -863,6 +861,7 @@ ProjectDialog::ProjectDialog() { msg = memnew(Label); msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE); + msg->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); vb->add_child(msg); // Renderer selection. @@ -957,20 +956,20 @@ ProjectDialog::ProjectDialog() { spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL); default_files_container->add_child(spacer); - fdialog = memnew(EditorFileDialog); - fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes. - fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + fdialog_project = memnew(EditorFileDialog); + fdialog_project->set_previews_enabled(false); //Crucial, otherwise the engine crashes. + fdialog_project->set_access(EditorFileDialog::ACCESS_FILESYSTEM); fdialog_install = memnew(EditorFileDialog); fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes. fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM); - add_child(fdialog); + add_child(fdialog_project); add_child(fdialog_install); - project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed)); - project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed)); - install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_update_path)); - fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected)); - fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected)); + project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_project_name_changed).unbind(1)); + project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_project_path_changed).unbind(1)); + install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_install_path_changed).unbind(1)); + fdialog_project->connect("dir_selected", callable_mp(this, &ProjectDialog::_project_path_selected)); + fdialog_project->connect("file_selected", callable_mp(this, &ProjectDialog::_project_path_selected)); fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected)); fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected)); diff --git a/editor/project_manager/project_dialog.h b/editor/project_manager/project_dialog.h index dcc5cf71f8..1418edc57f 100644 --- a/editor/project_manager/project_dialog.h +++ b/editor/project_manager/project_dialog.h @@ -34,6 +34,7 @@ #include "scene/gui/dialogs.h" class Button; +class CheckButton; class EditorFileDialog; class LineEdit; class OptionButton; @@ -65,11 +66,11 @@ private: Mode mode = MODE_NEW; bool is_folder_empty = true; - Button *browse = nullptr; + CheckButton *create_dir = nullptr; + Button *project_browse = nullptr; Button *install_browse = nullptr; - Button *create_dir = nullptr; VBoxContainer *name_container = nullptr; - VBoxContainer *path_container = nullptr; + VBoxContainer *project_path_container = nullptr; VBoxContainer *install_path_container = nullptr; VBoxContainer *renderer_container = nullptr; @@ -78,54 +79,63 @@ private: Ref<ButtonGroup> renderer_button_group; Label *msg = nullptr; - LineEdit *project_path = nullptr; LineEdit *project_name = nullptr; + LineEdit *project_path = nullptr; LineEdit *install_path = nullptr; - TextureRect *status_rect = nullptr; + TextureRect *project_status_rect = nullptr; TextureRect *install_status_rect = nullptr; OptionButton *vcs_metadata_selection = nullptr; - EditorFileDialog *fdialog = nullptr; + EditorFileDialog *fdialog_project = nullptr; EditorFileDialog *fdialog_install = nullptr; AcceptDialog *dialog_error = nullptr; String zip_path; String zip_title; - String fav_dir; - String created_folder_path; + void _set_message(const String &p_msg, MessageType p_type, InputType input_type = PROJECT_PATH); + void _validate_path(); - void _set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH); + // Project path for MODE_NEW and MODE_INSTALL. Install path for MODE_IMPORT. + // Install path is only visible when importing a ZIP. + String _get_target_path(); + void _set_target_path(const String &p_text); - String _test_path(); - void _update_path(const String &p_path); - void _path_text_changed(const String &p_path); - void _path_selected(const String &p_path); - void _file_selected(const String &p_path); - void _install_path_selected(const String &p_path); + // Calculated from project name / ZIP name. + String auto_dir; + + // Updates `auto_dir`. If the target path dir name is equal to `auto_dir` (the default state), the target path is also updated. + void _update_target_auto_dir(); - void _browse_path(); + // While `create_dir` is disabled, stores the last target path dir name, or an empty string if equal to `auto_dir`. + String last_custom_target_dir; + void _create_dir_toggled(bool p_pressed); + + void _project_name_changed(); + void _project_path_changed(); + void _install_path_changed(); + + void _browse_project_path(); void _browse_install_path(); - void _create_folder(); - void _text_changed(const String &p_text); - void _nonempty_confirmation_ok_pressed(); + void _project_path_selected(const String &p_path); + void _install_path_selected(const String &p_path); + void _renderer_selected(); - void _remove_created_folder(); + void _nonempty_confirmation_ok_pressed(); void ok_pressed() override; - void cancel_pressed() override; protected: - void _notification(int p_what); static void _bind_methods(); public: - void set_zip_path(const String &p_path); - void set_zip_title(const String &p_title); void set_mode(Mode p_mode); + void set_project_name(const String &p_name); void set_project_path(const String &p_path); + void set_zip_path(const String &p_path); + void set_zip_title(const String &p_title); void ask_for_path_and_show(); void show_dialog(); diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 6849d87923..8aa2fe41ce 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1852,6 +1852,9 @@ void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme Ref<StyleBoxFlat> editor_log_button_pressed = style_flat_button_pressed->duplicate(); editor_log_button_pressed->set_border_width(SIDE_BOTTOM, 2 * EDSCALE); editor_log_button_pressed->set_border_color(p_config.accent_color); + + p_theme->set_stylebox("normal", "EditorLogFilterButton", style_flat_button); + p_theme->set_stylebox("hover", "EditorLogFilterButton", style_flat_button_hover); p_theme->set_stylebox("pressed", "EditorLogFilterButton", editor_log_button_pressed); } diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml index a68d65e8d3..d98580b771 100644 --- a/modules/gdscript/doc_classes/@GDScript.xml +++ b/modules/gdscript/doc_classes/@GDScript.xml @@ -95,7 +95,7 @@ print(get_stack()) [/codeblock] Starting from [code]_ready()[/code], [code]bar()[/code] would print: - [codeblock] + [codeblock lang=text] [{function:bar, line:12, source:res://script.gd}, {function:foo, line:9, source:res://script.gd}, {function:_ready, line:6, source:res://script.gd}] [/codeblock] [b]Note:[/b] This function only works if the running instance is connected to a debugging server (i.e. an editor instance). [method get_stack] will not work in projects exported in release mode, or in projects exported in debug mode if not connected to a debugging server. @@ -116,7 +116,7 @@ print(d.values()) [/codeblock] Prints out: - [codeblock] + [codeblock lang=text] [@subpath, @path, foo] [, res://test.gd, bar] [/codeblock] @@ -190,7 +190,7 @@ <description> Like [method @GlobalScope.print], but includes the current stack frame when running with the debugger turned on. The output in the console may look like the following: - [codeblock] + [codeblock lang=text] Test print At: res://test.gd:15:_process() [/codeblock] @@ -202,7 +202,7 @@ <description> Prints a stack trace at the current code location. See also [method get_stack]. The output in the console may look like the following: - [codeblock] + [codeblock lang=text] Frame 0 - res://test.gd:16 in function '_process' [/codeblock] [b]Note:[/b] This function only works if the running instance is connected to a debugging server (i.e. an editor instance). [method print_stack] will not work in projects exported in release mode, or in projects exported in debug mode if not connected to a debugging server. @@ -232,7 +232,7 @@ print(array[i]) [/codeblock] Output: - [codeblock] + [codeblock lang=text] 9 6 3 @@ -243,7 +243,7 @@ print(i / 10.0) [/codeblock] Output: - [codeblock] + [codeblock lang=text] 0.3 0.2 0.1 diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index aae45274e0..49341cb670 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -3460,6 +3460,7 @@ enum DocLineState { DOC_LINE_NORMAL, DOC_LINE_IN_CODE, DOC_LINE_IN_CODEBLOCK, + DOC_LINE_IN_KBD, }; static String _process_doc_line(const String &p_line, const String &p_text, const String &p_space_prefix, DocLineState &r_state) { @@ -3505,21 +3506,23 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons from = rb_pos + 1; String tag = line.substr(lb_pos + 1, rb_pos - lb_pos - 1); - if (tag == "code") { + if (tag == "code" || tag.begins_with("code ")) { r_state = DOC_LINE_IN_CODE; - } else if (tag == "codeblock") { + } else if (tag == "codeblock" || tag.begins_with("codeblock ")) { if (lb_pos == 0) { line_join = "\n"; } else { result += line.substr(buffer_start, lb_pos - buffer_start) + '\n'; } - result += "[codeblock]"; + result += "[" + tag + "]"; if (from < len) { result += '\n'; } r_state = DOC_LINE_IN_CODEBLOCK; buffer_start = from; + } else if (tag == "kbd") { + r_state = DOC_LINE_IN_KBD; } } break; case DOC_LINE_IN_CODE: { @@ -3529,7 +3532,7 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons break; } - from = pos + 7; + from = pos + 7; // `len("[/code]")`. r_state = DOC_LINE_NORMAL; } break; @@ -3540,7 +3543,7 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons break; } - from = pos + 12; + from = pos + 12; // `len("[/codeblock]")`. if (pos == 0) { line_join = "\n"; @@ -3555,6 +3558,17 @@ static String _process_doc_line(const String &p_line, const String &p_text, cons r_state = DOC_LINE_NORMAL; buffer_start = from; } break; + case DOC_LINE_IN_KBD: { + int pos = line.find("[/kbd]", from); + if (pos < 0) { + process = false; + break; + } + + from = pos + 6; // `len("[/kbd]")`. + + r_state = DOC_LINE_NORMAL; + } break; } } diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index 8489fc08c1..03d830741b 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -105,7 +105,7 @@ Error GDScriptLanguageProtocol::LSPeer::handle_data() { Error GDScriptLanguageProtocol::LSPeer::send_data() { int sent = 0; - if (!res_queue.is_empty()) { + while (!res_queue.is_empty()) { CharString c_res = res_queue[0]; if (res_sent < c_res.size()) { Error err = connection->put_partial_data((const uint8_t *)c_res.get_data() + res_sent, c_res.size() - res_sent - 1, sent); @@ -229,7 +229,9 @@ void GDScriptLanguageProtocol::initialized(const Variant &p_params) { notify_client("gdscript/capabilities", capabilities.to_json()); } -void GDScriptLanguageProtocol::poll() { +void GDScriptLanguageProtocol::poll(int p_limit_usec) { + uint64_t target_ticks = OS::get_singleton()->get_ticks_usec() + p_limit_usec; + if (server->is_connection_available()) { on_client_connected(); } @@ -244,16 +246,22 @@ void GDScriptLanguageProtocol::poll() { E = clients.begin(); continue; } else { - if (peer->connection->get_available_bytes() > 0) { + Error err = OK; + while (peer->connection->get_available_bytes() > 0) { latest_client_id = E->key; - Error err = peer->handle_data(); - if (err != OK && err != ERR_BUSY) { - on_client_disconnected(E->key); - E = clients.begin(); - continue; + err = peer->handle_data(); + if (err != OK || OS::get_singleton()->get_ticks_usec() >= target_ticks) { + break; } } - Error err = peer->send_data(); + + if (err != OK && err != ERR_BUSY) { + on_client_disconnected(E->key); + E = clients.begin(); + continue; + } + + err = peer->send_data(); if (err != OK && err != ERR_BUSY) { on_client_disconnected(E->key); E = clients.begin(); diff --git a/modules/gdscript/language_server/gdscript_language_protocol.h b/modules/gdscript/language_server/gdscript_language_protocol.h index a4d9dc6b1d..f29abaa337 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.h +++ b/modules/gdscript/language_server/gdscript_language_protocol.h @@ -105,7 +105,7 @@ public: _FORCE_INLINE_ Ref<GDScriptTextDocument> get_text_document() { return text_document; } _FORCE_INLINE_ bool is_initialized() const { return _initialized; } - void poll(); + void poll(int p_limit_usec); Error start(int p_port, const IPAddress &p_bind_ip); void stop(); diff --git a/modules/gdscript/language_server/gdscript_language_server.cpp b/modules/gdscript/language_server/gdscript_language_server.cpp index 9ba41352f2..3df26ea576 100644 --- a/modules/gdscript/language_server/gdscript_language_server.cpp +++ b/modules/gdscript/language_server/gdscript_language_server.cpp @@ -44,6 +44,7 @@ GDScriptLanguageServer::GDScriptLanguageServer() { _EDITOR_DEF("network/language_server/enable_smart_resolve", true); _EDITOR_DEF("network/language_server/show_native_symbols_in_editor", false); _EDITOR_DEF("network/language_server/use_thread", use_thread); + _EDITOR_DEF("network/language_server/poll_limit_usec", poll_limit_usec); } void GDScriptLanguageServer::_notification(int p_what) { @@ -58,7 +59,7 @@ void GDScriptLanguageServer::_notification(int p_what) { case NOTIFICATION_INTERNAL_PROCESS: { if (started && !use_thread) { - protocol.poll(); + protocol.poll(poll_limit_usec); } } break; @@ -70,7 +71,8 @@ void GDScriptLanguageServer::_notification(int p_what) { String remote_host = String(_EDITOR_GET("network/language_server/remote_host")); int remote_port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); bool remote_use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); - if (remote_host != host || remote_port != port || remote_use_thread != use_thread) { + int remote_poll_limit = (int)_EDITOR_GET("network/language_server/poll_limit_usec"); + if (remote_host != host || remote_port != port || remote_use_thread != use_thread || remote_poll_limit != poll_limit_usec) { stop(); start(); } @@ -83,7 +85,7 @@ void GDScriptLanguageServer::thread_main(void *p_userdata) { GDScriptLanguageServer *self = static_cast<GDScriptLanguageServer *>(p_userdata); while (self->thread_running) { // Poll 20 times per second - self->protocol.poll(); + self->protocol.poll(self->poll_limit_usec); OS::get_singleton()->delay_usec(50000); } } @@ -92,6 +94,7 @@ void GDScriptLanguageServer::start() { host = String(_EDITOR_GET("network/language_server/remote_host")); port = (GDScriptLanguageServer::port_override > -1) ? GDScriptLanguageServer::port_override : (int)_EDITOR_GET("network/language_server/remote_port"); use_thread = (bool)_EDITOR_GET("network/language_server/use_thread"); + poll_limit_usec = (int)_EDITOR_GET("network/language_server/poll_limit_usec"); if (protocol.start(port, IPAddress(host)) == OK) { EditorNode::get_log()->add_message("--- GDScript language server started on port " + itos(port) + " ---", EditorLog::MSG_TYPE_EDITOR); if (use_thread) { diff --git a/modules/gdscript/language_server/gdscript_language_server.h b/modules/gdscript/language_server/gdscript_language_server.h index e845d139bf..2ace5ca446 100644 --- a/modules/gdscript/language_server/gdscript_language_server.h +++ b/modules/gdscript/language_server/gdscript_language_server.h @@ -47,6 +47,7 @@ class GDScriptLanguageServer : public EditorPlugin { bool use_thread = false; String host = "127.0.0.1"; int port = 6005; + int poll_limit_usec = 100000; static void thread_main(void *p_userdata); private: diff --git a/modules/navigation/2d/nav_mesh_generator_2d.cpp b/modules/navigation/2d/nav_mesh_generator_2d.cpp index d1ac784103..3dbf9f8735 100644 --- a/modules/navigation/2d/nav_mesh_generator_2d.cpp +++ b/modules/navigation/2d/nav_mesh_generator_2d.cpp @@ -789,7 +789,7 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation const int rect_end_y = baking_rect.position[1] + baking_rect.size[1] + baking_rect_offset.y; Rect64 clipper_rect = Rect64(rect_begin_x, rect_begin_y, rect_end_x, rect_end_y); - RectClip rect_clip = RectClip(clipper_rect); + RectClip64 rect_clip = RectClip64(clipper_rect); traversable_polygon_paths = rect_clip.Execute(traversable_polygon_paths); obstruction_polygon_paths = rect_clip.Execute(obstruction_polygon_paths); @@ -821,7 +821,7 @@ void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<Navigation const int rect_end_y = baking_rect.position[1] + baking_rect.size[1] + baking_rect_offset.y - border_size; Rect64 clipper_rect = Rect64(rect_begin_x, rect_begin_y, rect_end_x, rect_end_y); - RectClip rect_clip = RectClip(clipper_rect); + RectClip64 rect_clip = RectClip64(clipper_rect); path_solution = rect_clip.Execute(path_solution); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index 737b4ac20b..9a6b6d5037 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -41,12 +41,16 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -67,12 +71,74 @@ public final class PermissionsUtil { } /** - * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a> - * @param permissionName the name of the requested permission. + * Request a list of dangerous permissions. The requested permissions must be included in the app's AndroidManifest + * @param permissions list of the permissions to request. + * @param activity the caller activity for this method. + * @return true/false. "true" if permissions are already granted, "false" if a permissions request was dispatched. + */ + public static boolean requestPermissions(Activity activity, List<String> permissions) { + if (activity == null) { + return false; + } + + if (permissions == null || permissions.isEmpty()) { + return true; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Not necessary, asked on install already + return true; + } + + Set<String> requestedPermissions = new HashSet<>(); + for (String permission : permissions) { + try { + if (permission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + Log.d(TAG, "Requesting permission " + permission); + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } catch (Exception ignored) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } + } + } else { + PermissionInfo permissionInfo = getPermissionInfo(activity, permission); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Requesting permission " + permission); + requestedPermissions.add(permission); + } + } + } catch (PackageManager.NameNotFoundException e) { + // Skip this permission and continue. + Log.w(TAG, "Unable to identify permission " + permission, e); + } + } + + if (requestedPermissions.isEmpty()) { + // If list is empty, all of dangerous permissions were granted. + return true; + } + + activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); + return true; + } + + /** + * Request a dangerous permission. The requested permission must be included in the app's AndroidManifest + * @param permissionName the name of the permission to request. * @param activity the caller activity for this method. * @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched. */ public static boolean requestPermission(String permissionName, Activity activity) { + if (activity == null || TextUtils.isEmpty(permissionName)) { + return false; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Not necessary, asked on install already return true; @@ -137,11 +203,15 @@ public final class PermissionsUtil { * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched. */ public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) { + if (activity == null) { + return false; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } - String[] manifestPermissions; + List<String> manifestPermissions; try { manifestPermissions = getManifestPermissions(activity); } catch (PackageManager.NameNotFoundException e) { @@ -149,48 +219,17 @@ public final class PermissionsUtil { return false; } - if (manifestPermissions.length == 0) + if (manifestPermissions.isEmpty()) { return true; - - List<String> requestedPermissions = new ArrayList<>(); - for (String manifestPermission : manifestPermissions) { - if (excludes != null && excludes.contains(manifestPermission)) { - continue; - } - try { - if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { - Log.d(TAG, "Requesting permission " + manifestPermission); - try { - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); - intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); - activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); - } catch (Exception ignored) { - Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); - } - } - } else { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); - int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Requesting permission " + manifestPermission); - requestedPermissions.add(manifestPermission); - } - } - } catch (PackageManager.NameNotFoundException e) { - // Skip this permission and continue. - Log.w(TAG, "Unable to identify permission " + manifestPermission, e); - } } - if (requestedPermissions.isEmpty()) { - // If list is empty, all of dangerous permissions were granted. - return true; + if (excludes != null && !excludes.isEmpty()) { + for (String excludedPermission : excludes) { + manifestPermissions.remove(excludedPermission); + } } - activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); - return false; + return requestPermissions(activity, manifestPermissions); } /** @@ -199,15 +238,16 @@ public final class PermissionsUtil { * @return granted permissions list */ public static String[] getGrantedPermissions(Context context) { - String[] manifestPermissions; + List<String> manifestPermissions; try { manifestPermissions = getManifestPermissions(context); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); return new String[0]; } - if (manifestPermissions.length == 0) - return manifestPermissions; + if (manifestPermissions.isEmpty()) { + return new String[0]; + } List<String> grantedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { @@ -256,12 +296,12 @@ public final class PermissionsUtil { * @return manifest permissions list * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. */ - private static String[] getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { + public static List<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); if (packageInfo.requestedPermissions == null) - return new String[0]; - return packageInfo.requestedPermissions; + return Collections.emptyList(); + return Arrays.asList(packageInfo.requestedPermissions); } /** diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 42f8b205c5..0884119278 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -2573,7 +2573,7 @@ struct Win32InputTextDialogInit { const Callable &callback; }; -static constexpr int scale_with_dpi(int p_pos, int p_dpi) { +static int scale_with_dpi(int p_pos, int p_dpi) { return IsProcessDPIAware() ? (p_pos * p_dpi / 96) : p_pos; } diff --git a/scene/resources/2d/tile_set.h b/scene/resources/2d/tile_set.h index b55afd2de5..c17241a436 100644 --- a/scene/resources/2d/tile_set.h +++ b/scene/resources/2d/tile_set.h @@ -299,6 +299,10 @@ protected: void _get_property_list(List<PropertyInfo> *p_list) const; void _validate_property(PropertyInfo &p_property) const; +#ifdef TOOLS_ENABLED + virtual uint32_t hash_edited_version_for_preview() const override { return 0; } // Not using preview, so disable it for performance. +#endif + private: // --- TileSet data --- // Basic shape and layout. diff --git a/scene/resources/navigation_mesh_source_geometry_data_2d.cpp b/scene/resources/navigation_mesh_source_geometry_data_2d.cpp index fabe1659c6..7c33aa9e38 100644 --- a/scene/resources/navigation_mesh_source_geometry_data_2d.cpp +++ b/scene/resources/navigation_mesh_source_geometry_data_2d.cpp @@ -113,6 +113,12 @@ void NavigationMeshSourceGeometryData2D::add_obstruction_outline(const PackedVec } } +void NavigationMeshSourceGeometryData2D::merge(const Ref<NavigationMeshSourceGeometryData2D> &p_other_geometry) { + // No need to worry about `root_node_transform` here as the data is already xformed. + traversable_outlines.append_array(p_other_geometry->traversable_outlines); + obstruction_outlines.append_array(p_other_geometry->obstruction_outlines); +} + void NavigationMeshSourceGeometryData2D::_bind_methods() { ClassDB::bind_method(D_METHOD("clear"), &NavigationMeshSourceGeometryData2D::clear); ClassDB::bind_method(D_METHOD("has_data"), &NavigationMeshSourceGeometryData2D::has_data); @@ -126,6 +132,8 @@ void NavigationMeshSourceGeometryData2D::_bind_methods() { ClassDB::bind_method(D_METHOD("add_traversable_outline", "shape_outline"), &NavigationMeshSourceGeometryData2D::add_traversable_outline); ClassDB::bind_method(D_METHOD("add_obstruction_outline", "shape_outline"), &NavigationMeshSourceGeometryData2D::add_obstruction_outline); + ClassDB::bind_method(D_METHOD("merge", "other_geometry"), &NavigationMeshSourceGeometryData2D::merge); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "traversable_outlines", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_traversable_outlines", "get_traversable_outlines"); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "obstruction_outlines", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_obstruction_outlines", "get_obstruction_outlines"); } diff --git a/scene/resources/navigation_mesh_source_geometry_data_2d.h b/scene/resources/navigation_mesh_source_geometry_data_2d.h index 985f90fb9e..4accdbc1f4 100644 --- a/scene/resources/navigation_mesh_source_geometry_data_2d.h +++ b/scene/resources/navigation_mesh_source_geometry_data_2d.h @@ -71,6 +71,8 @@ public: bool has_data() { return traversable_outlines.size(); }; void clear(); + void merge(const Ref<NavigationMeshSourceGeometryData2D> &p_other_geometry); + NavigationMeshSourceGeometryData2D() {} ~NavigationMeshSourceGeometryData2D() { clear(); } }; diff --git a/scene/resources/navigation_mesh_source_geometry_data_3d.cpp b/scene/resources/navigation_mesh_source_geometry_data_3d.cpp index e39ffab47a..43fb592bba 100644 --- a/scene/resources/navigation_mesh_source_geometry_data_3d.cpp +++ b/scene/resources/navigation_mesh_source_geometry_data_3d.cpp @@ -165,6 +165,17 @@ void NavigationMeshSourceGeometryData3D::add_faces(const PackedVector3Array &p_f _add_faces(p_faces, root_node_transform * p_xform); } +void NavigationMeshSourceGeometryData3D::merge(const Ref<NavigationMeshSourceGeometryData3D> &p_other_geometry) { + // No need to worry about `root_node_transform` here as the vertices are already xformed. + const int64_t number_of_vertices_before_merge = vertices.size(); + const int64_t number_of_indices_before_merge = indices.size(); + vertices.append_array(p_other_geometry->vertices); + indices.append_array(p_other_geometry->indices); + for (int64_t i = number_of_indices_before_merge; i < indices.size(); i++) { + indices.set(i, indices[i] + number_of_vertices_before_merge / 3); + } +} + void NavigationMeshSourceGeometryData3D::_bind_methods() { ClassDB::bind_method(D_METHOD("set_vertices", "vertices"), &NavigationMeshSourceGeometryData3D::set_vertices); ClassDB::bind_method(D_METHOD("get_vertices"), &NavigationMeshSourceGeometryData3D::get_vertices); @@ -178,6 +189,7 @@ void NavigationMeshSourceGeometryData3D::_bind_methods() { ClassDB::bind_method(D_METHOD("add_mesh", "mesh", "xform"), &NavigationMeshSourceGeometryData3D::add_mesh); ClassDB::bind_method(D_METHOD("add_mesh_array", "mesh_array", "xform"), &NavigationMeshSourceGeometryData3D::add_mesh_array); ClassDB::bind_method(D_METHOD("add_faces", "faces", "xform"), &NavigationMeshSourceGeometryData3D::add_faces); + ClassDB::bind_method(D_METHOD("merge", "other_geometry"), &NavigationMeshSourceGeometryData3D::merge); ADD_PROPERTY(PropertyInfo(Variant::PACKED_VECTOR3_ARRAY, "vertices", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_vertices", "get_vertices"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "indices", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "set_indices", "get_indices"); diff --git a/scene/resources/navigation_mesh_source_geometry_data_3d.h b/scene/resources/navigation_mesh_source_geometry_data_3d.h index 10048773fe..5f68692971 100644 --- a/scene/resources/navigation_mesh_source_geometry_data_3d.h +++ b/scene/resources/navigation_mesh_source_geometry_data_3d.h @@ -68,6 +68,8 @@ public: void add_mesh_array(const Array &p_mesh_array, const Transform3D &p_xform); void add_faces(const PackedVector3Array &p_faces, const Transform3D &p_xform); + void merge(const Ref<NavigationMeshSourceGeometryData3D> &p_other_geometry); + NavigationMeshSourceGeometryData3D() {} ~NavigationMeshSourceGeometryData3D() { clear(); } }; diff --git a/scene/resources/visual_shader_nodes.h b/scene/resources/visual_shader_nodes.h index 05c8fbd16c..0bd0c631b8 100644 --- a/scene/resources/visual_shader_nodes.h +++ b/scene/resources/visual_shader_nodes.h @@ -514,6 +514,8 @@ public: virtual Vector<StringName> get_editable_properties() const override; virtual bool is_use_prop_slots() const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeCurveXYZTexture(); }; @@ -657,6 +659,8 @@ public: virtual Vector<StringName> get_editable_properties() const override; virtual String get_warning(Shader::Mode p_mode, VisualShader::Type p_type) const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeCubemap(); }; @@ -832,6 +836,8 @@ public: virtual Vector<StringName> get_editable_properties() const override; + virtual Category get_category() const override { return CATEGORY_SCALAR; } + VisualShaderNodeIntOp(); }; @@ -880,6 +886,8 @@ public: virtual Vector<StringName> get_editable_properties() const override; + virtual Category get_category() const override { return CATEGORY_SCALAR; } + VisualShaderNodeUIntOp(); }; @@ -1502,6 +1510,8 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_VECTOR; } + VisualShaderNodeDotProduct(); }; @@ -1548,6 +1558,8 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_VECTOR; } + VisualShaderNodeDeterminant(); }; @@ -1591,6 +1603,14 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { + if (op_type == OP_TYPE_FLOAT || op_type == OP_TYPE_INT || op_type == OP_TYPE_UINT) { + return CATEGORY_SCALAR; + } else { + return CATEGORY_VECTOR; + } + } + VisualShaderNodeClamp(); }; @@ -1990,8 +2010,6 @@ public: virtual void set_op_type(OpType p_op_type) override; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; - virtual Category get_category() const override { return CATEGORY_VECTOR; } - VisualShaderNodeVectorDecompose(); }; @@ -2734,6 +2752,8 @@ public: virtual bool is_generate_input_var(int p_port) const override; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_UTILITY; } + VisualShaderNodeFresnel(); }; diff --git a/scene/resources/visual_shader_particle_nodes.h b/scene/resources/visual_shader_particle_nodes.h index 23d06d4b7c..31ba310c3c 100644 --- a/scene/resources/visual_shader_particle_nodes.h +++ b/scene/resources/visual_shader_particle_nodes.h @@ -75,8 +75,6 @@ public: virtual String generate_global_per_node(Shader::Mode p_mode, int p_id) const override; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; - virtual Category get_category() const override { return CATEGORY_PARTICLE; } - VisualShaderNodeParticleSphereEmitter(); }; @@ -94,8 +92,6 @@ public: virtual String generate_global_per_node(Shader::Mode p_mode, int p_id) const override; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; - virtual Category get_category() const override { return CATEGORY_PARTICLE; } - VisualShaderNodeParticleBoxEmitter(); }; @@ -112,8 +108,6 @@ public: virtual String generate_global_per_node(Shader::Mode p_mode, int p_id) const override; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; - virtual Category get_category() const override { return CATEGORY_PARTICLE; } - VisualShaderNodeParticleRingEmitter(); }; @@ -166,8 +160,6 @@ public: HashMap<StringName, String> get_editable_properties_names() const override; Vector<VisualShader::DefaultTextureParam> get_default_texture_parameters(VisualShader::Type p_type, int p_id) const override; - virtual Category get_category() const override { return CATEGORY_PARTICLE; } - VisualShaderNodeParticleMeshEmitter(); }; diff --git a/scene/resources/visual_shader_sdf_nodes.h b/scene/resources/visual_shader_sdf_nodes.h index 525098e5fc..d84cdb431b 100644 --- a/scene/resources/visual_shader_sdf_nodes.h +++ b/scene/resources/visual_shader_sdf_nodes.h @@ -49,6 +49,8 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeSDFToScreenUV(); }; @@ -69,6 +71,8 @@ public: virtual bool is_input_port_default(int p_port, Shader::Mode p_mode) const override; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeScreenUVToSDF(); }; @@ -88,6 +92,8 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeTextureSDF(); }; @@ -107,6 +113,8 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeTextureSDFNormal(); }; @@ -126,6 +134,8 @@ public: virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual Category get_category() const override { return CATEGORY_TEXTURES; } + VisualShaderNodeSDFRaymarch(); }; diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp index 55c6c420eb..0b504eca0a 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp @@ -737,7 +737,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { default_shader = material_storage->shader_allocate(); material_storage->shader_initialize(default_shader); material_storage->shader_set_code(default_shader, R"( -// Default 3D material shader (clustered). +// Default 3D material shader (Forward+). shader_type spatial; @@ -768,11 +768,11 @@ void fragment() { material_storage->shader_initialize(overdraw_material_shader); // Use relatively low opacity so that more "layers" of overlapping objects can be distinguished. material_storage->shader_set_code(overdraw_material_shader, R"( -// 3D editor Overdraw debug draw mode shader (clustered). +// 3D editor Overdraw debug draw mode shader (Forward+). shader_type spatial; -render_mode blend_add, unshaded; +render_mode blend_add, unshaded, fog_disabled; void fragment() { ALBEDO = vec3(0.4, 0.8, 0.8); @@ -792,11 +792,11 @@ void fragment() { debug_shadow_splits_material_shader = material_storage->shader_allocate(); material_storage->shader_initialize(debug_shadow_splits_material_shader); material_storage->shader_set_code(debug_shadow_splits_material_shader, R"( -// 3D debug shadow splits mode shader(mobile). +// 3D debug shadow splits mode shader (Forward+). shader_type spatial; -render_mode debug_shadow_splits; +render_mode debug_shadow_splits, fog_disabled; void fragment() { ALBEDO = vec3(1.0, 1.0, 1.0); diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp index 3592ee2f6d..95ba76a707 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp @@ -641,7 +641,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { default_shader = material_storage->shader_allocate(); material_storage->shader_initialize(default_shader); material_storage->shader_set_code(default_shader, R"( -// Default 3D material shader (mobile). +// Default 3D material shader (Mobile). shader_type spatial; @@ -671,11 +671,11 @@ void fragment() { material_storage->shader_initialize(overdraw_material_shader); // Use relatively low opacity so that more "layers" of overlapping objects can be distinguished. material_storage->shader_set_code(overdraw_material_shader, R"( -// 3D editor Overdraw debug draw mode shader (mobile). +// 3D editor Overdraw debug draw mode shader (Mobile). shader_type spatial; -render_mode blend_add, unshaded; +render_mode blend_add, unshaded, fog_disabled; void fragment() { ALBEDO = vec3(0.4, 0.8, 0.8); @@ -696,11 +696,11 @@ void fragment() { material_storage->shader_initialize(debug_shadow_splits_material_shader); // Use relatively low opacity so that more "layers" of overlapping objects can be distinguished. material_storage->shader_set_code(debug_shadow_splits_material_shader, R"( -// 3D debug shadow splits mode shader(mobile). +// 3D debug shadow splits mode shader (Mobile). shader_type spatial; -render_mode debug_shadow_splits; +render_mode debug_shadow_splits, fog_disabled; void fragment() { ALBEDO = vec3(1.0, 1.0, 1.0); diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp index 62ecec3991..e61bb9eae8 100644 --- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp @@ -1153,12 +1153,19 @@ void RendererSceneRenderRD::render_scene(const Ref<RenderSceneBuffers> &p_render PagedArray<RID> empty; - if (get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_UNSHADED) { + if (get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_UNSHADED || get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_OVERDRAW) { render_data.lights = ∅ render_data.reflection_probes = ∅ render_data.voxel_gi_instances = ∅ } + if (get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_UNSHADED || + get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_OVERDRAW || + get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_LIGHTING || + get_debug_draw_mode() == RS::VIEWPORT_DEBUG_DRAW_PSSM_SPLITS) { + render_data.decals = ∅ + } + Color clear_color; if (p_render_buffers.is_valid() && p_reflection_probe.is_null()) { clear_color = texture_storage->render_target_get_clear_request_color(rb->get_render_target()); diff --git a/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp b/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp index 28b2c9b40c..e78b8de4db 100644 --- a/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp @@ -1452,6 +1452,11 @@ void ParticlesStorage::update_particles() { if (uint32_t(history_size) != particles->frame_history.size()) { particles->frame_history.resize(history_size); memset(particles->frame_history.ptr(), 0, sizeof(ParticlesFrameParams) * history_size); + // Set the frame number so that we are able to distinguish an uninitialized + // frame from the true frame number zero. See issue #88712 for details. + for (int i = 0; i < history_size; i++) { + particles->frame_history[i].frame = UINT32_MAX; + } } if (uint32_t(trail_steps) != particles->trail_params.size() || particles->frame_params_buffer.is_null()) { diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp index 4e5539e6a4..aa69cd8539 100644 --- a/servers/rendering/renderer_scene_cull.cpp +++ b/servers/rendering/renderer_scene_cull.cpp @@ -2053,7 +2053,7 @@ void RendererSceneCull::_update_instance_lightmap_captures(Instance *p_instance) Vector3 inner_pos = ((lm_pos - bounds.position) / bounds.size) * 2.0 - Vector3(1.0, 1.0, 1.0); - real_t blend = MAX(inner_pos.x, MAX(inner_pos.y, inner_pos.z)); + real_t blend = MAX(ABS(inner_pos.x), MAX(ABS(inner_pos.y), ABS(inner_pos.z))); //make blend more rounded blend = Math::lerp(inner_pos.length(), blend, blend); blend *= blend; diff --git a/servers/rendering/rendering_device_commons.h b/servers/rendering/rendering_device_commons.h index 688a6441a7..591752bc0e 100644 --- a/servers/rendering/rendering_device_commons.h +++ b/servers/rendering/rendering_device_commons.h @@ -878,6 +878,7 @@ protected: static const char *SHADER_STAGE_NAMES[SHADER_STAGE_MAX]; +public: struct ShaderUniform { UniformType type = UniformType::UNIFORM_TYPE_MAX; bool writable = false; @@ -925,6 +926,7 @@ protected: Vector<ShaderStage> stages; }; +protected: struct ShaderReflection : public ShaderDescription { BitField<ShaderStage> stages; BitField<ShaderStage> push_constant_stages; diff --git a/tests/servers/test_navigation_server_3d.h b/tests/servers/test_navigation_server_3d.h index b5547f2c94..827a1bed17 100644 --- a/tests/servers/test_navigation_server_3d.h +++ b/tests/servers/test_navigation_server_3d.h @@ -35,8 +35,6 @@ #include "scene/resources/3d/primitive_meshes.h" #include "servers/navigation_server_3d.h" -#include "tests/test_macros.h" - namespace TestNavigationServer3D { // TODO: Find a more generic way to create `Callable` mocks. @@ -580,6 +578,51 @@ TEST_SUITE("[Navigation]") { } #endif // DISABLE_DEPRECATED + TEST_CASE("[NavigationServer3D][SceneTree] Server should be able to parse geometry") { + NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); + + // Prepare scene tree with simple mesh to serve as an input geometry. + Node3D *node_3d = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(node_3d); + Ref<PlaneMesh> plane_mesh = memnew(PlaneMesh); + plane_mesh->set_size(Size2(10.0, 10.0)); + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_mesh(plane_mesh); + node_3d->add_child(mesh_instance); + + Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh); + Ref<NavigationMeshSourceGeometryData3D> source_geometry = memnew(NavigationMeshSourceGeometryData3D); + CHECK_EQ(source_geometry->get_vertices().size(), 0); + CHECK_EQ(source_geometry->get_indices().size(), 0); + + navigation_server->parse_source_geometry_data(navigation_mesh, source_geometry, mesh_instance); + CHECK_EQ(source_geometry->get_vertices().size(), 12); + CHECK_EQ(source_geometry->get_indices().size(), 6); + + SUBCASE("By default, parsing should remove any data that was parsed before") { + navigation_server->parse_source_geometry_data(navigation_mesh, source_geometry, mesh_instance); + CHECK_EQ(source_geometry->get_vertices().size(), 12); + CHECK_EQ(source_geometry->get_indices().size(), 6); + } + + SUBCASE("Parsed geometry should be extendible with other geometry") { + source_geometry->merge(source_geometry); // Merging with itself. + const Vector<float> vertices = source_geometry->get_vertices(); + const Vector<int> indices = source_geometry->get_indices(); + REQUIRE_EQ(vertices.size(), 24); + REQUIRE_EQ(indices.size(), 12); + // Check if first newly added vertex is the same as first vertex. + CHECK_EQ(vertices[0], vertices[12]); + CHECK_EQ(vertices[1], vertices[13]); + CHECK_EQ(vertices[2], vertices[14]); + // Check if first newly added index is the same as first index. + CHECK_EQ(indices[0] + 4, indices[6]); + } + + memdelete(mesh_instance); + memdelete(node_3d); + } + // This test case uses only public APIs on purpose - other test cases use simplified baking. TEST_CASE("[NavigationServer3D][SceneTree] Server should be able to bake map correctly") { NavigationServer3D *navigation_server = NavigationServer3D::get_singleton(); diff --git a/thirdparty/README.md b/thirdparty/README.md index cb03ddcb36..12333fa06f 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -94,14 +94,17 @@ Files extracted from upstream source: ## clipper2 - Upstream: https://github.com/AngusJohnson/Clipper2 -- Version: 1.2.2 (756c5079aacab5837e812a143c59dc48a09f22e7, 2023) +- Version: 1.3.0 (98db5662e8dd1808a5a7b50c5605a2289bb390e8, 2023) - License: BSL 1.0 Files extracted from upstream source: -- `CPP/Clipper2Lib` folder +- `CPP/Clipper2Lib/` folder (in root) - `LICENSE` +Apply the patches in the `patches/` folder when syncing on newer upstream +commits. + ## cvtt diff --git a/thirdparty/clipper2/include/clipper2/clipper.core.h b/thirdparty/clipper2/include/clipper2/clipper.core.h index 086d1b659c..a77cdad5f4 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.core.h +++ b/thirdparty/clipper2/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 March 2023 * +* Date : 24 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library structures and functions * @@ -19,6 +19,7 @@ #include <algorithm> #include <climits> #include <numeric> +#include "clipper2/clipper.version.h" #define CLIPPER2_THROW(exception) std::abort() @@ -44,15 +45,27 @@ namespace Clipper2Lib "Invalid scale (either 0 or too large)"; static const char* non_pair_error = "There must be 2 values for each coordinate"; + static const char* undefined_error = + "There is an undefined error in Clipper2"; #endif // error codes (2^n) - const int precision_error_i = 1; // non-fatal - const int scale_error_i = 2; // non-fatal - const int non_pair_error_i = 4; // non-fatal - const int range_error_i = 64; + const int precision_error_i = 1; // non-fatal + const int scale_error_i = 2; // non-fatal + const int non_pair_error_i = 4; // non-fatal + const int undefined_error_i = 32; // fatal + const int range_error_i = 64; +#ifndef PI static const double PI = 3.141592653589793238; +#endif + +#ifdef CLIPPER2_MAX_PRECISION + const int MAX_DECIMAL_PRECISION = CLIPPER2_MAX_PRECISION; +#else + const int MAX_DECIMAL_PRECISION = 8; // see Discussions #564 +#endif + static const int64_t MAX_COORD = INT64_MAX >> 2; static const int64_t MIN_COORD = -MAX_COORD; static const int64_t INVALID = INT64_MAX; @@ -72,6 +85,8 @@ namespace Clipper2Lib CLIPPER2_THROW(Clipper2Exception(scale_error)); case non_pair_error_i: CLIPPER2_THROW(Clipper2Exception(non_pair_error)); + case undefined_error_i: + CLIPPER2_THROW(Clipper2Exception(undefined_error)); case range_error_i: CLIPPER2_THROW(Clipper2Exception(range_error)); } @@ -80,6 +95,7 @@ namespace Clipper2Lib #endif } + //By far the most widely used filling rules for polygons are EvenOdd //and NonZero, sometimes called Alternate and Winding respectively. //https://en.wikipedia.org/wiki/Nonzero-rule @@ -132,10 +148,11 @@ namespace Clipper2Lib return Point(x * scale, y * scale, z); } + void SetZ(const int64_t z_value) { z = z_value; } friend std::ostream& operator<<(std::ostream& os, const Point& point) { - os << point.x << "," << point.y << "," << point.z << " "; + os << point.x << "," << point.y << "," << point.z; return os; } @@ -172,7 +189,7 @@ namespace Clipper2Lib friend std::ostream& operator<<(std::ostream& os, const Point& point) { - os << point.x << "," << point.y << " "; + os << point.x << "," << point.y; return os; } #endif @@ -220,6 +237,14 @@ namespace Clipper2Lib using Paths64 = std::vector< Path64>; using PathsD = std::vector< PathD>; + static const Point64 InvalidPoint64 = Point64( + (std::numeric_limits<int64_t>::max)(), + (std::numeric_limits<int64_t>::max)()); + static const PointD InvalidPointD = PointD( + (std::numeric_limits<double>::max)(), + (std::numeric_limits<double>::max)()); + + // Rect ------------------------------------------------------------------------ template <typename T> @@ -235,19 +260,13 @@ namespace Clipper2Lib T right; T bottom; - Rect() : - left(0), - top(0), - right(0), - bottom(0) {} - Rect(T l, T t, T r, T b) : left(l), top(t), right(r), bottom(b) {} - Rect(bool is_valid) + Rect(bool is_valid = true) { if (is_valid) { @@ -255,11 +274,13 @@ namespace Clipper2Lib } else { - left = top = std::numeric_limits<T>::max(); - right = bottom = -std::numeric_limits<int64_t>::max(); + left = top = (std::numeric_limits<T>::max)(); + right = bottom = (std::numeric_limits<T>::lowest)(); } } + bool IsValid() const { return left != (std::numeric_limits<T>::max)(); } + T Width() const { return right - left; } T Height() const { return bottom - top; } void Width(T width) { right = left + width; } @@ -307,10 +328,13 @@ namespace Clipper2Lib ((std::max)(top, rec.top) <= (std::min)(bottom, rec.bottom)); }; + bool operator==(const Rect<T>& other) const { + return left == other.left && right == other.right && + top == other.top && bottom == other.bottom; + } + friend std::ostream& operator<<(std::ostream& os, const Rect<T>& rect) { - os << "(" - << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom - << ")"; + os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") "; return os; } }; @@ -338,16 +362,22 @@ namespace Clipper2Lib return result; } - static const Rect64 MaxInvalidRect64 = Rect64( - INT64_MAX, INT64_MAX, INT64_MIN, INT64_MIN); - static const RectD MaxInvalidRectD = RectD( - MAX_DBL, MAX_DBL, -MAX_DBL, -MAX_DBL); + static const Rect64 InvalidRect64 = Rect64( + (std::numeric_limits<int64_t>::max)(), + (std::numeric_limits<int64_t>::max)(), + (std::numeric_limits<int64_t>::lowest)(), + (std::numeric_limits<int64_t>::lowest)()); + static const RectD InvalidRectD = RectD( + (std::numeric_limits<double>::max)(), + (std::numeric_limits<double>::max)(), + (std::numeric_limits<double>::lowest)(), + (std::numeric_limits<double>::lowest)()); template <typename T> Rect<T> GetBounds(const Path<T>& path) { - auto xmin = std::numeric_limits<T>::max(); - auto ymin = std::numeric_limits<T>::max(); + auto xmin = (std::numeric_limits<T>::max)(); + auto ymin = (std::numeric_limits<T>::max)(); auto xmax = std::numeric_limits<T>::lowest(); auto ymax = std::numeric_limits<T>::lowest(); for (const auto& p : path) @@ -363,8 +393,8 @@ namespace Clipper2Lib template <typename T> Rect<T> GetBounds(const Paths<T>& paths) { - auto xmin = std::numeric_limits<T>::max(); - auto ymin = std::numeric_limits<T>::max(); + auto xmin = (std::numeric_limits<T>::max)(); + auto ymin = (std::numeric_limits<T>::max)(); auto xmax = std::numeric_limits<T>::lowest(); auto ymax = std::numeric_limits<T>::lowest(); for (const Path<T>& path : paths) @@ -428,7 +458,7 @@ namespace Clipper2Lib } template <typename T1, typename T2> - inline Path<T1> ScalePath(const Path<T2>& path, + inline Path<T1> ScalePath(const Path<T2>& path, double scale, int& error_code) { return ScalePath<T1, T2>(path, scale, scale, error_code); @@ -488,26 +518,6 @@ namespace Clipper2Lib return result; } - inline PathD Path64ToPathD(const Path64& path) - { - return TransformPath<double, int64_t>(path); - } - - inline PathsD Paths64ToPathsD(const Paths64& paths) - { - return TransformPaths<double, int64_t>(paths); - } - - inline Path64 PathDToPath64(const PathD& path) - { - return TransformPath<int64_t, double>(path); - } - - inline Paths64 PathsDToPaths64(const PathsD& paths) - { - return TransformPaths<int64_t, double>(paths); - } - template<typename T> inline double Sqr(T val) { @@ -560,48 +570,32 @@ namespace Clipper2Lib } template<typename T> - inline Path<T> StripDuplicates(const Path<T>& path, bool is_closed_path) + inline void StripDuplicates( Path<T>& path, bool is_closed_path) { - if (path.size() == 0) return Path<T>(); - Path<T> result; - result.reserve(path.size()); - typename Path<T>::const_iterator path_iter = path.cbegin(); - Point<T> first_pt = *path_iter++, last_pt = first_pt; - result.push_back(first_pt); - for (; path_iter != path.cend(); ++path_iter) - { - if (*path_iter != last_pt) - { - last_pt = *path_iter; - result.push_back(last_pt); - } - } - if (!is_closed_path) return result; - while (result.size() > 1 && result.back() == first_pt) result.pop_back(); - return result; + //https://stackoverflow.com/questions/1041620/whats-the-most-efficient-way-to-erase-duplicates-and-sort-a-vector#:~:text=Let%27s%20compare%20three%20approaches%3A + path.erase(std::unique(path.begin(), path.end()), path.end()); + if (is_closed_path) + while (path.size() > 1 && path.back() == path.front()) path.pop_back(); } template<typename T> - inline Paths<T> StripDuplicates(const Paths<T>& paths, bool is_closed_path) + inline void StripDuplicates( Paths<T>& paths, bool is_closed_path) { - Paths<T> result; - result.reserve(paths.size()); - for (typename Paths<T>::const_iterator paths_citer = paths.cbegin(); - paths_citer != paths.cend(); ++paths_citer) + for (typename Paths<T>::iterator paths_citer = paths.begin(); + paths_citer != paths.end(); ++paths_citer) { - result.push_back(StripDuplicates(*paths_citer, is_closed_path)); + StripDuplicates(*paths_citer, is_closed_path); } - return result; } // Miscellaneous ------------------------------------------------------------ inline void CheckPrecision(int& precision, int& error_code) { - if (precision >= -8 && precision <= 8) return; + if (precision >= -MAX_DECIMAL_PRECISION && precision <= MAX_DECIMAL_PRECISION) return; error_code |= precision_error_i; // non-fatal error - DoError(precision_error_i); // unless exceptions enabled - precision = precision > 8 ? 8 : -8; + DoError(precision_error_i); // does nothing unless exceptions enabled + precision = precision > 0 ? MAX_DECIMAL_PRECISION : -MAX_DECIMAL_PRECISION; } inline void CheckPrecision(int& precision) @@ -693,29 +687,27 @@ namespace Clipper2Lib //nb: This statement is premised on using Cartesian coordinates return Area<T>(poly) >= 0; } - - inline int64_t CheckCastInt64(double val) - { - if ((val >= max_coord) || (val <= min_coord)) return INVALID; - else return static_cast<int64_t>(val); - } - + inline bool GetIntersectPoint(const Point64& ln1a, const Point64& ln1b, const Point64& ln2a, const Point64& ln2b, Point64& ip) { // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection - double dx1 = static_cast<double>(ln1b.x - ln1a.x); double dy1 = static_cast<double>(ln1b.y - ln1a.y); double dx2 = static_cast<double>(ln2b.x - ln2a.x); double dy2 = static_cast<double>(ln2b.y - ln2a.y); + double det = dy1 * dx2 - dy2 * dx1; - if (det == 0.0) return 0; - double qx = dx1 * ln1a.y - dy1 * ln1a.x; - double qy = dx2 * ln2a.y - dy2 * ln2a.x; - ip.x = CheckCastInt64((dx1 * qy - dx2 * qx) / det); - ip.y = CheckCastInt64((dy1 * qy - dy2 * qx) / det); - return (ip.x != INVALID && ip.y != INVALID); + if (det == 0.0) return false; + double t = ((ln1a.x - ln2a.x) * dy2 - (ln1a.y - ln2a.y) * dx2) / det; + if (t <= 0.0) ip = ln1a; // ?? check further (see also #568) + else if (t >= 1.0) ip = ln1b; // ?? check further + else + { + ip.x = static_cast<int64_t>(ln1a.x + t * dx1); + ip.y = static_cast<int64_t>(ln1a.y + t * dy1); + } + return true; } inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b, @@ -739,8 +731,9 @@ namespace Clipper2Lib } } - inline Point64 GetClosestPointOnSegment(const Point64& offPt, - const Point64& seg1, const Point64& seg2) + template<typename T> + inline Point<T> GetClosestPointOnSegment(const Point<T>& offPt, + const Point<T>& seg1, const Point<T>& seg2) { if (seg1.x == seg2.x && seg1.y == seg2.y) return seg1; double dx = static_cast<double>(seg2.x - seg1.x); @@ -750,9 +743,14 @@ namespace Clipper2Lib static_cast<double>(offPt.y - seg1.y) * dy) / (Sqr(dx) + Sqr(dy)); if (q < 0) q = 0; else if (q > 1) q = 1; - return Point64( - seg1.x + static_cast<int64_t>(nearbyint(q * dx)), - seg1.y + static_cast<int64_t>(nearbyint(q * dy))); + if constexpr (std::numeric_limits<T>::is_integer) + return Point<T>( + seg1.x + static_cast<T>(nearbyint(q * dx)), + seg1.y + static_cast<T>(nearbyint(q * dy))); + else + return Point<T>( + seg1.x + static_cast<T>(q * dx), + seg1.y + static_cast<T>(q * dy)); } enum class PointInPolygonResult { IsOn, IsInside, IsOutside }; diff --git a/thirdparty/clipper2/include/clipper2/clipper.engine.h b/thirdparty/clipper2/include/clipper2/clipper.engine.h index 30dc6c86ff..13c7f069aa 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.engine.h +++ b/thirdparty/clipper2/include/clipper2/clipper.engine.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 26 March 2023 * +* Date : 22 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -10,9 +10,8 @@ #ifndef CLIPPER_ENGINE_H #define CLIPPER_ENGINE_H -constexpr auto CLIPPER2_VERSION = "1.2.2"; - #include <cstdlib> +#include <stdint.h> //#541 #include <iostream> #include <queue> #include <vector> @@ -20,7 +19,7 @@ constexpr auto CLIPPER2_VERSION = "1.2.2"; #include <numeric> #include <memory> -#include "clipper.core.h" +#include "clipper2/clipper.core.h" namespace Clipper2Lib { @@ -91,10 +90,11 @@ namespace Clipper2Lib { OutPt* pts = nullptr; PolyPath* polypath = nullptr; OutRecList* splits = nullptr; + OutRec* recursive_split = nullptr; Rect64 bounds = {}; Path64 path; bool is_open = false; - bool horz_done = false; + ~OutRec() { if (splits) delete splits; // nb: don't delete the split pointers @@ -179,6 +179,20 @@ namespace Clipper2Lib { typedef std::vector<LocalMinima_ptr> LocalMinimaList; typedef std::vector<IntersectNode> IntersectNodeList; + // ReuseableDataContainer64 ------------------------------------------------ + + class ReuseableDataContainer64 { + private: + friend class ClipperBase; + LocalMinimaList minima_list_; + std::vector<Vertex*> vertex_lists_; + void AddLocMin(Vertex& vert, PathType polytype, bool is_open); + public: + virtual ~ReuseableDataContainer64(); + void Clear(); + void AddPaths(const Paths64& paths, PathType polytype, bool is_open); + }; + // ClipperBase ------------------------------------------------------------- class ClipperBase { @@ -235,7 +249,6 @@ namespace Clipper2Lib { void DoTopOfScanbeam(const int64_t top_y); Active *DoMaxima(Active &e); void JoinOutrecPaths(Active &e1, Active &e2); - void CompleteSplit(OutPt* op1, OutPt* op2, OutRec& outrec); void FixSelfIntersects(OutRec* outrec); void DoSplitOp(OutRec* outRec, OutPt* splitOp); @@ -249,6 +262,8 @@ namespace Clipper2Lib { inline void CheckJoinRight(Active& e, const Point64& pt, bool check_curr_x = false); protected: + bool preserve_collinear_ = true; + bool reverse_solution_ = false; int error_code_ = 0; bool has_open_paths_ = false; bool succeeded_ = true; @@ -256,8 +271,8 @@ namespace Clipper2Lib { bool ExecuteInternal(ClipType ct, FillRule ft, bool use_polytrees); void CleanCollinear(OutRec* outrec); bool CheckBounds(OutRec* outrec); + bool CheckSplitOwner(OutRec* outrec, OutRecList* splits); void RecursiveCheckOwners(OutRec* outrec, PolyPath* polypath); - void DeepCheckOwners(OutRec* outrec, PolyPath* polypath); #ifdef USINGZ ZCallback64 zCallback_ = nullptr; void SetZ(const Active& e1, const Active& e2, Point64& pt); @@ -267,10 +282,13 @@ namespace Clipper2Lib { void AddPaths(const Paths64& paths, PathType polytype, bool is_open); public: virtual ~ClipperBase(); - int ErrorCode() { return error_code_; }; - bool PreserveCollinear = true; - bool ReverseSolution = false; + int ErrorCode() const { return error_code_; }; + void PreserveCollinear(bool val) { preserve_collinear_ = val; }; + bool PreserveCollinear() const { return preserve_collinear_;}; + void ReverseSolution(bool val) { reverse_solution_ = val; }; + bool ReverseSolution() const { return reverse_solution_; }; void Clear(); + void AddReuseableData(const ReuseableDataContainer64& reuseable_data); #ifdef USINGZ int64_t DefaultZ = 0; #endif @@ -330,12 +348,12 @@ namespace Clipper2Lib { childs_.resize(0); } - const PolyPath64* operator [] (size_t index) const + PolyPath64* operator [] (size_t index) const { - return childs_[index].get(); + return childs_[index].get(); //std::unique_ptr } - const PolyPath64* Child(size_t index) const + PolyPath64* Child(size_t index) const { return childs_[index].get(); } @@ -375,24 +393,24 @@ namespace Clipper2Lib { class PolyPathD : public PolyPath { private: PolyPathDList childs_; - double inv_scale_; + double scale_; PathD polygon_; public: explicit PolyPathD(PolyPathD* parent = nullptr) : PolyPath(parent) { - inv_scale_ = parent ? parent->inv_scale_ : 1.0; + scale_ = parent ? parent->scale_ : 1.0; } ~PolyPathD() { childs_.resize(0); } - const PolyPathD* operator [] (size_t index) const + PolyPathD* operator [] (size_t index) const { return childs_[index].get(); } - const PolyPathD* Child(size_t index) const + PolyPathD* Child(size_t index) const { return childs_[index].get(); } @@ -400,14 +418,23 @@ namespace Clipper2Lib { PolyPathDList::const_iterator begin() const { return childs_.cbegin(); } PolyPathDList::const_iterator end() const { return childs_.cend(); } - void SetInvScale(double value) { inv_scale_ = value; } - double InvScale() { return inv_scale_; } + void SetScale(double value) { scale_ = value; } + double Scale() const { return scale_; } + PolyPathD* AddChild(const Path64& path) override { int error_code = 0; auto p = std::make_unique<PolyPathD>(this); PolyPathD* result = childs_.emplace_back(std::move(p)).get(); - result->polygon_ = ScalePath<double, int64_t>(path, inv_scale_, error_code); + result->polygon_ = ScalePath<double, int64_t>(path, scale_, error_code); + return result; + } + + PolyPathD* AddChild(const PathD& path) + { + auto p = std::make_unique<PolyPathD>(this); + PolyPathD* result = childs_.emplace_back(std::move(p)).get(); + result->polygon_ = path; return result; } @@ -595,7 +622,7 @@ namespace Clipper2Lib { if (ExecuteInternal(clip_type, fill_rule, true)) { polytree.Clear(); - polytree.SetInvScale(invScale_); + polytree.SetScale(invScale_); open_paths.clear(); BuildTreeD(polytree, open_paths); } diff --git a/thirdparty/clipper2/include/clipper2/clipper.export.h b/thirdparty/clipper2/include/clipper2/clipper.export.h index e8d678a41d..d7286132a4 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.export.h +++ b/thirdparty/clipper2/include/clipper2/clipper.export.h @@ -1,39 +1,78 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 23 March 2023 * +* Date : 26 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module exports the Clipper2 Library (ie DLL/so) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ -// The exported functions below refer to simple structures that -// can be understood across multiple languages. Consequently -// Path64, PathD, Polytree64 etc are converted from C++ classes -// (std::vector<> etc) into the following data structures: -// -// CPath64 (int64_t*) & CPathD (double_t*): -// Path64 and PathD are converted into arrays of x,y coordinates. -// However in these arrays the first x,y coordinate pair is a -// counter with 'x' containing the number of following coordinate -// pairs. ('y' should be 0, with one exception explained below.) -// __________________________________ -// |counter|coord1|coord2|...|coordN| -// |N ,0 |x1, y1|x2, y2|...|xN, yN| -// __________________________________ -// -// CPaths64 (int64_t**) & CPathsD (double_t**): -// These are arrays of pointers to CPath64 and CPathD where -// the first pointer is to a 'counter path'. This 'counter -// path' has a single x,y coord pair with 'y' (not 'x') -// containing the number of paths that follow. ('x' = 0). -// _______________________________ -// |counter|path1|path2|...|pathN| -// |addr0 |addr1|addr2|...|addrN| (*addr0[0]=0; *addr0[1]=N) -// _______________________________ -// -// The structures of CPolytree64 and CPolytreeD are defined -// below and these structures don't need to be explained here. + +/* + Boolean clipping: + cliptype: None=0, Intersection=1, Union=2, Difference=3, Xor=4 + fillrule: EvenOdd=0, NonZero=1, Positive=2, Negative=3 + + Polygon offsetting (inflate/deflate): + jointype: Square=0, Bevel=1, Round=2, Miter=3 + endtype: Polygon=0, Joined=1, Butt=2, Square=3, Round=4 + +The path structures used extensively in other parts of this library are all +based on std::vector classes. Since C++ classes can't be accessed by other +languages, these paths must be converted into simple C data structures that +can be understood by just about any programming language. And these C style +path structures are simple arrays of int64_t (CPath64) and double (CPathD). + +CPath64 and CPathD: +These are arrays of consecutive x and y path coordinates preceeded by +a pair of values containing the path's length (N) and a 0 value. +__________________________________ +|counter|coord1|coord2|...|coordN| +|N, 0 |x1, y1|x2, y2|...|xN, yN| +__________________________________ + +CPaths64 and CPathsD: +These are also arrays containing any number of consecutive CPath64 or +CPathD structures. But preceeding these consecutive paths, there is pair of +values that contain the total length of the array (A) structure and +the number (C) of CPath64 or CPathD it contains. +_______________________________ +|counter|path1|path2|...|pathC| +|A , C | | +_______________________________ + +CPolytree64 and CPolytreeD: +These are also arrays consisting of CPolyPath structures that represent +individual paths in a tree structure. However, the very first (ie top) +CPolyPath is just the tree container that won't have a path. And because +of that, its structure will be very slightly different from the remaining +CPolyPath. This difference will be discussed below. + +CPolyPath64 and CPolyPathD: +These are simple arrays consisting of a series of path coordinates followed +by any number of child (ie nested) CPolyPath. Preceeding these are two values +indicating the length of the path (N) and the number of child CPolyPath (C). +____________________________________________________________ +|counter|coord1|coord2|...|coordN| child1|child2|...|childC| +|N , C |x1, y1|x2, y2|...|xN, yN| | +____________________________________________________________ + +As mentioned above, the very first CPolyPath structure is just a container +that owns (both directly and indirectly) every other CPolyPath in the tree. +Since this first CPolyPath has no path, instead of a path length, its very +first value will contain the total length of the CPolytree array structure. + +All theses exported structures (CPaths64, CPathsD, CPolyTree64 & CPolyTreeD) +are arrays of type int64_t or double. And the first value in these arrays +will always contain the length of that array. + +These array structures are allocated in heap memory which will eventually +need to be released. But since applications dynamically linking to these +functions may use different memory managers, the only safe way to free up +this memory is to use the exported DisposeArray64 and DisposeArrayD +functions below. +*/ + #ifndef CLIPPER2_EXPORT_H #define CLIPPER2_EXPORT_H @@ -49,25 +88,14 @@ namespace Clipper2Lib { typedef int64_t* CPath64; -typedef int64_t** CPaths64; -typedef double* CPathD; -typedef double** CPathsD; - -typedef struct CPolyPath64 { - CPath64 polygon; - uint32_t is_hole; - uint32_t child_count; - CPolyPath64* childs; -} -CPolyTree64; +typedef int64_t* CPaths64; +typedef double* CPathD; +typedef double* CPathsD; -typedef struct CPolyPathD { - CPathD polygon; - uint32_t is_hole; - uint32_t child_count; - CPolyPathD* childs; -} -CPolyTreeD; +typedef int64_t* CPolyPath64; +typedef int64_t* CPolyTree64; +typedef double* CPolyPathD; +typedef double* CPolyTreeD; template <typename T> struct CRect { @@ -97,57 +125,53 @@ inline Rect<T> CRectToRect(const CRect<T>& rect) return result; } -#define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport) +#ifdef _WIN32 + #define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport) +#else + #define EXTERN_DLL_EXPORT extern "C" +#endif + ////////////////////////////////////////////////////// -// EXPORTED FUNCTION DEFINITIONS +// EXPORTED FUNCTION DECLARATIONS ////////////////////////////////////////////////////// EXTERN_DLL_EXPORT const char* Version(); -// Some of the functions below will return data in the various CPath -// and CPolyTree structures which are pointers to heap allocated -// memory. Eventually this memory will need to be released with one -// of the following 'DisposeExported' functions. (This may be the -// only safe way to release this memory since the executable -// accessing these exported functions may use a memory manager that -// allocates and releases heap memory in a different way. Also, -// CPath structures that have been constructed by the executable -// should not be destroyed using these 'DisposeExported' functions.) -EXTERN_DLL_EXPORT void DisposeExportedCPath64(CPath64 p); -EXTERN_DLL_EXPORT void DisposeExportedCPaths64(CPaths64& pp); -EXTERN_DLL_EXPORT void DisposeExportedCPathD(CPathD p); -EXTERN_DLL_EXPORT void DisposeExportedCPathsD(CPathsD& pp); -EXTERN_DLL_EXPORT void DisposeExportedCPolyTree64(CPolyTree64*& cpt); -EXTERN_DLL_EXPORT void DisposeExportedCPolyTreeD(CPolyTreeD*& cpt); - -// Boolean clipping: -// cliptype: None=0, Intersection=1, Union=2, Difference=3, Xor=4 -// fillrule: EvenOdd=0, NonZero=1, Positive=2, Negative=3 +EXTERN_DLL_EXPORT void DisposeArray64(int64_t*& p) +{ + delete[] p; +} + +EXTERN_DLL_EXPORT void DisposeArrayD(double*& p) +{ + delete[] p; +} + EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, uint8_t fillrule, const CPaths64 subjects, const CPaths64 subjects_open, const CPaths64 clips, CPaths64& solution, CPaths64& solution_open, bool preserve_collinear = true, bool reverse_solution = false); -EXTERN_DLL_EXPORT int BooleanOpPt64(uint8_t cliptype, + +EXTERN_DLL_EXPORT int BooleanOp_PolyTree64(uint8_t cliptype, uint8_t fillrule, const CPaths64 subjects, const CPaths64 subjects_open, const CPaths64 clips, - CPolyTree64*& solution, CPaths64& solution_open, + CPolyTree64& sol_tree, CPaths64& solution_open, bool preserve_collinear = true, bool reverse_solution = false); + EXTERN_DLL_EXPORT int BooleanOpD(uint8_t cliptype, uint8_t fillrule, const CPathsD subjects, const CPathsD subjects_open, const CPathsD clips, CPathsD& solution, CPathsD& solution_open, int precision = 2, bool preserve_collinear = true, bool reverse_solution = false); -EXTERN_DLL_EXPORT int BooleanOpPtD(uint8_t cliptype, + +EXTERN_DLL_EXPORT int BooleanOp_PolyTreeD(uint8_t cliptype, uint8_t fillrule, const CPathsD subjects, const CPathsD subjects_open, const CPathsD clips, - CPolyTreeD*& solution, CPathsD& solution_open, int precision = 2, + CPolyTreeD& solution, CPathsD& solution_open, int precision = 2, bool preserve_collinear = true, bool reverse_solution = false); -// Polygon offsetting (inflate/deflate): -// jointype: Square=0, Round=1, Miter=2 -// endtype: Polygon=0, Joined=1, Butt=2, Square=3, Round=4 EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, double delta, uint8_t jointype, uint8_t endtype, double miter_limit = 2.0, double arc_tolerance = 0.0, @@ -157,78 +181,185 @@ EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, int precision = 2, double miter_limit = 2.0, double arc_tolerance = 0.0, bool reverse_solution = false); -// ExecuteRectClip & ExecuteRectClipLines: -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClip64(const CRect64& rect, - const CPaths64 paths, bool convex_only = false); -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipD(const CRectD& rect, - const CPathsD paths, int precision = 2, bool convex_only = false); -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClipLines64(const CRect64& rect, +// RectClip & RectClipLines: +EXTERN_DLL_EXPORT CPaths64 RectClip64(const CRect64& rect, + const CPaths64 paths); +EXTERN_DLL_EXPORT CPathsD RectClipD(const CRectD& rect, + const CPathsD paths, int precision = 2); +EXTERN_DLL_EXPORT CPaths64 RectClipLines64(const CRect64& rect, const CPaths64 paths); -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipLinesD(const CRectD& rect, +EXTERN_DLL_EXPORT CPathsD RectClipLinesD(const CRectD& rect, const CPathsD paths, int precision = 2); ////////////////////////////////////////////////////// // INTERNAL FUNCTIONS ////////////////////////////////////////////////////// -inline CPath64 CreateCPath64(size_t cnt1, size_t cnt2); -inline CPath64 CreateCPath64(const Path64& p); -inline CPaths64 CreateCPaths64(const Paths64& pp); -inline Path64 ConvertCPath64(const CPath64& p); -inline Paths64 ConvertCPaths64(const CPaths64& pp); +template <typename T> +static void GetPathCountAndCPathsArrayLen(const Paths<T>& paths, + size_t& cnt, size_t& array_len) +{ + array_len = 2; + cnt = 0; + for (const Path<T>& path : paths) + if (path.size()) + { + array_len += path.size() * 2 + 2; + ++cnt; + } +} -inline CPathD CreateCPathD(size_t cnt1, size_t cnt2); -inline CPathD CreateCPathD(const PathD& p); -inline CPathsD CreateCPathsD(const PathsD& pp); -inline PathD ConvertCPathD(const CPathD& p); -inline PathsD ConvertCPathsD(const CPathsD& pp); +static size_t GetPolyPath64ArrayLen(const PolyPath64& pp) +{ + size_t result = 2; // poly_length + child_count + result += pp.Polygon().size() * 2; + //plus nested children :) + for (size_t i = 0; i < pp.Count(); ++i) + result += GetPolyPath64ArrayLen(*pp[i]); + return result; +} -// the following function avoid multiple conversions -inline CPathD CreateCPathD(const Path64& p, double scale); -inline CPathsD CreateCPathsD(const Paths64& pp, double scale); -inline Path64 ConvertCPathD(const CPathD& p, double scale); -inline Paths64 ConvertCPathsD(const CPathsD& pp, double scale); +static void GetPolytreeCountAndCStorageSize(const PolyTree64& tree, + size_t& cnt, size_t& array_len) +{ + cnt = tree.Count(); // nb: top level count only + array_len = GetPolyPath64ArrayLen(tree); +} -inline CPolyTree64* CreateCPolyTree64(const PolyTree64& pt); -inline CPolyTreeD* CreateCPolyTreeD(const PolyTree64& pt, double scale); +template <typename T> +static T* CreateCPaths(const Paths<T>& paths) +{ + size_t cnt = 0, array_len = 0; + GetPathCountAndCPathsArrayLen(paths, cnt, array_len); + T* result = new T[array_len], * v = result; + *v++ = array_len; + *v++ = cnt; + for (const Path<T>& path : paths) + { + if (!path.size()) continue; + *v++ = path.size(); + *v++ = 0; + for (const Point<T>& pt : path) + { + *v++ = pt.x; + *v++ = pt.y; + } + } + return result; +} -EXTERN_DLL_EXPORT const char* Version() + +CPathsD CreateCPathsDFromPaths64(const Paths64& paths, double scale) { - return CLIPPER2_VERSION; + if (!paths.size()) return nullptr; + size_t cnt, array_len; + GetPathCountAndCPathsArrayLen(paths, cnt, array_len); + CPathsD result = new double[array_len], v = result; + *v++ = (double)array_len; + *v++ = (double)cnt; + for (const Path64& path : paths) + { + if (!path.size()) continue; + *v = (double)path.size(); + ++v; *v++ = 0; + for (const Point64& pt : path) + { + *v++ = pt.x * scale; + *v++ = pt.y * scale; + } + } + return result; } -EXTERN_DLL_EXPORT void DisposeExportedCPath64(CPath64 p) +template <typename T> +static Paths<T> ConvertCPaths(T* paths) +{ + Paths<T> result; + if (!paths) return result; + T* v = paths; ++v; + size_t cnt = *v++; + result.reserve(cnt); + for (size_t i = 0; i < cnt; ++i) + { + size_t cnt2 = *v; + v += 2; + Path<T> path; + path.reserve(cnt2); + for (size_t j = 0; j < cnt2; ++j) + { + T x = *v++, y = *v++; + path.push_back(Point<T>(x, y)); + } + result.push_back(path); + } + return result; +} + + +static Paths64 ConvertCPathsDToPaths64(const CPathsD paths, double scale) { - if (p) delete[] p; + Paths64 result; + if (!paths) return result; + double* v = paths; + ++v; // skip the first value (0) + int64_t cnt = (int64_t)*v++; + result.reserve(cnt); + for (int i = 0; i < cnt; ++i) + { + int64_t cnt2 = (int64_t)*v; + v += 2; + Path64 path; + path.reserve(cnt2); + for (int j = 0; j < cnt2; ++j) + { + double x = *v++ * scale; + double y = *v++ * scale; + path.push_back(Point64(x, y)); + } + result.push_back(path); + } + return result; } -EXTERN_DLL_EXPORT void DisposeExportedCPaths64(CPaths64& pp) +template <typename T> +static void CreateCPolyPath(const PolyPath64* pp, T*& v, T scale) { - if (!pp) return; - CPaths64 v = pp; - CPath64 cnts = *v; - const size_t cnt = static_cast<size_t>(cnts[1]); - for (size_t i = 0; i <= cnt; ++i) //nb: cnt +1 - DisposeExportedCPath64(*v++); - delete[] pp; - pp = nullptr; + *v++ = static_cast<T>(pp->Polygon().size()); + *v++ = static_cast<T>(pp->Count()); + for (const Point64& pt : pp->Polygon()) + { + *v++ = static_cast<T>(pt.x * scale); + *v++ = static_cast<T>(pt.y * scale); + } + for (size_t i = 0; i < pp->Count(); ++i) + CreateCPolyPath(pp->Child(i), v, scale); } -EXTERN_DLL_EXPORT void DisposeExportedCPathD(CPathD p) +template <typename T> +static T* CreateCPolyTree(const PolyTree64& tree, T scale) { - if (p) delete[] p; + if (scale == 0) scale = 1; + size_t cnt, array_len; + GetPolytreeCountAndCStorageSize(tree, cnt, array_len); + if (!cnt) return nullptr; + // allocate storage + T* result = new T[array_len]; + T* v = result; + + *v++ = static_cast<T>(array_len); + *v++ = static_cast<T>(tree.Count()); + for (size_t i = 0; i < tree.Count(); ++i) + CreateCPolyPath(tree.Child(i), v, scale); + return result; } -EXTERN_DLL_EXPORT void DisposeExportedCPathsD(CPathsD& pp) +////////////////////////////////////////////////////// +// EXPORTED FUNCTION DEFINITIONS +////////////////////////////////////////////////////// + +EXTERN_DLL_EXPORT const char* Version() { - if (!pp) return; - CPathsD v = pp; - CPathD cnts = *v; - size_t cnt = static_cast<size_t>(cnts[1]); - for (size_t i = 0; i <= cnt; ++i) //nb: cnt +1 - DisposeExportedCPathD(*v++); - delete[] pp; - pp = nullptr; + return CLIPPER2_VERSION; } EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, @@ -241,48 +372,48 @@ EXTERN_DLL_EXPORT int BooleanOp64(uint8_t cliptype, if (fillrule > static_cast<uint8_t>(FillRule::Negative)) return -3; Paths64 sub, sub_open, clp, sol, sol_open; - sub = ConvertCPaths64(subjects); - sub_open = ConvertCPaths64(subjects_open); - clp = ConvertCPaths64(clips); + sub = ConvertCPaths(subjects); + sub_open = ConvertCPaths(subjects_open); + clp = ConvertCPaths(clips); Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); if (sub.size() > 0) clipper.AddSubject(sub); if (sub_open.size() > 0) clipper.AddOpenSubject(sub_open); if (clp.size() > 0) clipper.AddClip(clp); if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), sol, sol_open)) return -1; // clipping bug - should never happen :) - solution = CreateCPaths64(sol); - solution_open = CreateCPaths64(sol_open); + solution = CreateCPaths(sol); + solution_open = CreateCPaths(sol_open); return 0; //success !! } -EXTERN_DLL_EXPORT int BooleanOpPt64(uint8_t cliptype, +EXTERN_DLL_EXPORT int BooleanOp_PolyTree64(uint8_t cliptype, uint8_t fillrule, const CPaths64 subjects, const CPaths64 subjects_open, const CPaths64 clips, - CPolyTree64*& solution, CPaths64& solution_open, + CPolyTree64& sol_tree, CPaths64& solution_open, bool preserve_collinear, bool reverse_solution) { if (cliptype > static_cast<uint8_t>(ClipType::Xor)) return -4; if (fillrule > static_cast<uint8_t>(FillRule::Negative)) return -3; Paths64 sub, sub_open, clp, sol_open; - sub = ConvertCPaths64(subjects); - sub_open = ConvertCPaths64(subjects_open); - clp = ConvertCPaths64(clips); + sub = ConvertCPaths(subjects); + sub_open = ConvertCPaths(subjects_open); + clp = ConvertCPaths(clips); - PolyTree64 pt; + PolyTree64 tree; Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); if (sub.size() > 0) clipper.AddSubject(sub); if (sub_open.size() > 0) clipper.AddOpenSubject(sub_open); if (clp.size() > 0) clipper.AddClip(clp); - if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), pt, sol_open)) + if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), tree, sol_open)) return -1; // clipping bug - should never happen :) - solution = CreateCPolyTree64(pt); - solution_open = CreateCPaths64(sol_open); + sol_tree = CreateCPolyTree(tree, (int64_t)1); + solution_open = CreateCPaths(sol_open); return 0; //success !! } @@ -298,57 +429,54 @@ EXTERN_DLL_EXPORT int BooleanOpD(uint8_t cliptype, const double scale = std::pow(10, precision); Paths64 sub, sub_open, clp, sol, sol_open; - sub = ConvertCPathsD(subjects, scale); - sub_open = ConvertCPathsD(subjects_open, scale); - clp = ConvertCPathsD(clips, scale); + sub = ConvertCPathsDToPaths64(subjects, scale); + sub_open = ConvertCPathsDToPaths64(subjects_open, scale); + clp = ConvertCPathsDToPaths64(clips, scale); Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); if (sub.size() > 0) clipper.AddSubject(sub); - if (sub_open.size() > 0) - clipper.AddOpenSubject(sub_open); + if (sub_open.size() > 0) clipper.AddOpenSubject(sub_open); if (clp.size() > 0) clipper.AddClip(clp); if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), sol, sol_open)) return -1; - - if (sol.size() > 0) solution = CreateCPathsD(sol, 1 / scale); - if (sol_open.size() > 0) - solution_open = CreateCPathsD(sol_open, 1 / scale); + solution = CreateCPathsDFromPaths64(sol, 1 / scale); + solution_open = CreateCPathsDFromPaths64(sol_open, 1 / scale); return 0; } -EXTERN_DLL_EXPORT int BooleanOpPtD(uint8_t cliptype, +EXTERN_DLL_EXPORT int BooleanOp_PolyTreeD(uint8_t cliptype, uint8_t fillrule, const CPathsD subjects, const CPathsD subjects_open, const CPathsD clips, - CPolyTreeD*& solution, CPathsD& solution_open, int precision, + CPolyTreeD& solution, CPathsD& solution_open, int precision, bool preserve_collinear, bool reverse_solution) { if (precision < -8 || precision > 8) return -5; if (cliptype > static_cast<uint8_t>(ClipType::Xor)) return -4; if (fillrule > static_cast<uint8_t>(FillRule::Negative)) return -3; - const double scale = std::pow(10, precision); + double scale = std::pow(10, precision); + + int err = 0; Paths64 sub, sub_open, clp, sol_open; - sub = ConvertCPathsD(subjects, scale); - sub_open = ConvertCPathsD(subjects_open, scale); - clp = ConvertCPathsD(clips, scale); + sub = ConvertCPathsDToPaths64(subjects, scale); + sub_open = ConvertCPathsDToPaths64(subjects_open, scale); + clp = ConvertCPathsDToPaths64(clips, scale); - PolyTree64 sol; + PolyTree64 tree; Clipper64 clipper; - clipper.PreserveCollinear = preserve_collinear; - clipper.ReverseSolution = reverse_solution; + clipper.PreserveCollinear(preserve_collinear); + clipper.ReverseSolution(reverse_solution); if (sub.size() > 0) clipper.AddSubject(sub); - if (sub_open.size() > 0) - clipper.AddOpenSubject(sub_open); + if (sub_open.size() > 0) clipper.AddOpenSubject(sub_open); if (clp.size() > 0) clipper.AddClip(clp); - if (!clipper.Execute(ClipType(cliptype), - FillRule(fillrule), sol, sol_open)) return -1; + if (!clipper.Execute(ClipType(cliptype), FillRule(fillrule), tree, sol_open)) + return -1; // clipping bug - should never happen :) - solution = CreateCPolyTreeD(sol, 1 / scale); - if (sol_open.size() > 0) - solution_open = CreateCPathsD(sol_open, 1 / scale); - return 0; + solution = CreateCPolyTree(tree, 1/scale); + solution_open = CreateCPathsDFromPaths64(sol_open, 1 / scale); + return 0; //success !! } EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, @@ -356,14 +484,13 @@ EXTERN_DLL_EXPORT CPaths64 InflatePaths64(const CPaths64 paths, double arc_tolerance, bool reverse_solution) { Paths64 pp; - pp = ConvertCPaths64(paths); - + pp = ConvertCPaths(paths); ClipperOffset clip_offset( miter_limit, arc_tolerance, reverse_solution); clip_offset.AddPaths(pp, JoinType(jointype), EndType(endtype)); Paths64 result; clip_offset.Execute(delta, result); - return CreateCPaths64(result); + return CreateCPaths(result); } EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, @@ -372,28 +499,28 @@ EXTERN_DLL_EXPORT CPathsD InflatePathsD(const CPathsD paths, double arc_tolerance, bool reverse_solution) { if (precision < -8 || precision > 8 || !paths) return nullptr; + const double scale = std::pow(10, precision); ClipperOffset clip_offset(miter_limit, arc_tolerance, reverse_solution); - Paths64 pp = ConvertCPathsD(paths, scale); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); clip_offset.AddPaths(pp, JoinType(jointype), EndType(endtype)); Paths64 result; clip_offset.Execute(delta * scale, result); - return CreateCPathsD(result, 1/scale); + + return CreateCPathsDFromPaths64(result, 1 / scale); } -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClip64(const CRect64& rect, - const CPaths64 paths, bool convex_only) +EXTERN_DLL_EXPORT CPaths64 RectClip64(const CRect64& rect, const CPaths64 paths) { if (CRectIsEmpty(rect) || !paths) return nullptr; Rect64 r64 = CRectToRect(rect); - class RectClip rc(r64); - Paths64 pp = ConvertCPaths64(paths); - Paths64 result = rc.Execute(pp, convex_only); - return CreateCPaths64(result); + class RectClip64 rc(r64); + Paths64 pp = ConvertCPaths(paths); + Paths64 result = rc.Execute(pp); + return CreateCPaths(result); } -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipD(const CRectD& rect, - const CPathsD paths, int precision, bool convex_only) +EXTERN_DLL_EXPORT CPathsD RectClipD(const CRectD& rect, const CPathsD paths, int precision) { if (CRectIsEmpty(rect) || !paths) return nullptr; if (precision < -8 || precision > 8) return nullptr; @@ -401,372 +528,36 @@ EXTERN_DLL_EXPORT CPathsD ExecuteRectClipD(const CRectD& rect, RectD r = CRectToRect(rect); Rect64 rec = ScaleRect<int64_t, double>(r, scale); - Paths64 pp = ConvertCPathsD(paths, scale); - class RectClip rc(rec); - Paths64 result = rc.Execute(pp, convex_only); - return CreateCPathsD(result, 1/scale); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); + class RectClip64 rc(rec); + Paths64 result = rc.Execute(pp); + + return CreateCPathsDFromPaths64(result, 1 / scale); } -EXTERN_DLL_EXPORT CPaths64 ExecuteRectClipLines64(const CRect64& rect, +EXTERN_DLL_EXPORT CPaths64 RectClipLines64(const CRect64& rect, const CPaths64 paths) { if (CRectIsEmpty(rect) || !paths) return nullptr; Rect64 r = CRectToRect(rect); - class RectClipLines rcl (r); - Paths64 pp = ConvertCPaths64(paths); + class RectClipLines64 rcl (r); + Paths64 pp = ConvertCPaths(paths); Paths64 result = rcl.Execute(pp); - return CreateCPaths64(result); + return CreateCPaths(result); } -EXTERN_DLL_EXPORT CPathsD ExecuteRectClipLinesD(const CRectD& rect, +EXTERN_DLL_EXPORT CPathsD RectClipLinesD(const CRectD& rect, const CPathsD paths, int precision) { if (CRectIsEmpty(rect) || !paths) return nullptr; if (precision < -8 || precision > 8) return nullptr; + const double scale = std::pow(10, precision); Rect64 r = ScaleRect<int64_t, double>(CRectToRect(rect), scale); - class RectClipLines rcl(r); - Paths64 pp = ConvertCPathsD(paths, scale); + class RectClipLines64 rcl(r); + Paths64 pp = ConvertCPathsDToPaths64(paths, scale); Paths64 result = rcl.Execute(pp); - return CreateCPathsD(result, 1/scale); -} - -inline CPath64 CreateCPath64(size_t cnt1, size_t cnt2) -{ - // allocates memory for CPath64, fills in the counter, and - // returns the structure ready to be filled with path data - CPath64 result = new int64_t[2 + cnt1 *2]; - result[0] = cnt1; - result[1] = cnt2; - return result; -} - -inline CPath64 CreateCPath64(const Path64& p) -{ - // allocates memory for CPath64, fills the counter - // and returns the memory filled with path data - size_t cnt = p.size(); - if (!cnt) return nullptr; - CPath64 result = CreateCPath64(cnt, 0); - CPath64 v = result; - v += 2; // skip counters - for (const Point64& pt : p) - { - *v++ = pt.x; - *v++ = pt.y; - } - return result; -} - -inline Path64 ConvertCPath64(const CPath64& p) -{ - Path64 result; - if (p && *p) - { - CPath64 v = p; - const size_t cnt = static_cast<size_t>(p[0]); - v += 2; // skip counters - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - { - // x,y here avoids right to left function evaluation - // result.push_back(Point64(*v++, *v++)); - int64_t x = *v++; - int64_t y = *v++; - result.push_back(Point64(x, y)); - } - } - return result; -} - -inline CPaths64 CreateCPaths64(const Paths64& pp) -{ - // allocates memory for multiple CPath64 and - // and returns this memory filled with path data - size_t cnt = pp.size(), cnt2 = cnt; - - // don't allocate space for empty paths - for (size_t i = 0; i < cnt; ++i) - if (!pp[i].size()) --cnt2; - if (!cnt2) return nullptr; - - CPaths64 result = new int64_t* [cnt2 + 1]; - CPaths64 v = result; - *v++ = CreateCPath64(0, cnt2); // assign a counter path - for (const Path64& p : pp) - { - *v = CreateCPath64(p); - if (*v) ++v; - } - return result; -} - -inline Paths64 ConvertCPaths64(const CPaths64& pp) -{ - Paths64 result; - if (pp) - { - CPaths64 v = pp; - CPath64 cnts = pp[0]; - const size_t cnt = static_cast<size_t>(cnts[1]); // nb 2nd cnt - ++v; // skip cnts - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - result.push_back(ConvertCPath64(*v++)); - } - return result; -} - -inline CPathD CreateCPathD(size_t cnt1, size_t cnt2) -{ - // allocates memory for CPathD, fills in the counter, and - // returns the structure ready to be filled with path data - CPathD result = new double[2 + cnt1 * 2]; - result[0] = static_cast<double>(cnt1); - result[1] = static_cast<double>(cnt2); - return result; -} - -inline CPathD CreateCPathD(const PathD& p) -{ - // allocates memory for CPath, fills the counter - // and returns the memory fills with path data - size_t cnt = p.size(); - if (!cnt) return nullptr; - CPathD result = CreateCPathD(cnt, 0); - CPathD v = result; - v += 2; // skip counters - for (const PointD& pt : p) - { - *v++ = pt.x; - *v++ = pt.y; - } - return result; -} - -inline PathD ConvertCPathD(const CPathD& p) -{ - PathD result; - if (p) - { - CPathD v = p; - size_t cnt = static_cast<size_t>(v[0]); - v += 2; // skip counters - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - { - // x,y here avoids right to left function evaluation - // result.push_back(PointD(*v++, *v++)); - double x = *v++; - double y = *v++; - result.push_back(PointD(x, y)); - } - } - return result; -} - -inline CPathsD CreateCPathsD(const PathsD& pp) -{ - size_t cnt = pp.size(), cnt2 = cnt; - // don't allocate space for empty paths - for (size_t i = 0; i < cnt; ++i) - if (!pp[i].size()) --cnt2; - if (!cnt2) return nullptr; - CPathsD result = new double * [cnt2 + 1]; - CPathsD v = result; - *v++ = CreateCPathD(0, cnt2); // assign counter path - for (const PathD& p : pp) - { - *v = CreateCPathD(p); - if (*v) { ++v; } - } - return result; -} - -inline PathsD ConvertCPathsD(const CPathsD& pp) -{ - PathsD result; - if (pp) - { - CPathsD v = pp; - CPathD cnts = v[0]; - size_t cnt = static_cast<size_t>(cnts[1]); - ++v; // skip cnts path - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - result.push_back(ConvertCPathD(*v++)); - } - return result; -} - -inline Path64 ConvertCPathD(const CPathD& p, double scale) -{ - Path64 result; - if (p) - { - CPathD v = p; - size_t cnt = static_cast<size_t>(*v); - v += 2; // skip counters - result.reserve(cnt); - for (size_t i = 0; i < cnt; ++i) - { - // x,y here avoids right to left function evaluation - // result.push_back(PointD(*v++, *v++)); - double x = *v++ * scale; - double y = *v++ * scale; - result.push_back(Point64(x, y)); - } - } - return result; -} - -inline Paths64 ConvertCPathsD(const CPathsD& pp, double scale) -{ - Paths64 result; - if (pp) - { - CPathsD v = pp; - CPathD cnts = v[0]; - size_t cnt = static_cast<size_t>(cnts[1]); - result.reserve(cnt); - ++v; // skip cnts path - for (size_t i = 0; i < cnt; ++i) - result.push_back(ConvertCPathD(*v++, scale)); - } - return result; -} - -inline CPathD CreateCPathD(const Path64& p, double scale) -{ - // allocates memory for CPathD, fills in the counter, and - // returns the structure filled with *scaled* path data - size_t cnt = p.size(); - if (!cnt) return nullptr; - CPathD result = CreateCPathD(cnt, 0); - CPathD v = result; - v += 2; // skip cnts - for (const Point64& pt : p) - { - *v++ = pt.x * scale; - *v++ = pt.y * scale; - } - return result; -} - -inline CPathsD CreateCPathsD(const Paths64& pp, double scale) -{ - // allocates memory for *multiple* CPathD, and - // returns the structure filled with scaled path data - size_t cnt = pp.size(), cnt2 = cnt; - // don't allocate space for empty paths - for (size_t i = 0; i < cnt; ++i) - if (!pp[i].size()) --cnt2; - if (!cnt2) return nullptr; - CPathsD result = new double* [cnt2 + 1]; - CPathsD v = result; - *v++ = CreateCPathD(0, cnt2); - for (const Path64& p : pp) - { - *v = CreateCPathD(p, scale); - if (*v) ++v; - } - return result; -} - -inline void InitCPolyPath64(CPolyTree64* cpt, - bool is_hole, const std::unique_ptr <PolyPath64>& pp) -{ - cpt->polygon = CreateCPath64(pp->Polygon()); - cpt->is_hole = is_hole; - size_t child_cnt = pp->Count(); - cpt->child_count = static_cast<uint32_t>(child_cnt); - cpt->childs = nullptr; - if (!child_cnt) return; - cpt->childs = new CPolyPath64[child_cnt]; - CPolyPath64* child = cpt->childs; - for (const std::unique_ptr <PolyPath64>& pp_child : *pp) - InitCPolyPath64(child++, !is_hole, pp_child); -} - -inline CPolyTree64* CreateCPolyTree64(const PolyTree64& pt) -{ - CPolyTree64* result = new CPolyTree64(); - result->polygon = nullptr; - result->is_hole = false; - size_t child_cnt = pt.Count(); - result->childs = nullptr; - result->child_count = static_cast<uint32_t>(child_cnt); - if (!child_cnt) return result; - result->childs = new CPolyPath64[child_cnt]; - CPolyPath64* child = result->childs; - for (const std::unique_ptr <PolyPath64>& pp : pt) - InitCPolyPath64(child++, true, pp); - return result; -} - -inline void DisposeCPolyPath64(CPolyPath64* cpp) -{ - if (!cpp->child_count) return; - CPolyPath64* child = cpp->childs; - for (size_t i = 0; i < cpp->child_count; ++i) - DisposeCPolyPath64(child); - delete[] cpp->childs; -} - -EXTERN_DLL_EXPORT void DisposeExportedCPolyTree64(CPolyTree64*& cpt) -{ - if (!cpt) return; - DisposeCPolyPath64(cpt); - delete cpt; - cpt = nullptr; -} - -inline void InitCPolyPathD(CPolyTreeD* cpt, - bool is_hole, const std::unique_ptr <PolyPath64>& pp, double scale) -{ - cpt->polygon = CreateCPathD(pp->Polygon(), scale); - cpt->is_hole = is_hole; - size_t child_cnt = pp->Count(); - cpt->child_count = static_cast<uint32_t>(child_cnt); - cpt->childs = nullptr; - if (!child_cnt) return; - cpt->childs = new CPolyPathD[child_cnt]; - CPolyPathD* child = cpt->childs; - for (const std::unique_ptr <PolyPath64>& pp_child : *pp) - InitCPolyPathD(child++, !is_hole, pp_child, scale); -} - -inline CPolyTreeD* CreateCPolyTreeD(const PolyTree64& pt, double scale) -{ - CPolyTreeD* result = new CPolyTreeD(); - result->polygon = nullptr; - result->is_hole = false; - size_t child_cnt = pt.Count(); - result->child_count = static_cast<uint32_t>(child_cnt); - result->childs = nullptr; - if (!child_cnt) return result; - result->childs = new CPolyPathD[child_cnt]; - CPolyPathD* child = result->childs; - for (const std::unique_ptr <PolyPath64>& pp : pt) - InitCPolyPathD(child++, true, pp, scale); - return result; -} - -inline void DisposeCPolyPathD(CPolyPathD* cpp) -{ - if (!cpp->child_count) return; - CPolyPathD* child = cpp->childs; - for (size_t i = 0; i < cpp->child_count; ++i) - DisposeCPolyPathD(child++); - delete[] cpp->childs; -} - -EXTERN_DLL_EXPORT void DisposeExportedCPolyTreeD(CPolyTreeD*& cpt) -{ - if (!cpt) return; - DisposeCPolyPathD(cpt); - delete cpt; - cpt = nullptr; + return CreateCPathsDFromPaths64(result, 1 / scale); } } // end Clipper2Lib namespace diff --git a/thirdparty/clipper2/include/clipper2/clipper.h b/thirdparty/clipper2/include/clipper2/clipper.h index 6579f59c18..0f516b60e8 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.h +++ b/thirdparty/clipper2/include/clipper2/clipper.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 23 March 2023 * +* Date : 18 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -14,11 +14,11 @@ #include <type_traits> #include <vector> -#include "clipper.core.h" -#include "clipper.engine.h" -#include "clipper.offset.h" -#include "clipper.minkowski.h" -#include "clipper.rectclip.h" +#include "clipper2/clipper.core.h" +#include "clipper2/clipper.engine.h" +#include "clipper2/clipper.offset.h" +#include "clipper2/clipper.minkowski.h" +#include "clipper2/clipper.rectclip.h" namespace Clipper2Lib { @@ -161,60 +161,61 @@ namespace Clipper2Lib { return ScalePaths<double, int64_t>(solution, 1 / scale, error_code); } - inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + template <typename T> + inline Path<T> TranslatePath(const Path<T>& path, T dx, T dy) { - Path64 result; + Path<T> result; result.reserve(path.size()); std::transform(path.begin(), path.end(), back_inserter(result), - [dx, dy](const auto& pt) { return Point64(pt.x + dx, pt.y +dy); }); + [dx, dy](const auto& pt) { return Point<T>(pt.x + dx, pt.y +dy); }); return result; } + inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + { + return TranslatePath<int64_t>(path, dx, dy); + } + inline PathD TranslatePath(const PathD& path, double dx, double dy) { - PathD result; - result.reserve(path.size()); - std::transform(path.begin(), path.end(), back_inserter(result), - [dx, dy](const auto& pt) { return PointD(pt.x + dx, pt.y + dy); }); - return result; + return TranslatePath<double>(path, dx, dy); } - inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + template <typename T> + inline Paths<T> TranslatePaths(const Paths<T>& paths, T dx, T dy) { - Paths64 result; + Paths<T> result; result.reserve(paths.size()); std::transform(paths.begin(), paths.end(), back_inserter(result), [dx, dy](const auto& path) { return TranslatePath(path, dx, dy); }); return result; } + inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + { + return TranslatePaths<int64_t>(paths, dx, dy); + } + inline PathsD TranslatePaths(const PathsD& paths, double dx, double dy) { - PathsD result; - result.reserve(paths.size()); - std::transform(paths.begin(), paths.end(), back_inserter(result), - [dx, dy](const auto& path) { return TranslatePath(path, dx, dy); }); - return result; + return TranslatePaths<double>(paths, dx, dy); } - inline Paths64 ExecuteRectClip(const Rect64& rect, - const Paths64& paths, bool convex_only = false) + inline Paths64 RectClip(const Rect64& rect, const Paths64& paths) { if (rect.IsEmpty() || paths.empty()) return Paths64(); - RectClip rc(rect); - return rc.Execute(paths, convex_only); + RectClip64 rc(rect); + return rc.Execute(paths); } - inline Paths64 ExecuteRectClip(const Rect64& rect, - const Path64& path, bool convex_only = false) + inline Paths64 RectClip(const Rect64& rect, const Path64& path) { if (rect.IsEmpty() || path.empty()) return Paths64(); - RectClip rc(rect); - return rc.Execute(Paths64{ path }, convex_only); + RectClip64 rc(rect); + return rc.Execute(Paths64{ path }); } - inline PathsD ExecuteRectClip(const RectD& rect, - const PathsD& paths, bool convex_only = false, int precision = 2) + inline PathsD RectClip(const RectD& rect, const PathsD& paths, int precision = 2) { if (rect.IsEmpty() || paths.empty()) return PathsD(); int error_code = 0; @@ -222,37 +223,31 @@ namespace Clipper2Lib { if (error_code) return PathsD(); const double scale = std::pow(10, precision); Rect64 r = ScaleRect<int64_t, double>(rect, scale); - RectClip rc(r); + RectClip64 rc(r); Paths64 pp = ScalePaths<int64_t, double>(paths, scale, error_code); if (error_code) return PathsD(); // ie: error_code result is lost return ScalePaths<double, int64_t>( - rc.Execute(pp, convex_only), 1 / scale, error_code); + rc.Execute(pp), 1 / scale, error_code); } - inline PathsD ExecuteRectClip(const RectD& rect, - const PathD& path, bool convex_only = false, int precision = 2) + inline PathsD RectClip(const RectD& rect, const PathD& path, int precision = 2) { - return ExecuteRectClip(rect, PathsD{ path }, convex_only, precision); + return RectClip(rect, PathsD{ path }, precision); } - inline Paths64 ExecuteRectClipLines(const Rect64& rect, const Paths64& lines) + inline Paths64 RectClipLines(const Rect64& rect, const Paths64& lines) { if (rect.IsEmpty() || lines.empty()) return Paths64(); - RectClipLines rcl(rect); + RectClipLines64 rcl(rect); return rcl.Execute(lines); } - inline Paths64 ExecuteRectClipLines(const Rect64& rect, const Path64& line) - { - return ExecuteRectClipLines(rect, Paths64{ line }); - } - - inline PathsD ExecuteRectClipLines(const RectD& rect, const PathD& line, int precision = 2) + inline Paths64 RectClipLines(const Rect64& rect, const Path64& line) { - return ExecuteRectClip(rect, PathsD{ line }, precision); + return RectClipLines(rect, Paths64{ line }); } - inline PathsD ExecuteRectClipLines(const RectD& rect, const PathsD& lines, int precision = 2) + inline PathsD RectClipLines(const RectD& rect, const PathsD& lines, int precision = 2) { if (rect.IsEmpty() || lines.empty()) return PathsD(); int error_code = 0; @@ -260,13 +255,18 @@ namespace Clipper2Lib { if (error_code) return PathsD(); const double scale = std::pow(10, precision); Rect64 r = ScaleRect<int64_t, double>(rect, scale); - RectClipLines rcl(r); + RectClipLines64 rcl(r); Paths64 p = ScalePaths<int64_t, double>(lines, scale, error_code); if (error_code) return PathsD(); p = rcl.Execute(p); return ScalePaths<double, int64_t>(p, 1 / scale, error_code); } + inline PathsD RectClipLines(const RectD& rect, const PathD& line, int precision = 2) + { + return RectClipLines(rect, PathsD{ line }, precision); + } + namespace details { @@ -290,14 +290,9 @@ namespace Clipper2Lib { { // return false if this child isn't fully contained by its parent - // the following algorithm is a bit too crude, and doesn't account - // for rounding errors. A better algorithm is to return false when - // consecutive vertices are found outside the parent's polygon. - - //const Path64& path = pp.Polygon(); - //if (std::any_of(child->Polygon().cbegin(), child->Polygon().cend(), - // [path](const auto& pt) {return (PointInPolygon(pt, path) == - // PointInPolygonResult::IsOutside); })) return false; + // checking for a single vertex outside is a bit too crude since + // it doesn't account for rounding errors. It's better to check + // for consecutive vertices found outside the parent's polygon. int outsideCnt = 0; for (const Point64& pt : child->Polygon()) @@ -317,74 +312,68 @@ namespace Clipper2Lib { } static void OutlinePolyPath(std::ostream& os, - bool isHole, size_t count, const std::string& preamble) + size_t idx, bool isHole, size_t count, const std::string& preamble) { std::string plural = (count == 1) ? "." : "s."; if (isHole) - { - if (count) - os << preamble << "+- Hole with " << count << - " nested polygon" << plural << std::endl; - else - os << preamble << "+- Hole" << std::endl; - } + os << preamble << "+- Hole (" << idx << ") contains " << count << + " nested polygon" << plural << std::endl; else - { - if (count) - os << preamble << "+- Polygon with " << count << + os << preamble << "+- Polygon (" << idx << ") contains " << count << " hole" << plural << std::endl; - else - os << preamble << "+- Polygon" << std::endl; - } } static void OutlinePolyPath64(std::ostream& os, const PolyPath64& pp, - std::string preamble, bool last_child) + size_t idx, std::string preamble) { - OutlinePolyPath(os, pp.IsHole(), pp.Count(), preamble); - preamble += (!last_child) ? "| " : " "; - if (pp.Count()) - { - PolyPath64List::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - OutlinePolyPath64(os, **it, preamble, false); - OutlinePolyPath64(os, **it, preamble, true); - } + OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPath64(os, *pp.Child(i), i, preamble + " "); } static void OutlinePolyPathD(std::ostream& os, const PolyPathD& pp, - std::string preamble, bool last_child) + size_t idx, std::string preamble) { - OutlinePolyPath(os, pp.IsHole(), pp.Count(), preamble); - preamble += (!last_child) ? "| " : " "; - if (pp.Count()) - { - PolyPathDList::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - OutlinePolyPathD(os, **it, preamble, false); - OutlinePolyPathD(os, **it, preamble, true); - } + OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " "); + } + + template<typename T, typename U> + inline constexpr void MakePathGeneric(const T an_array, + size_t array_size, std::vector<U>& result) + { + result.reserve(array_size / 2); + for (size_t i = 0; i < array_size; i +=2) +#ifdef USINGZ + result.push_back( U{ an_array[i], an_array[i +1], 0} ); +#else + result.push_back( U{ an_array[i], an_array[i + 1]} ); +#endif } } // end details namespace inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) { - PolyPath64List::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - details::OutlinePolyPath64(os, **it, " ", false); - details::OutlinePolyPath64(os, **it, " ", true); + std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; + os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPath64(os, *pp.Child(i), i, " "); os << std::endl << std::endl; - if (!pp.Level()) os << std::endl; return os; } inline std::ostream& operator<< (std::ostream& os, const PolyTreeD& pp) { - PolyPathDList::const_iterator it = pp.begin(); - for (; it < pp.end() - 1; ++it) - details::OutlinePolyPathD(os, **it, " ", false); - details::OutlinePolyPathD(os, **it, " ", true); + std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; + os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; + for (size_t i = 0; i < pp.Count(); ++i) + if (pp.Child(i)->Count()) + details::OutlinePolyPathD(os, *pp.Child(i), i, " "); os << std::endl << std::endl; if (!pp.Level()) os << std::endl; return os; @@ -415,22 +404,6 @@ namespace Clipper2Lib { return true; } - namespace details { - - template<typename T, typename U> - inline constexpr void MakePathGeneric(const T list, size_t size, - std::vector<U>& result) - { - for (size_t i = 0; i < size; ++i) -#ifdef USINGZ - result[i / 2] = U{list[i], list[++i], 0}; -#else - result[i / 2] = U{list[i], list[++i]}; -#endif - } - - } // end details namespace - template<typename T, typename std::enable_if< std::is_integral<T>::value && @@ -441,7 +414,7 @@ namespace Clipper2Lib { const auto size = list.size() - list.size() % 2; if (list.size() != size) DoError(non_pair_error_i); // non-fatal without exception handling - Path64 result(size / 2); // else ignores unpaired value + Path64 result; details::MakePathGeneric(list, size, result); return result; } @@ -455,7 +428,7 @@ namespace Clipper2Lib { { // Make the compiler error on unpaired value (i.e. no runtime effects). static_assert(N % 2 == 0, "MakePath requires an even number of arguments"); - Path64 result(N / 2); + Path64 result; details::MakePathGeneric(list, N, result); return result; } @@ -470,7 +443,7 @@ namespace Clipper2Lib { const auto size = list.size() - list.size() % 2; if (list.size() != size) DoError(non_pair_error_i); // non-fatal without exception handling - PathD result(size / 2); // else ignores unpaired value + PathD result; details::MakePathGeneric(list, size, result); return result; } @@ -484,11 +457,44 @@ namespace Clipper2Lib { { // Make the compiler error on unpaired value (i.e. no runtime effects). static_assert(N % 2 == 0, "MakePath requires an even number of arguments"); - PathD result(N / 2); + PathD result; details::MakePathGeneric(list, N, result); return result; } +#ifdef USINGZ + template<typename T2, std::size_t N> + inline Path64 MakePathZ(const T2(&list)[N]) + { + static_assert(N % 3 == 0 && std::numeric_limits<T2>::is_integer, + "MakePathZ requires integer values in multiples of 3"); + std::size_t size = N / 3; + Path64 result(size); + for (size_t i = 0; i < size; ++i) + result[i] = Point64(list[i * 3], + list[i * 3 + 1], list[i * 3 + 2]); + return result; + } + + template<typename T2, std::size_t N> + inline PathD MakePathZD(const T2(&list)[N]) + { + static_assert(N % 3 == 0, + "MakePathZD requires values in multiples of 3"); + std::size_t size = N / 3; + PathD result(size); + if constexpr (std::numeric_limits<T2>::is_integer) + for (size_t i = 0; i < size; ++i) + result[i] = PointD(list[i * 3], + list[i * 3 + 1], list[i * 3 + 2]); + else + for (size_t i = 0; i < size; ++i) + result[i] = PointD(list[i * 3], list[i * 3 + 1], + static_cast<int64_t>(list[i * 3 + 2])); + return result; + } +#endif + inline Path64 TrimCollinear(const Path64& p, bool is_open_path = false) { size_t len = p.size(); @@ -644,8 +650,8 @@ namespace Clipper2Lib { } template <typename T> - inline Path<T> SimplifyPath(const Path<T> path, - double epsilon, bool isOpenPath = false) + inline Path<T> SimplifyPath(const Path<T> &path, + double epsilon, bool isClosedPath = true) { const size_t len = path.size(), high = len -1; const double epsSqr = Sqr(epsilon); @@ -653,16 +659,16 @@ namespace Clipper2Lib { std::vector<bool> flags(len); std::vector<double> distSqr(len); - size_t prior = high, curr = 0, start, next, prior2, next2; - if (isOpenPath) + size_t prior = high, curr = 0, start, next, prior2; + if (isClosedPath) { - distSqr[0] = MAX_DBL; - distSqr[high] = MAX_DBL; + distSqr[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); + distSqr[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); } else { - distSqr[0] = PerpendicDistFromLineSqrd(path[0], path[high], path[1]); - distSqr[high] = PerpendicDistFromLineSqrd(path[high], path[0], path[high - 1]); + distSqr[0] = MAX_DBL; + distSqr[high] = MAX_DBL; } for (size_t i = 1; i < high; ++i) distSqr[i] = PerpendicDistFromLineSqrd(path[i], path[i - 1], path[i + 1]); @@ -683,26 +689,25 @@ namespace Clipper2Lib { next = GetNext(curr, high, flags); if (next == prior) break; + // flag for removal the smaller of adjacent 'distances' if (distSqr[next] < distSqr[curr]) { - flags[next] = true; - next = GetNext(next, high, flags); - next2 = GetNext(next, high, flags); - distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]); - if (next != high || !isOpenPath) - distSqr[next] = PerpendicDistFromLineSqrd(path[next], path[curr], path[next2]); + prior2 = prior; + prior = curr; curr = next; + next = GetNext(next, high, flags); } else - { - flags[curr] = true; - curr = next; - next = GetNext(next, high, flags); prior2 = GetPrior(prior, high, flags); + + flags[curr] = true; + curr = next; + next = GetNext(next, high, flags); + + if (isClosedPath || ((curr != high) && (curr != 0))) distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]); - if (prior != 0 || !isOpenPath) - distSqr[prior] = PerpendicDistFromLineSqrd(path[prior], path[prior2], path[curr]); - } + if (isClosedPath || ((prior != 0) && (prior != high))) + distSqr[prior] = PerpendicDistFromLineSqrd(path[prior], path[prior2], path[curr]); } Path<T> result; result.reserve(len); @@ -712,13 +717,13 @@ namespace Clipper2Lib { } template <typename T> - inline Paths<T> SimplifyPaths(const Paths<T> paths, - double epsilon, bool isOpenPath = false) + inline Paths<T> SimplifyPaths(const Paths<T> &paths, + double epsilon, bool isClosedPath = true) { Paths<T> result; result.reserve(paths.size()); for (const auto& path : paths) - result.push_back(SimplifyPath(path, epsilon, isOpenPath)); + result.push_back(SimplifyPath(path, epsilon, isClosedPath)); return result; } diff --git a/thirdparty/clipper2/include/clipper2/clipper.minkowski.h b/thirdparty/clipper2/include/clipper2/clipper.minkowski.h index 71c221bb50..ebddd08a59 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.minkowski.h +++ b/thirdparty/clipper2/include/clipper2/clipper.minkowski.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 28 January 2023 * +* Date : 1 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Minkowski Sum and Difference * @@ -13,7 +13,7 @@ #include <cstdlib> #include <vector> #include <string> -#include "clipper.core.h" +#include "clipper2/clipper.core.h" namespace Clipper2Lib { diff --git a/thirdparty/clipper2/include/clipper2/clipper.offset.h b/thirdparty/clipper2/include/clipper2/clipper.offset.h index f5d47e07ee..30992bfa55 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.offset.h +++ b/thirdparty/clipper2/include/clipper2/clipper.offset.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 March 2023 * +* Date : 19 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -15,7 +15,9 @@ namespace Clipper2Lib { -enum class JoinType { Square, Round, Miter }; +enum class JoinType { Square, Bevel, Round, Miter }; +//Square : Joins are 'squared' at exactly the offset distance (more complex code) +//Bevel : Similar to Square, but the offset distance varies with angle (simple code & faster) enum class EndType {Polygon, Joined, Butt, Square, Round}; //Butt : offsets both sides of a path, with square blunt ends @@ -24,6 +26,7 @@ enum class EndType {Polygon, Joined, Butt, Square, Round}; //Joined : offsets both sides of a path, with joined ends //Polygon: offsets only one side of a closed path +typedef std::function<double(const Path64& path, const PathD& path_normals, size_t curr_idx, size_t prev_idx)> DeltaCallback64; class ClipperOffset { private: @@ -31,27 +34,27 @@ private: class Group { public: Paths64 paths_in; - Paths64 paths_out; - Path64 path; + std::vector<bool> is_hole_list; + std::vector<Rect64> bounds_list; + int lowest_path_idx = -1; bool is_reversed = false; JoinType join_type; EndType end_type; - Group(const Paths64& _paths, JoinType _join_type, EndType _end_type) : - paths_in(_paths), join_type(_join_type), end_type(_end_type) {} + Group(const Paths64& _paths, JoinType _join_type, EndType _end_type); }; int error_code_ = 0; double delta_ = 0.0; double group_delta_ = 0.0; - double abs_group_delta_ = 0.0; double temp_lim_ = 0.0; double steps_per_rad_ = 0.0; double step_sin_ = 0.0; double step_cos_ = 0.0; PathD norms; + Path64 path_out; Paths64 solution; std::vector<Group> groups_; - JoinType join_type_ = JoinType::Square; + JoinType join_type_ = JoinType::Bevel; EndType end_type_ = EndType::Polygon; double miter_limit_ = 0.0; @@ -62,15 +65,19 @@ private: #ifdef USINGZ ZCallback64 zCallback64_ = nullptr; #endif - - void DoSquare(Group& group, const Path64& path, size_t j, size_t k); - void DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a); - void DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle); + DeltaCallback64 deltaCallback64_ = nullptr; + + size_t CalcSolutionCapacity(); + bool CheckReverseOrientation(); + void DoBevel(const Path64& path, size_t j, size_t k); + void DoSquare(const Path64& path, size_t j, size_t k); + void DoMiter(const Path64& path, size_t j, size_t k, double cos_a); + void DoRound(const Path64& path, size_t j, size_t k, double angle); void BuildNormals(const Path64& path); - void OffsetPolygon(Group& group, Path64& path); - void OffsetOpenJoined(Group& group, Path64& path); - void OffsetOpenPath(Group& group, Path64& path); - void OffsetPoint(Group& group, Path64& path, size_t j, size_t& k); + void OffsetPolygon(Group& group, const Path64& path); + void OffsetOpenJoined(Group& group, const Path64& path); + void OffsetOpenPath(Group& group, const Path64& path); + void OffsetPoint(Group& group, const Path64& path, size_t j, size_t k); void DoGroupOffset(Group &group); void ExecuteInternal(double delta); public: @@ -91,6 +98,7 @@ public: void Execute(double delta, Paths64& paths); void Execute(double delta, PolyTree64& polytree); + void Execute(DeltaCallback64 delta_cb, Paths64& paths); double MiterLimit() const { return miter_limit_; } void MiterLimit(double miter_limit) { miter_limit_ = miter_limit; } @@ -108,6 +116,8 @@ public: #ifdef USINGZ void SetZCallback(ZCallback64 cb) { zCallback64_ = cb; } #endif + void SetDeltaCallback(DeltaCallback64 cb) { deltaCallback64_ = cb; } + }; } diff --git a/thirdparty/clipper2/include/clipper2/clipper.rectclip.h b/thirdparty/clipper2/include/clipper2/clipper.rectclip.h index 2a9bb35d08..ff043f25f0 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.rectclip.h +++ b/thirdparty/clipper2/include/clipper2/clipper.rectclip.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 9 February 2023 * +* Date : 1 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -13,8 +13,7 @@ #include <cstdlib> #include <vector> #include <queue> -#include "clipper.h" -#include "clipper.core.h" +#include "clipper2/clipper.core.h" namespace Clipper2Lib { @@ -34,10 +33,10 @@ namespace Clipper2Lib }; //------------------------------------------------------------------------------ - // RectClip + // RectClip64 //------------------------------------------------------------------------------ - class RectClip { + class RectClip64 { private: void ExecuteInternal(const Path64& path); Path64 GetPath(OutPt2*& op); @@ -58,23 +57,23 @@ namespace Clipper2Lib void AddCorner(Location prev, Location curr); void AddCorner(Location& loc, bool isClockwise); public: - explicit RectClip(const Rect64& rect) : + explicit RectClip64(const Rect64& rect) : rect_(rect), rect_as_path_(rect.AsPath()), rect_mp_(rect.MidPoint()) {} - Paths64 Execute(const Paths64& paths, bool convex_only = false); + Paths64 Execute(const Paths64& paths); }; //------------------------------------------------------------------------------ - // RectClipLines + // RectClipLines64 //------------------------------------------------------------------------------ - class RectClipLines : public RectClip { + class RectClipLines64 : public RectClip64 { private: void ExecuteInternal(const Path64& path); Path64 GetPath(OutPt2*& op); public: - explicit RectClipLines(const Rect64& rect) : RectClip(rect) {}; + explicit RectClipLines64(const Rect64& rect) : RectClip64(rect) {}; Paths64 Execute(const Paths64& paths); }; diff --git a/thirdparty/clipper2/include/clipper2/clipper.version.h b/thirdparty/clipper2/include/clipper2/clipper.version.h new file mode 100644 index 0000000000..d7644067e2 --- /dev/null +++ b/thirdparty/clipper2/include/clipper2/clipper.version.h @@ -0,0 +1,6 @@ +#ifndef CLIPPER_VERSION_H +#define CLIPPER_VERSION_H + +constexpr auto CLIPPER2_VERSION = "1.3.0"; + +#endif // CLIPPER_VERSION_H diff --git a/thirdparty/clipper2/clipper2-exceptions.patch b/thirdparty/clipper2/patches/clipper2-exceptions.patch index 8a6acc124e..0e1c6585fe 100644 --- a/thirdparty/clipper2/clipper2-exceptions.patch +++ b/thirdparty/clipper2/patches/clipper2-exceptions.patch @@ -1,17 +1,17 @@ diff --git a/thirdparty/clipper2/include/clipper2/clipper.core.h b/thirdparty/clipper2/include/clipper2/clipper.core.h -index c7522cb900..086d1b659c 100644 +index b3dddeeaa2..a77cdad5f4 100644 --- a/thirdparty/clipper2/include/clipper2/clipper.core.h +++ b/thirdparty/clipper2/include/clipper2/clipper.core.h -@@ -20,6 +20,8 @@ - #include <climits> +@@ -21,6 +21,8 @@ #include <numeric> + #include "clipper2/clipper.version.h" +#define CLIPPER2_THROW(exception) std::abort() + namespace Clipper2Lib { -@@ -65,16 +67,16 @@ namespace Clipper2Lib +@@ -78,18 +80,18 @@ namespace Clipper2Lib switch (error_code) { case precision_error_i: @@ -23,6 +23,9 @@ index c7522cb900..086d1b659c 100644 case non_pair_error_i: - throw Clipper2Exception(non_pair_error); + CLIPPER2_THROW(Clipper2Exception(non_pair_error)); + case undefined_error_i: +- throw Clipper2Exception(undefined_error); ++ CLIPPER2_THROW(Clipper2Exception(undefined_error)); case range_error_i: - throw Clipper2Exception(range_error); + CLIPPER2_THROW(Clipper2Exception(range_error)); diff --git a/thirdparty/clipper2/src/clipper.engine.cpp b/thirdparty/clipper2/src/clipper.engine.cpp index 2d61b8aafa..9358b74b70 100644 --- a/thirdparty/clipper2/src/clipper.engine.cpp +++ b/thirdparty/clipper2/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 March 2023 * +* Date : 22 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -15,6 +15,7 @@ #include <algorithm> #include "clipper2/clipper.engine.h" +#include "clipper2/clipper.h" // https://github.com/AngusJohnson/Clipper2/discussions/334 // #discussioncomment-4248602 @@ -419,6 +420,12 @@ namespace Clipper2Lib { return outrec; } + inline bool IsValidOwner(OutRec* outrec, OutRec* testOwner) + { + // prevent outrec owning itself either directly or indirectly + while (testOwner && testOwner != outrec) testOwner = testOwner->owner; + return !testOwner; + } inline void UncoupleOutRec(Active ae) { @@ -484,108 +491,135 @@ namespace Clipper2Lib { outrec->owner = new_owner; } - //------------------------------------------------------------------------------ - // ClipperBase methods ... - //------------------------------------------------------------------------------ - - ClipperBase::~ClipperBase() + static PointInPolygonResult PointInOpPolygon(const Point64& pt, OutPt* op) { - Clear(); - } + if (op == op->next || op->prev == op->next) + return PointInPolygonResult::IsOutside; - void ClipperBase::DeleteEdges(Active*& e) - { - while (e) + OutPt* op2 = op; + do { - Active* e2 = e; - e = e->next_in_ael; - delete e2; - } - } + if (op->pt.y != pt.y) break; + op = op->next; + } while (op != op2); + if (op->pt.y == pt.y) // not a proper polygon + return PointInPolygonResult::IsOutside; - void ClipperBase::CleanUp() - { - DeleteEdges(actives_); - scanline_list_ = std::priority_queue<int64_t>(); - intersect_nodes_.clear(); - DisposeAllOutRecs(); - horz_seg_list_.clear(); - horz_join_list_.clear(); - } + bool is_above = op->pt.y < pt.y, starting_above = is_above; + int val = 0; + op2 = op->next; + while (op2 != op) + { + if (is_above) + while (op2 != op && op2->pt.y < pt.y) op2 = op2->next; + else + while (op2 != op && op2->pt.y > pt.y) op2 = op2->next; + if (op2 == op) break; + // must have touched or crossed the pt.Y horizonal + // and this must happen an even number of times - void ClipperBase::Clear() - { - CleanUp(); - DisposeVerticesAndLocalMinima(); - current_locmin_iter_ = minima_list_.begin(); - minima_list_sorted_ = false; - has_open_paths_ = false; - } + if (op2->pt.y == pt.y) // touching the horizontal + { + if (op2->pt.x == pt.x || (op2->pt.y == op2->prev->pt.y && + (pt.x < op2->prev->pt.x) != (pt.x < op2->pt.x))) + return PointInPolygonResult::IsOn; + + op2 = op2->next; + if (op2 == op) break; + continue; + } + if (pt.x < op2->pt.x && pt.x < op2->prev->pt.x); + // do nothing because + // we're only interested in edges crossing on the left + else if ((pt.x > op2->prev->pt.x && pt.x > op2->pt.x)) + val = 1 - val; // toggle val + else + { + double d = CrossProduct(op2->prev->pt, op2->pt, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; + } + is_above = !is_above; + op2 = op2->next; + } - void ClipperBase::Reset() - { - if (!minima_list_sorted_) + if (is_above != starting_above) { - std::sort(minima_list_.begin(), minima_list_.end(), LocMinSorter()); - minima_list_sorted_ = true; + double d = CrossProduct(op2->prev->pt, op2->pt, pt); + if (d == 0) return PointInPolygonResult::IsOn; + if ((d < 0) == is_above) val = 1 - val; } - LocalMinimaList::const_reverse_iterator i; - for (i = minima_list_.rbegin(); i != minima_list_.rend(); ++i) - InsertScanline((*i)->vertex->pt.y); - current_locmin_iter_ = minima_list_.begin(); - actives_ = nullptr; - sel_ = nullptr; - succeeded_ = true; + if (val == 0) return PointInPolygonResult::IsOutside; + else return PointInPolygonResult::IsInside; } - -#ifdef USINGZ - void ClipperBase::SetZ(const Active& e1, const Active& e2, Point64& ip) + inline Path64 GetCleanPath(OutPt* op) { - if (!zCallback_) return; - // prioritize subject over clip vertices by passing - // subject vertices before clip vertices in the callback - if (GetPolyType(e1) == PathType::Subject) - { - if (ip == e1.bot) ip.z = e1.bot.z; - else if (ip == e1.top) ip.z = e1.top.z; - else if (ip == e2.bot) ip.z = e2.bot.z; - else if (ip == e2.top) ip.z = e2.top.z; - else ip.z = DefaultZ; - zCallback_(e1.bot, e1.top, e2.bot, e2.top, ip); - } - else + Path64 result; + OutPt* op2 = op; + while (op2->next != op && + ((op2->pt.x == op2->next->pt.x && op2->pt.x == op2->prev->pt.x) || + (op2->pt.y == op2->next->pt.y && op2->pt.y == op2->prev->pt.y))) op2 = op2->next; + result.push_back(op2->pt); + OutPt* prevOp = op2; + op2 = op2->next; + while (op2 != op) { - if (ip == e2.bot) ip.z = e2.bot.z; - else if (ip == e2.top) ip.z = e2.top.z; - else if (ip == e1.bot) ip.z = e1.bot.z; - else if (ip == e1.top) ip.z = e1.top.z; - else ip.z = DefaultZ; - zCallback_(e2.bot, e2.top, e1.bot, e1.top, ip); + if ((op2->pt.x != op2->next->pt.x || op2->pt.x != prevOp->pt.x) && + (op2->pt.y != op2->next->pt.y || op2->pt.y != prevOp->pt.y)) + { + result.push_back(op2->pt); + prevOp = op2; + } + op2 = op2->next; } + return result; } -#endif - void ClipperBase::AddPath(const Path64& path, PathType polytype, bool is_open) + inline bool Path1InsidePath2(OutPt* op1, OutPt* op2) { - Paths64 tmp; - tmp.push_back(path); - AddPaths(tmp, polytype, is_open); + // we need to make some accommodation for rounding errors + // so we won't jump if the first vertex is found outside + PointInPolygonResult result; + int outside_cnt = 0; + OutPt* op = op1; + do + { + result = PointInOpPolygon(op->pt, op2); + if (result == PointInPolygonResult::IsOutside) ++outside_cnt; + else if (result == PointInPolygonResult::IsInside) --outside_cnt; + op = op->next; + } while (op != op1 && std::abs(outside_cnt) < 2); + if (std::abs(outside_cnt) > 1) return (outside_cnt < 0); + // since path1's location is still equivocal, check its midpoint + Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint(); + Path64 path2 = GetCleanPath(op2); + return PointInPolygon(mp, path2) != PointInPolygonResult::IsOutside; } + //------------------------------------------------------------------------------ + //------------------------------------------------------------------------------ - void ClipperBase::AddPaths(const Paths64& paths, PathType polytype, bool is_open) + void AddLocMin(LocalMinimaList& list, + Vertex& vert, PathType polytype, bool is_open) { - if (is_open) has_open_paths_ = true; - minima_list_sorted_ = false; + //make sure the vertex is added only once ... + if ((VertexFlags::LocalMin & vert.flags) != VertexFlags::None) return; + vert.flags = (vert.flags | VertexFlags::LocalMin); + list.push_back(std::make_unique <LocalMinima>(&vert, polytype, is_open)); + } + + void AddPaths_(const Paths64& paths, PathType polytype, bool is_open, + std::vector<Vertex*>& vertexLists, LocalMinimaList& locMinList) + { const auto total_vertex_count = - std::accumulate(paths.begin(), paths.end(), 0, - [](const auto& a, const Path64& path) - {return a + static_cast<unsigned>(path.size());}); + std::accumulate(paths.begin(), paths.end(), 0, + [](const auto& a, const Path64& path) + {return a + static_cast<unsigned>(path.size()); }); if (total_vertex_count == 0) return; Vertex* vertices = new Vertex[total_vertex_count], * v = vertices; @@ -631,7 +665,7 @@ namespace Clipper2Lib { if (going_up) { v0->flags = VertexFlags::OpenStart; - AddLocMin(*v0, polytype, true); + AddLocMin(locMinList , *v0, polytype, true); } else v0->flags = VertexFlags::OpenStart | VertexFlags::LocalMax; @@ -659,7 +693,7 @@ namespace Clipper2Lib { else if (curr_v->pt.y < prev_v->pt.y && !going_up) { going_up = true; - AddLocMin(*prev_v, polytype, is_open); + AddLocMin(locMinList, *prev_v, polytype, is_open); } prev_v = curr_v; curr_v = curr_v->next; @@ -671,18 +705,161 @@ namespace Clipper2Lib { if (going_up) prev_v->flags = prev_v->flags | VertexFlags::LocalMax; else - AddLocMin(*prev_v, polytype, is_open); + AddLocMin(locMinList, *prev_v, polytype, is_open); } else if (going_up != going_up0) { - if (going_up0) AddLocMin(*prev_v, polytype, false); + if (going_up0) AddLocMin(locMinList, *prev_v, polytype, false); else prev_v->flags = prev_v->flags | VertexFlags::LocalMax; } } // end processing current path - vertex_lists_.emplace_back(vertices); - } // end AddPaths + vertexLists.emplace_back(vertices); + } + //------------------------------------------------------------------------------ + // ReuseableDataContainer64 methods ... + //------------------------------------------------------------------------------ + + void ReuseableDataContainer64::AddLocMin(Vertex& vert, PathType polytype, bool is_open) + { + //make sure the vertex is added only once ... + if ((VertexFlags::LocalMin & vert.flags) != VertexFlags::None) return; + + vert.flags = (vert.flags | VertexFlags::LocalMin); + minima_list_.push_back(std::make_unique <LocalMinima>(&vert, polytype, is_open)); + } + + void ReuseableDataContainer64::AddPaths(const Paths64& paths, + PathType polytype, bool is_open) + { + AddPaths_(paths, polytype, is_open, vertex_lists_, minima_list_); + } + + ReuseableDataContainer64::~ReuseableDataContainer64() + { + Clear(); + } + + void ReuseableDataContainer64::Clear() + { + minima_list_.clear(); + for (auto v : vertex_lists_) delete[] v; + vertex_lists_.clear(); + } + + //------------------------------------------------------------------------------ + // ClipperBase methods ... + //------------------------------------------------------------------------------ + + ClipperBase::~ClipperBase() + { + Clear(); + } + + void ClipperBase::DeleteEdges(Active*& e) + { + while (e) + { + Active* e2 = e; + e = e->next_in_ael; + delete e2; + } + } + + void ClipperBase::CleanUp() + { + DeleteEdges(actives_); + scanline_list_ = std::priority_queue<int64_t>(); + intersect_nodes_.clear(); + DisposeAllOutRecs(); + horz_seg_list_.clear(); + horz_join_list_.clear(); + } + + + void ClipperBase::Clear() + { + CleanUp(); + DisposeVerticesAndLocalMinima(); + current_locmin_iter_ = minima_list_.begin(); + minima_list_sorted_ = false; + has_open_paths_ = false; + } + + + void ClipperBase::Reset() + { + if (!minima_list_sorted_) + { + std::stable_sort(minima_list_.begin(), minima_list_.end(), LocMinSorter()); //#594 + minima_list_sorted_ = true; + } + LocalMinimaList::const_reverse_iterator i; + for (i = minima_list_.rbegin(); i != minima_list_.rend(); ++i) + InsertScanline((*i)->vertex->pt.y); + + current_locmin_iter_ = minima_list_.begin(); + actives_ = nullptr; + sel_ = nullptr; + succeeded_ = true; + } + + +#ifdef USINGZ + void ClipperBase::SetZ(const Active& e1, const Active& e2, Point64& ip) + { + if (!zCallback_) return; + // prioritize subject over clip vertices by passing + // subject vertices before clip vertices in the callback + if (GetPolyType(e1) == PathType::Subject) + { + if (ip == e1.bot) ip.z = e1.bot.z; + else if (ip == e1.top) ip.z = e1.top.z; + else if (ip == e2.bot) ip.z = e2.bot.z; + else if (ip == e2.top) ip.z = e2.top.z; + else ip.z = DefaultZ; + zCallback_(e1.bot, e1.top, e2.bot, e2.top, ip); + } + else + { + if (ip == e2.bot) ip.z = e2.bot.z; + else if (ip == e2.top) ip.z = e2.top.z; + else if (ip == e1.bot) ip.z = e1.bot.z; + else if (ip == e1.top) ip.z = e1.top.z; + else ip.z = DefaultZ; + zCallback_(e2.bot, e2.top, e1.bot, e1.top, ip); + } + } +#endif + + void ClipperBase::AddPath(const Path64& path, PathType polytype, bool is_open) + { + Paths64 tmp; + tmp.push_back(path); + AddPaths(tmp, polytype, is_open); + } + + void ClipperBase::AddPaths(const Paths64& paths, PathType polytype, bool is_open) + { + if (is_open) has_open_paths_ = true; + minima_list_sorted_ = false; + AddPaths_(paths, polytype, is_open, vertex_lists_, minima_list_); + } + + void ClipperBase::AddReuseableData(const ReuseableDataContainer64& reuseable_data) + { + // nb: reuseable_data will continue to own the vertices + // and remains responsible for their clean up. + succeeded_ = false; + minima_list_sorted_ = false; + LocalMinimaList::const_iterator i; + for (i = reuseable_data.minima_list_.cbegin(); i != reuseable_data.minima_list_.cend(); ++i) + { + minima_list_.push_back(std::make_unique <LocalMinima>((*i)->vertex, (*i)->polytype, (*i)->is_open)); + if ((*i)->is_open) has_open_paths_ = true; + } + } void ClipperBase::InsertScanline(int64_t y) { @@ -1236,7 +1413,7 @@ namespace Clipper2Lib { else SetOwner(&outrec, e->outrec); // nb: outRec.owner here is likely NOT the real - // owner but this will be checked in DeepCheckOwner() + // owner but this will be checked in RecursiveCheckOwners() } UncoupleOutRec(e1); @@ -1293,13 +1470,14 @@ namespace Clipper2Lib { e2.outrec->front_edge = nullptr; e2.outrec->back_edge = nullptr; e2.outrec->pts = nullptr; - SetOwner(e2.outrec, e1.outrec); if (IsOpenEnd(e1)) { e2.outrec->pts = e1.outrec->pts; e1.outrec->pts = nullptr; } + else + SetOwner(e2.outrec, e1.outrec); //and e1 and e2 are maxima and are about to be dropped from the Actives list. e1.outrec = nullptr; @@ -1315,6 +1493,7 @@ namespace Clipper2Lib { result->owner = nullptr; result->polypath = nullptr; result->is_open = false; + result->splits = nullptr; return result; } @@ -1364,7 +1543,7 @@ namespace Clipper2Lib { //NB if preserveCollinear == true, then only remove 180 deg. spikes if ((CrossProduct(op2->prev->pt, op2->pt, op2->next->pt) == 0) && (op2->pt == op2->prev->pt || - op2->pt == op2->next->pt || !PreserveCollinear || + op2->pt == op2->next->pt || !preserve_collinear_ || DotProduct(op2->prev->pt, op2->pt, op2->next->pt) < 0)) { @@ -1409,11 +1588,6 @@ namespace Clipper2Lib { return; } - // nb: area1 is the path's area *before* splitting, whereas area2 is - // the area of the triangle containing splitOp & splitOp.next. - // So the only way for these areas to have the same sign is if - // the split triangle is larger than the path containing prevOp or - // if there's more than one self=intersection. double area2 = AreaTriangle(ip, splitOp->pt, splitOp->next->pt); double absArea2 = std::fabs(area2); @@ -1433,18 +1607,17 @@ namespace Clipper2Lib { prevOp->next = newOp2; } + // area1 is the path's area *before* splitting, whereas area2 is + // the area of the triangle containing splitOp & splitOp.next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self-intersection. if (absArea2 >= 1 && (absArea2 > absArea1 || (area2 > 0) == (area1 > 0))) { OutRec* newOr = NewOutRec(); newOr->owner = outrec->owner; - if (using_polytree_) - { - if (!outrec->splits) outrec->splits = new OutRecList(); - outrec->splits->push_back(newOr); - } - splitOp->outrec = newOr; splitOp->next->outrec = newOr; OutPt* newOp = new OutPt(ip, newOr); @@ -1453,6 +1626,20 @@ namespace Clipper2Lib { newOr->pts = newOp; splitOp->prev = newOp; splitOp->next->next = newOp; + + if (using_polytree_) + { + if (Path1InsidePath2(prevOp, newOp)) + { + newOr->splits = new OutRecList(); + newOr->splits->push_back(outrec); + } + else + { + if (!outrec->splits) outrec->splits = new OutRecList(); + outrec->splits->push_back(newOr); + } + } } else { @@ -1521,6 +1708,28 @@ namespace Clipper2Lib { return op; } + inline void TrimHorz(Active& horzEdge, bool preserveCollinear) + { + bool wasTrimmed = false; + Point64 pt = NextVertex(horzEdge)->pt; + while (pt.y == horzEdge.top.y) + { + //always trim 180 deg. spikes (in closed paths) + //but otherwise break if preserveCollinear = true + if (preserveCollinear && + ((pt.x < horzEdge.top.x) != (horzEdge.bot.x < horzEdge.top.x))) + break; + + horzEdge.vertex_top = NextVertex(horzEdge); + horzEdge.top = pt; + wasTrimmed = true; + if (IsMaxima(horzEdge)) break; + pt = NextVertex(horzEdge)->pt; + } + + if (wasTrimmed) SetDx(horzEdge); // +/-infinity + } + inline void ClipperBase::UpdateEdgeIntoAEL(Active* e) { @@ -1532,11 +1741,15 @@ namespace Clipper2Lib { if (IsJoined(*e)) Split(*e, e->bot); - if (IsHorizontal(*e)) return; - InsertScanline(e->top.y); + if (IsHorizontal(*e)) + { + if (!IsOpen(*e)) TrimHorz(*e, preserve_collinear_); + return; + } + InsertScanline(e->top.y); CheckJoinLeft(*e, e->bot); - CheckJoinRight(*e, e->bot); + CheckJoinRight(*e, e->bot, true); // (#500) } Active* FindEdgeWithMatchingLocMin(Active* e) @@ -1596,17 +1809,14 @@ namespace Clipper2Lib { default: if (std::abs(edge_c->wind_cnt) != 1) return nullptr; break; } + OutPt* resultOp; //toggle contribution ... if (IsHotEdge(*edge_o)) { - OutPt* resultOp = AddOutPt(*edge_o, pt); -#ifdef USINGZ - if (zCallback_) SetZ(e1, e2, resultOp->pt); -#endif + resultOp = AddOutPt(*edge_o, pt); if (IsFront(*edge_o)) edge_o->outrec->front_edge = nullptr; else edge_o->outrec->back_edge = nullptr; edge_o->outrec = nullptr; - return resultOp; } //horizontal edges can pass under open paths at a LocMins @@ -1626,11 +1836,16 @@ namespace Clipper2Lib { return e3->outrec->pts; } else - return StartOpenPath(*edge_o, pt); + resultOp = StartOpenPath(*edge_o, pt); } else - return StartOpenPath(*edge_o, pt); - } + resultOp = StartOpenPath(*edge_o, pt); + +#ifdef USINGZ + if (zCallback_) SetZ(*edge_o, *edge_c, resultOp->pt); +#endif + return resultOp; + } // end of an open path intersection //MANAGING CLOSED PATHS FROM HERE ON @@ -1895,105 +2110,6 @@ namespace Clipper2Lib { } while (op != outrec->pts); } - inline Rect64 GetBounds(OutPt* op) - { - Rect64 result(op->pt.x, op->pt.y, op->pt.x, op->pt.y); - OutPt* op2 = op->next; - while (op2 != op) - { - if (op2->pt.x < result.left) result.left = op2->pt.x; - else if (op2->pt.x > result.right) result.right = op2->pt.x; - if (op2->pt.y < result.top) result.top = op2->pt.y; - else if (op2->pt.y > result.bottom) result.bottom = op2->pt.y; - op2 = op2->next; - } - return result; - } - - static PointInPolygonResult PointInOpPolygon(const Point64& pt, OutPt* op) - { - if (op == op->next || op->prev == op->next) - return PointInPolygonResult::IsOutside; - - OutPt* op2 = op; - do - { - if (op->pt.y != pt.y) break; - op = op->next; - } while (op != op2); - if (op->pt.y == pt.y) // not a proper polygon - return PointInPolygonResult::IsOutside; - - bool is_above = op->pt.y < pt.y, starting_above = is_above; - int val = 0; - op2 = op->next; - while (op2 != op) - { - if (is_above) - while (op2 != op && op2->pt.y < pt.y) op2 = op2->next; - else - while (op2 != op && op2->pt.y > pt.y) op2 = op2->next; - if (op2 == op) break; - - // must have touched or crossed the pt.Y horizonal - // and this must happen an even number of times - - if (op2->pt.y == pt.y) // touching the horizontal - { - if (op2->pt.x == pt.x || (op2->pt.y == op2->prev->pt.y && - (pt.x < op2->prev->pt.x) != (pt.x < op2->pt.x))) - return PointInPolygonResult::IsOn; - - op2 = op2->next; - if (op2 == op) break; - continue; - } - - if (pt.x < op2->pt.x && pt.x < op2->prev->pt.x); - // do nothing because - // we're only interested in edges crossing on the left - else if ((pt.x > op2->prev->pt.x && pt.x > op2->pt.x)) - val = 1 - val; // toggle val - else - { - double d = CrossProduct(op2->prev->pt, op2->pt, pt); - if (d == 0) return PointInPolygonResult::IsOn; - if ((d < 0) == is_above) val = 1 - val; - } - is_above = !is_above; - op2 = op2->next; - } - - if (is_above != starting_above) - { - double d = CrossProduct(op2->prev->pt, op2->pt, pt); - if (d == 0) return PointInPolygonResult::IsOn; - if ((d < 0) == is_above) val = 1 - val; - } - - if (val == 0) return PointInPolygonResult::IsOutside; - else return PointInPolygonResult::IsInside; - } - - inline bool Path1InsidePath2(OutPt* op1, OutPt* op2) - { - // we need to make some accommodation for rounding errors - // so we won't jump if the first vertex is found outside - int outside_cnt = 0; - OutPt* op = op1; - do - { - PointInPolygonResult result = PointInOpPolygon(op->pt, op2); - if (result == PointInPolygonResult::IsOutside) ++outside_cnt; - else if (result == PointInPolygonResult::IsInside) --outside_cnt; - op = op->next; - } while (op != op1 && std::abs(outside_cnt) < 2); - if (std::abs(outside_cnt) > 1) return (outside_cnt < 0); - // since path1's location is still equivocal, check its midpoint - Point64 mp = GetBounds(op).MidPoint(); - return PointInOpPolygon(mp, op2) == PointInPolygonResult::IsInside; - } - inline bool SetHorzSegHeadingForward(HorzSegment& hs, OutPt* opP, OutPt* opN) { if (opP->pt.x == opN->pt.x) return false; @@ -2051,7 +2167,7 @@ namespace Clipper2Lib { horz_seg_list_.end(), [](HorzSegment& hs) { return UpdateHorzSegment(hs); }); if (j < 2) return; - std::sort(horz_seg_list_.begin(), horz_seg_list_.end(), HorzSegSorter()); + std::stable_sort(horz_seg_list_.begin(), horz_seg_list_.end(), HorzSegSorter()); HorzSegmentList::iterator hs1 = horz_seg_list_.begin(), hs2; HorzSegmentList::iterator hs_end = hs1 +j; @@ -2061,8 +2177,8 @@ namespace Clipper2Lib { { for (hs2 = hs1 + 1; hs2 != hs_end; ++hs2) { - if (hs2->left_op->pt.x >= hs1->right_op->pt.x) break; - if (hs2->left_to_right == hs1->left_to_right || + if ((hs2->left_op->pt.x >= hs1->right_op->pt.x) || + (hs2->left_to_right == hs1->left_to_right) || (hs2->right_op->pt.x <= hs1->left_op->pt.x)) continue; int64_t curr_y = hs1->left_op->pt.y; if (hs1->left_to_right) @@ -2095,6 +2211,17 @@ namespace Clipper2Lib { } } + void MoveSplits(OutRec* fromOr, OutRec* toOr) + { + if (!fromOr->splits) return; + if (!toOr->splits) toOr->splits = new OutRecList(); + OutRecList::iterator orIter = fromOr->splits->begin(); + for (; orIter != fromOr->splits->end(); ++orIter) + toOr->splits->push_back(*orIter); + fromOr->splits->clear(); + } + + void ClipperBase::ProcessHorzJoins() { for (const HorzJoin& j : horz_join_list_) @@ -2109,36 +2236,53 @@ namespace Clipper2Lib { op1b->prev = op2b; op2b->next = op1b; - if (or1 == or2) + if (or1 == or2) // 'join' is really a split { - or2 = new OutRec(); + or2 = NewOutRec(); or2->pts = op1b; FixOutRecPts(or2); + + //if or1->pts has moved to or2 then update or1->pts!! if (or1->pts->outrec == or2) { or1->pts = j.op1; or1->pts->outrec = or1; } - if (using_polytree_) + if (using_polytree_) //#498, #520, #584, D#576, #618 { - if (Path1InsidePath2(or2->pts, or1->pts)) - SetOwner(or2, or1); - else if (Path1InsidePath2(or1->pts, or2->pts)) - SetOwner(or1, or2); - else + if (Path1InsidePath2(or1->pts, or2->pts)) + { + //swap or1's & or2's pts + OutPt* tmp = or1->pts; + or1->pts = or2->pts; + or2->pts = tmp; + FixOutRecPts(or1); + FixOutRecPts(or2); + //or2 is now inside or1 or2->owner = or1; + } + else if (Path1InsidePath2(or2->pts, or1->pts)) + { + or2->owner = or1; + } + else + or2->owner = or1->owner; + + if (!or1->splits) or1->splits = new OutRecList(); + or1->splits->push_back(or2); } else or2->owner = or1; - - outrec_list_.push_back(or2); } else { or2->pts = nullptr; if (using_polytree_) + { SetOwner(or2, or1); + MoveSplits(or2, or1); //#618 + } else or2->owner = or1; } @@ -2335,35 +2479,6 @@ namespace Clipper2Lib { } } - inline bool HorzIsSpike(const Active& horzEdge) - { - Point64 nextPt = NextVertex(horzEdge)->pt; - return (nextPt.y == horzEdge.bot.y) && - (horzEdge.bot.x < horzEdge.top.x) != (horzEdge.top.x < nextPt.x); - } - - inline void TrimHorz(Active& horzEdge, bool preserveCollinear) - { - bool wasTrimmed = false; - Point64 pt = NextVertex(horzEdge)->pt; - while (pt.y == horzEdge.top.y) - { - //always trim 180 deg. spikes (in closed paths) - //but otherwise break if preserveCollinear = true - if (preserveCollinear && - ((pt.x < horzEdge.top.x) != (horzEdge.bot.x < horzEdge.top.x))) - break; - - horzEdge.vertex_top = NextVertex(horzEdge); - horzEdge.top = pt; - wasTrimmed = true; - if (IsMaxima(horzEdge)) break; - pt = NextVertex(horzEdge)->pt; - } - - if (wasTrimmed) SetDx(horzEdge); // +/-infinity - } - void ClipperBase::DoHorizontal(Active& horz) /******************************************************************************* * Notes: Horizontal edges (HEs) at scanline intersections (ie at the top or * @@ -2389,10 +2504,10 @@ namespace Clipper2Lib { else vertex_max = GetCurrYMaximaVertex(horz); - // remove 180 deg.spikes and also simplify - // consecutive horizontals when PreserveCollinear = true - if (vertex_max && !horzIsOpen && vertex_max != horz.vertex_top) - TrimHorz(horz, PreserveCollinear); + //// remove 180 deg.spikes and also simplify + //// consecutive horizontals when PreserveCollinear = true + //if (!horzIsOpen && vertex_max != horz.vertex_top) + // TrimHorz(horz, PreserveCollinear); int64_t horz_left, horz_right; bool is_left_to_right = @@ -2407,7 +2522,6 @@ namespace Clipper2Lib { #endif AddTrialHorzJoin(op); } - OutRec* currHorzOutrec = horz.outrec; while (true) // loop through consec. horizontal edges { @@ -2422,6 +2536,9 @@ namespace Clipper2Lib { if (IsHotEdge(horz) && IsJoined(*e)) Split(*e, e->top); + //if (IsHotEdge(horz) != IsHotEdge(*e)) + // DoError(undefined_error_i); + if (IsHotEdge(horz)) { while (horz.vertex_top != vertex_max) @@ -2476,6 +2593,7 @@ namespace Clipper2Lib { { IntersectEdges(horz, *e, pt); SwapPositionsInAEL(horz, *e); + CheckJoinLeft(*e, pt); horz.curr_x = e->curr_x; e = horz.next_in_ael; } @@ -2483,13 +2601,13 @@ namespace Clipper2Lib { { IntersectEdges(*e, horz, pt); SwapPositionsInAEL(*e, horz); + CheckJoinRight(*e, pt); horz.curr_x = e->curr_x; e = horz.prev_in_ael; } - if (horz.outrec && horz.outrec != currHorzOutrec) + if (horz.outrec) { - currHorzOutrec = horz.outrec; //nb: The outrec containining the op returned by IntersectEdges //above may no longer be associated with horzEdge. AddTrialHorzJoin(GetLastOp(horz)); @@ -2519,14 +2637,16 @@ namespace Clipper2Lib { AddOutPt(horz, horz.top); UpdateEdgeIntoAEL(&horz); - if (PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) - TrimHorz(horz, true); - is_left_to_right = ResetHorzDirection(horz, vertex_max, horz_left, horz_right); } - if (IsHotEdge(horz)) AddOutPt(horz, horz.top); + if (IsHotEdge(horz)) + { + OutPt* op = AddOutPt(horz, horz.top); + AddTrialHorzJoin(op); + } + UpdateEdgeIntoAEL(&horz); // end of an intermediate horiz. } @@ -2638,10 +2758,10 @@ namespace Clipper2Lib { const Point64& pt, bool check_curr_x) { Active* prev = e.prev_in_ael; - if (IsOpen(e) || !IsHotEdge(e) || !prev || - IsOpen(*prev) || !IsHotEdge(*prev) || - pt.y < e.top.y + 2 || pt.y < prev->top.y + 2) // avoid trivial joins - return; + if (IsOpen(e) || !IsHotEdge(e) || !prev || + IsOpen(*prev) || !IsHotEdge(*prev)) return; + if ((pt.y < e.top.y + 2 || pt.y < prev->top.y + 2) && + ((e.bot.y > pt.y) || (prev->bot.y > pt.y))) return; // avoid trivial joins if (check_curr_x) { @@ -2664,10 +2784,10 @@ namespace Clipper2Lib { const Point64& pt, bool check_curr_x) { Active* next = e.next_in_ael; - if (IsOpen(e) || !IsHotEdge(e) || - !next || IsOpen(*next) || !IsHotEdge(*next) || - pt.y < e.top.y +2 || pt.y < next->top.y +2) // avoids trivial joins - return; + if (IsOpen(e) || !IsHotEdge(e) || + !next || IsOpen(*next) || !IsHotEdge(*next)) return; + if ((pt.y < e.top.y +2 || pt.y < next->top.y +2) && + ((e.bot.y > pt.y) || (next->bot.y > pt.y))) return; // avoid trivial joins if (check_curr_x) { @@ -2682,6 +2802,7 @@ namespace Clipper2Lib { JoinOutrecPaths(e, *next); else JoinOutrecPaths(*next, e); + e.join_with = JoinWith::Right; next->join_with = JoinWith::Left; } @@ -2752,12 +2873,34 @@ namespace Clipper2Lib { if (!outrec->bounds.IsEmpty()) return true; CleanCollinear(outrec); if (!outrec->pts || - !BuildPath64(outrec->pts, ReverseSolution, false, outrec->path)) - return false; + !BuildPath64(outrec->pts, reverse_solution_, false, outrec->path)){ + return false;} outrec->bounds = GetBounds(outrec->path); return true; } + bool ClipperBase::CheckSplitOwner(OutRec* outrec, OutRecList* splits) + { + for (auto split : *splits) + { + split = GetRealOutRec(split); + if(!split || split == outrec || split->recursive_split == outrec) continue; + split->recursive_split = outrec; // prevent infinite loops + + if (split->splits && CheckSplitOwner(outrec, split->splits)) + return true; + else if (CheckBounds(split) && + IsValidOwner(outrec, split) && + split->bounds.Contains(outrec->bounds) && + Path1InsidePath2(outrec->pts, split->pts)) + { + outrec->owner = split; //found in split + return true; + } + } + return false; + } + void ClipperBase::RecursiveCheckOwners(OutRec* outrec, PolyPath* polypath) { // pre-condition: outrec will have valid bounds @@ -2765,52 +2908,25 @@ namespace Clipper2Lib { if (outrec->polypath || outrec->bounds.IsEmpty()) return; - while (outrec->owner && - (!outrec->owner->pts || !CheckBounds(outrec->owner))) - outrec->owner = outrec->owner->owner; - - if (outrec->owner && !outrec->owner->polypath) - RecursiveCheckOwners(outrec->owner, polypath); - while (outrec->owner) - if (outrec->owner->bounds.Contains(outrec->bounds) && - Path1InsidePath2(outrec->pts, outrec->owner->pts)) - break; // found - owner contain outrec! - else - outrec->owner = outrec->owner->owner; + { + if (outrec->owner->splits && CheckSplitOwner(outrec, outrec->owner->splits)) break; + if (outrec->owner->pts && CheckBounds(outrec->owner) && + outrec->owner->bounds.Contains(outrec->bounds) && + Path1InsidePath2(outrec->pts, outrec->owner->pts)) break; + outrec->owner = outrec->owner->owner; + } if (outrec->owner) + { + if (!outrec->owner->polypath) + RecursiveCheckOwners(outrec->owner, polypath); outrec->polypath = outrec->owner->polypath->AddChild(outrec->path); + } else outrec->polypath = polypath->AddChild(outrec->path); } - void ClipperBase::DeepCheckOwners(OutRec* outrec, PolyPath* polypath) - { - RecursiveCheckOwners(outrec, polypath); - - while (outrec->owner && outrec->owner->splits) - { - OutRec* split = nullptr; - for (auto s : *outrec->owner->splits) - { - split = GetRealOutRec(s); - if (split && split != outrec && - split != outrec->owner && CheckBounds(split) && - split->bounds.Contains(outrec->bounds) && - Path1InsidePath2(outrec->pts, split->pts)) - { - RecursiveCheckOwners(split, polypath); - outrec->owner = split; //found in split - break; // inner 'for' loop - } - else - split = nullptr; - } - if (!split) break; - } - } - void Clipper64::BuildPaths64(Paths64& solutionClosed, Paths64* solutionOpen) { solutionClosed.resize(0); @@ -2832,7 +2948,7 @@ namespace Clipper2Lib { Path64 path; if (solutionOpen && outrec->is_open) { - if (BuildPath64(outrec->pts, ReverseSolution, true, path)) + if (BuildPath64(outrec->pts, reverse_solution_, true, path)) solutionOpen->emplace_back(std::move(path)); } else @@ -2840,7 +2956,7 @@ namespace Clipper2Lib { // nb: CleanCollinear can add to outrec_list_ CleanCollinear(outrec); //closed paths should always return a Positive orientation - if (BuildPath64(outrec->pts, ReverseSolution, false, path)) + if (BuildPath64(outrec->pts, reverse_solution_, false, path)) solutionClosed.emplace_back(std::move(path)); } } @@ -2863,13 +2979,13 @@ namespace Clipper2Lib { if (outrec->is_open) { Path64 path; - if (BuildPath64(outrec->pts, ReverseSolution, true, path)) + if (BuildPath64(outrec->pts, reverse_solution_, true, path)) open_paths.push_back(path); continue; } if (CheckBounds(outrec)) - DeepCheckOwners(outrec, &polytree); + RecursiveCheckOwners(outrec, &polytree); } } @@ -2940,14 +3056,14 @@ namespace Clipper2Lib { PathD path; if (solutionOpen && outrec->is_open) { - if (BuildPathD(outrec->pts, ReverseSolution, true, path, invScale_)) + if (BuildPathD(outrec->pts, reverse_solution_, true, path, invScale_)) solutionOpen->emplace_back(std::move(path)); } else { CleanCollinear(outrec); //closed paths should always return a Positive orientation - if (BuildPathD(outrec->pts, ReverseSolution, false, path, invScale_)) + if (BuildPathD(outrec->pts, reverse_solution_, false, path, invScale_)) solutionClosed.emplace_back(std::move(path)); } } @@ -2960,19 +3076,22 @@ namespace Clipper2Lib { if (has_open_paths_) open_paths.reserve(outrec_list_.size()); - for (OutRec* outrec : outrec_list_) + // outrec_list_.size() is not static here because + // BuildPathD below can indirectly add additional OutRec //#607 + for (size_t i = 0; i < outrec_list_.size(); ++i) { + OutRec* outrec = outrec_list_[i]; if (!outrec || !outrec->pts) continue; if (outrec->is_open) { PathD path; - if (BuildPathD(outrec->pts, ReverseSolution, true, path, invScale_)) + if (BuildPathD(outrec->pts, reverse_solution_, true, path, invScale_)) open_paths.push_back(path); continue; } if (CheckBounds(outrec)) - DeepCheckOwners(outrec, &polytree); + RecursiveCheckOwners(outrec, &polytree); } } diff --git a/thirdparty/clipper2/src/clipper.offset.cpp b/thirdparty/clipper2/src/clipper.offset.cpp index 78cd82376a..0282aa49bb 100644 --- a/thirdparty/clipper2/src/clipper.offset.cpp +++ b/thirdparty/clipper2/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 March 2023 * +* Date : 28 November 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -20,38 +20,63 @@ const double floating_point_tolerance = 1e-12; // Miscellaneous methods //------------------------------------------------------------------------------ -void GetBoundsAndLowestPolyIdx(const Paths64& paths, Rect64& r, int & idx) +inline bool ToggleBoolIf(bool val, bool condition) { - idx = -1; - r = MaxInvalidRect64; - int64_t lpx = 0; - for (int i = 0; i < static_cast<int>(paths.size()); ++i) - for (const Point64& p : paths[i]) + return condition ? !val : val; +} + +void GetMultiBounds(const Paths64& paths, std::vector<Rect64>& recList) +{ + recList.reserve(paths.size()); + for (const Path64& path : paths) + { + if (path.size() < 1) { - if (p.y >= r.bottom) - { - if (p.y > r.bottom || p.x < lpx) - { - idx = i; - lpx = p.x; - r.bottom = p.y; - } - } - else if (p.y < r.top) r.top = p.y; - if (p.x > r.right) r.right = p.x; - else if (p.x < r.left) r.left = p.x; + recList.push_back(InvalidRect64); + continue; + } + int64_t x = path[0].x, y = path[0].y; + Rect64 r = Rect64(x, y, x, y); + for (const Point64& pt : path) + { + if (pt.y > r.bottom) r.bottom = pt.y; + else if (pt.y < r.top) r.top = pt.y; + if (pt.x > r.right) r.right = pt.x; + else if (pt.x < r.left) r.left = pt.x; } - //if (idx < 0) r = Rect64(0, 0, 0, 0); - //if (r.top == INT64_MIN) r.bottom = r.top; - //if (r.left == INT64_MIN) r.left = r.right; + recList.push_back(r); + } } -bool IsSafeOffset(const Rect64& r, double abs_delta) +bool ValidateBounds(std::vector<Rect64>& recList, double delta) { - return r.left > min_coord + abs_delta && - r.right < max_coord - abs_delta && - r.top > min_coord + abs_delta && - r.bottom < max_coord - abs_delta; + int64_t int_delta = static_cast<int64_t>(delta); + int64_t big = MAX_COORD - int_delta; + int64_t small = MIN_COORD + int_delta; + for (const Rect64& r : recList) + { + if (!r.IsValid()) continue; // ignore invalid paths + else if (r.left < small || r.right > big || + r.top < small || r.bottom > big) return false; + } + return true; +} + +int GetLowestClosedPathIdx(std::vector<Rect64>& boundsList) +{ + int i = -1, result = -1; + Point64 botPt = Point64(INT64_MAX, INT64_MIN); + for (const Rect64& r : boundsList) + { + ++i; + if (!r.IsValid()) continue; // ignore invalid paths + else if (r.bottom > botPt.y || (r.bottom == botPt.y && r.left < botPt.x)) + { + botPt = Point64(r.left, r.bottom); + result = static_cast<int>(i); + } + } + return result; } PointD GetUnitNormal(const Point64& pt1, const Point64& pt2) @@ -78,8 +103,7 @@ inline double Hypot(double x, double y) } inline PointD NormalizeVector(const PointD& vec) -{ - +{ double h = Hypot(vec.x, vec.y); if (AlmostZero(h)) return PointD(0,0); double inverseHypot = 1 / h; @@ -126,6 +150,44 @@ inline void NegatePath(PathD& path) } } + +//------------------------------------------------------------------------------ +// ClipperOffset::Group methods +//------------------------------------------------------------------------------ + +ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType _end_type): + paths_in(_paths), join_type(_join_type), end_type(_end_type) +{ + bool is_joined = + (end_type == EndType::Polygon) || + (end_type == EndType::Joined); + for (Path64& p: paths_in) + StripDuplicates(p, is_joined); + + // get bounds of each path --> bounds_list + GetMultiBounds(paths_in, bounds_list); + + if (end_type == EndType::Polygon) + { + is_hole_list.reserve(paths_in.size()); + for (const Path64& path : paths_in) + is_hole_list.push_back(Area(path) < 0); + lowest_path_idx = GetLowestClosedPathIdx(bounds_list); + // the lowermost path must be an outer path, so if its orientation is negative, + // then flag the whole group is 'reversed' (will negate delta etc.) + // as this is much more efficient than reversing every path. + is_reversed = (lowest_path_idx >= 0) && is_hole_list[lowest_path_idx]; + if (is_reversed) is_hole_list.flip(); + } + else + { + lowest_path_idx = -1; + is_reversed = false; + is_hole_list.resize(paths_in.size()); + } +} + + //------------------------------------------------------------------------------ // ClipperOffset methods //------------------------------------------------------------------------------ @@ -148,10 +210,10 @@ void ClipperOffset::BuildNormals(const Path64& path) norms.clear(); norms.reserve(path.size()); if (path.size() == 0) return; - Path64::const_iterator path_iter, path_last_iter = --path.cend(); - for (path_iter = path.cbegin(); path_iter != path_last_iter; ++path_iter) + Path64::const_iterator path_iter, path_stop_iter = --path.cend(); + for (path_iter = path.cbegin(); path_iter != path_stop_iter; ++path_iter) norms.push_back(GetUnitNormal(*path_iter,*(path_iter +1))); - norms.push_back(GetUnitNormal(*path_last_iter, *(path.cbegin()))); + norms.push_back(GetUnitNormal(*path_stop_iter, *(path.cbegin()))); } inline PointD TranslatePoint(const PointD& pt, double dx, double dy) @@ -201,19 +263,39 @@ PointD IntersectPoint(const PointD& pt1a, const PointD& pt1b, } } -void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t k) +void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k) +{ + PointD pt1, pt2; + if (j == k) + { + double abs_delta = std::abs(group_delta_); + pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y); + pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y); + } + else + { + pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y); + pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y); + } + path_out.push_back(Point64(pt1)); + path_out.push_back(Point64(pt2)); +} + +void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k) { PointD vec; if (j == k) - vec = PointD(norms[0].y, -norms[0].x); + vec = PointD(norms[j].y, -norms[j].x); else vec = GetAvgUnitVector( PointD(-norms[k].y, norms[k].x), PointD(norms[j].y, -norms[j].x)); + double abs_delta = std::abs(group_delta_); + // now offset the original vertex delta units along unit vector PointD ptQ = PointD(path[j]); - ptQ = TranslatePoint(ptQ, abs_group_delta_ * vec.x, abs_group_delta_ * vec.y); + ptQ = TranslatePoint(ptQ, abs_delta * vec.x, abs_delta * vec.y); // get perpendicular vertices PointD pt1 = TranslatePoint(ptQ, group_delta_ * vec.y, group_delta_ * -vec.x); PointD pt2 = TranslatePoint(ptQ, group_delta_ * -vec.y, group_delta_ * vec.x); @@ -227,8 +309,8 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t pt.z = ptQ.z; #endif //get the second intersect point through reflecion - group.path.push_back(Point64(ReflectPoint(pt, ptQ))); - group.path.push_back(Point64(pt)); + path_out.push_back(Point64(ReflectPoint(pt, ptQ))); + path_out.push_back(Point64(pt)); } else { @@ -237,57 +319,67 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t #ifdef USINGZ pt.z = ptQ.z; #endif - group.path.push_back(Point64(pt)); + path_out.push_back(Point64(pt)); //get the second intersect point through reflecion - group.path.push_back(Point64(ReflectPoint(pt, ptQ))); + path_out.push_back(Point64(ReflectPoint(pt, ptQ))); } } -void ClipperOffset::DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a) +void ClipperOffset::DoMiter(const Path64& path, size_t j, size_t k, double cos_a) { double q = group_delta_ / (cos_a + 1); #ifdef USINGZ - group.path.push_back(Point64( + path_out.push_back(Point64( path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q, path[j].z)); #else - group.path.push_back(Point64( + path_out.push_back(Point64( path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q)); #endif } -void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle) +void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle) { + if (deltaCallback64_) { + // when deltaCallback64_ is assigned, group_delta_ won't be constant, + // so we'll need to do the following calculations for *every* vertex. + double abs_delta = std::fabs(group_delta_); + double arcTol = (arc_tolerance_ > floating_point_tolerance ? + std::min(abs_delta, arc_tolerance_) : + std::log10(2 + abs_delta) * default_arc_tolerance); + double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI); + step_sin_ = std::sin(2 * PI / steps_per_360); + step_cos_ = std::cos(2 * PI / steps_per_360); + if (group_delta_ < 0.0) step_sin_ = -step_sin_; + steps_per_rad_ = steps_per_360 / (2 * PI); + } + Point64 pt = path[j]; PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); if (j == k) offsetVec.Negate(); #ifdef USINGZ - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); #else - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); #endif - if (angle > -PI + 0.01) // avoid 180deg concave + int steps = static_cast<int>(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456 + for (int i = 1; i < steps; ++i) // ie 1 less than steps { - int steps = static_cast<int>(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456 - for (int i = 1; i < steps; ++i) // ie 1 less than steps - { - offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y, - offsetVec.x * step_sin_ + offsetVec.y * step_cos_); + offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y, + offsetVec.x * step_sin_ + offsetVec.y * step_cos_); #ifdef USINGZ - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); #else - group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); + path_out.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); #endif - - } } - group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); + path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } -void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) +void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size_t k) { // Let A = change in angle where edges join // A == 0: ie no change in angle (flat join) @@ -302,50 +394,57 @@ void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) if (sin_a > 1.0) sin_a = 1.0; else if (sin_a < -1.0) sin_a = -1.0; - if (cos_a > 0.99) // almost straight - less than 8 degrees + if (deltaCallback64_) { + group_delta_ = deltaCallback64_(path, norms, j, k); + if (group.is_reversed) group_delta_ = -group_delta_; + } + if (std::fabs(group_delta_) <= floating_point_tolerance) { - group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); - if (cos_a < 0.9998) // greater than 1 degree (#424) - group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); // (#418) + path_out.push_back(path[j]); + return; } - else if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) + + if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) // test for concavity first (#593) { // is concave - group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); + path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_)); // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper - group.path.push_back(path[j]); // (#405) - group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); - } - else if (join_type_ == JoinType::Round) - DoRound(group, path, j, k, std::atan2(sin_a, cos_a)); + // path reversals are fully cleaned with the trailing clipper + path_out.push_back(path[j]); // (#405) + path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_)); + } + else if (cos_a > 0.999 && join_type_ != JoinType::Round) + { + // almost straight - less than 2.5 degree (#424, #482, #526 & #724) + DoMiter(path, j, k, cos_a); + } else if (join_type_ == JoinType::Miter) { - // miter unless the angle is so acute the miter would exceeds ML - if (cos_a > temp_lim_ - 1) DoMiter(group, path, j, k, cos_a); - else DoSquare(group, path, j, k); + // miter unless the angle is sufficiently acute to exceed ML + if (cos_a > temp_lim_ - 1) DoMiter(path, j, k, cos_a); + else DoSquare(path, j, k); } - // don't bother squaring angles that deviate < ~20 degrees because - // squaring will be indistinguishable from mitering and just be a lot slower - else if (cos_a > 0.9) - DoMiter(group, path, j, k, cos_a); + else if (join_type_ == JoinType::Round) + DoRound(path, j, k, std::atan2(sin_a, cos_a)); + else if ( join_type_ == JoinType::Bevel) + DoBevel(path, j, k); else - DoSquare(group, path, j, k); - - k = j; + DoSquare(path, j, k); } -void ClipperOffset::OffsetPolygon(Group& group, Path64& path) +void ClipperOffset::OffsetPolygon(Group& group, const Path64& path) { - for (Path64::size_type i = 0, j = path.size() -1; i < path.size(); j = i, ++i) - OffsetPoint(group, path, i, j); - group.paths_out.push_back(group.path); + path_out.clear(); + for (Path64::size_type j = 0, k = path.size() -1; j < path.size(); k = j, ++j) + OffsetPoint(group, path, j, k); + solution.push_back(path_out); } -void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) +void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path) { OffsetPolygon(group, path); - std::reverse(path.begin(), path.end()); + Path64 reverse_path(path); + std::reverse(reverse_path.begin(), reverse_path.end()); //rebuild normals // BuildNormals(path); std::reverse(norms.begin(), norms.end()); @@ -353,41 +452,36 @@ void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) norms.erase(norms.begin()); NegatePath(norms); - group.path.clear(); - OffsetPolygon(group, path); + OffsetPolygon(group, reverse_path); } -void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) +void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path) { // do the line start cap - switch (end_type_) + if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0); + + if (std::fabs(group_delta_) <= floating_point_tolerance) + path_out.push_back(path[0]); + else { - case EndType::Butt: -#ifdef USINGZ - group.path.push_back(Point64( - path[0].x - norms[0].x * group_delta_, - path[0].y - norms[0].y * group_delta_, - path[0].z)); -#else - group.path.push_back(Point64( - path[0].x - norms[0].x * group_delta_, - path[0].y - norms[0].y * group_delta_)); -#endif - group.path.push_back(GetPerpendic(path[0], norms[0], group_delta_)); - break; - case EndType::Round: - DoRound(group, path, 0,0, PI); - break; - default: - DoSquare(group, path, 0, 0); - break; + switch (end_type_) + { + case EndType::Butt: + DoBevel(path, 0, 0); + break; + case EndType::Round: + DoRound(path, 0, 0, PI); + break; + default: + DoSquare(path, 0, 0); + break; + } } - + size_t highI = path.size() - 1; - // offset the left side going forward - for (Path64::size_type i = 1, k = 0; i < highI; ++i) - OffsetPoint(group, path, i, k); + for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j) + OffsetPoint(group, path, j, k); // reverse normals for (size_t i = highI; i > 0; --i) @@ -395,60 +489,46 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) norms[0] = norms[highI]; // do the line end cap - switch (end_type_) + if (deltaCallback64_) + group_delta_ = deltaCallback64_(path, norms, highI, highI); + + if (std::fabs(group_delta_) <= floating_point_tolerance) + path_out.push_back(path[highI]); + else { - case EndType::Butt: -#ifdef USINGZ - group.path.push_back(Point64( - path[highI].x - norms[highI].x * group_delta_, - path[highI].y - norms[highI].y * group_delta_, - path[highI].z)); -#else - group.path.push_back(Point64( - path[highI].x - norms[highI].x * group_delta_, - path[highI].y - norms[highI].y * group_delta_)); -#endif - group.path.push_back(GetPerpendic(path[highI], norms[highI], group_delta_)); - break; - case EndType::Round: - DoRound(group, path, highI, highI, PI); - break; - default: - DoSquare(group, path, highI, highI); - break; + switch (end_type_) + { + case EndType::Butt: + DoBevel(path, highI, highI); + break; + case EndType::Round: + DoRound(path, highI, highI, PI); + break; + default: + DoSquare(path, highI, highI); + break; + } } - for (size_t i = highI, k = 0; i > 0; --i) - OffsetPoint(group, path, i, k); - group.paths_out.push_back(group.path); + for (size_t j = highI, k = 0; j > 0; k = j, --j) + OffsetPoint(group, path, j, k); + solution.push_back(path_out); } void ClipperOffset::DoGroupOffset(Group& group) { - Rect64 r; - int idx = -1; - //the lowermost polygon must be an outer polygon. So we can use that as the - //designated orientation for outer polygons (needed for tidy-up clipping) - GetBoundsAndLowestPolyIdx(group.paths_in, r, idx); - if (idx < 0) return; - if (group.end_type == EndType::Polygon) { - double area = Area(group.paths_in[idx]); - //if (area == 0) return; // probably unhelpful (#430) - group.is_reversed = (area < 0); - if (group.is_reversed) group_delta_ = -delta_; - else group_delta_ = delta_; - } - else - { - group.is_reversed = false; - group_delta_ = std::abs(delta_) * 0.5; + // a straight path (2 points) can now also be 'polygon' offset + // where the ends will be treated as (180 deg.) joins + if (group.lowest_path_idx < 0) delta_ = std::abs(delta_); + group_delta_ = (group.is_reversed) ? -delta_ : delta_; } - abs_group_delta_ = std::fabs(group_delta_); + else + group_delta_ = std::abs(delta_);// *0.5; - // do range checking - if (!IsSafeOffset(r, abs_group_delta_)) + double abs_delta = std::fabs(group_delta_); + if (!ValidateBounds(group.bounds_list, abs_delta)) { DoError(range_error_i); error_code_ |= range_error_i; @@ -458,80 +538,98 @@ void ClipperOffset::DoGroupOffset(Group& group) join_type_ = group.join_type; end_type_ = group.end_type; - //calculate a sensible number of steps (for 360 deg for the given offset if (group.join_type == JoinType::Round || group.end_type == EndType::Round) { + // calculate a sensible number of steps (for 360 deg for the given offset) // arcTol - when arc_tolerance_ is undefined (0), the amount of // curve imprecision that's allowed is based on the size of the // offset (delta). Obviously very large offsets will almost always // require much less precision. See also offset_triginometry2.svg double arcTol = (arc_tolerance_ > floating_point_tolerance ? - std::min(abs_group_delta_, arc_tolerance_) : - std::log10(2 + abs_group_delta_) * default_arc_tolerance); - double steps_per_360 = PI / std::acos(1 - arcTol / abs_group_delta_); - if (steps_per_360 > abs_group_delta_ * PI) - steps_per_360 = abs_group_delta_ * PI; //ie avoids excessive precision + std::min(abs_delta, arc_tolerance_) : + std::log10(2 + abs_delta) * default_arc_tolerance); + double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI); step_sin_ = std::sin(2 * PI / steps_per_360); step_cos_ = std::cos(2 * PI / steps_per_360); - if (group_delta_ < 0.0) step_sin_ = -step_sin_; - steps_per_rad_ = steps_per_360 / (2 *PI); + if (group_delta_ < 0.0) step_sin_ = -step_sin_; + steps_per_rad_ = steps_per_360 / (2 * PI); } - bool is_joined = - (end_type_ == EndType::Polygon) || - (end_type_ == EndType::Joined); - Paths64::const_iterator path_iter; - for(path_iter = group.paths_in.cbegin(); path_iter != group.paths_in.cend(); ++path_iter) + std::vector<Rect64>::const_iterator path_rect_it = group.bounds_list.cbegin(); + std::vector<bool>::const_iterator is_hole_it = group.is_hole_list.cbegin(); + Paths64::const_iterator path_in_it = group.paths_in.cbegin(); + for ( ; path_in_it != group.paths_in.cend(); ++path_in_it, ++path_rect_it, ++is_hole_it) { - Path64 path = StripDuplicates(*path_iter, is_joined); - Path64::size_type cnt = path.size(); - if (cnt == 0 || ((cnt < 3) && group.end_type == EndType::Polygon)) - continue; + if (!path_rect_it->IsValid()) continue; + Path64::size_type pathLen = path_in_it->size(); + path_out.clear(); - group.path.clear(); - if (cnt == 1) // single point - only valid with open paths + if (pathLen == 1) // single point { if (group_delta_ < 1) continue; + const Point64& pt = (*path_in_it)[0]; //single vertex so build a circle or square ... if (group.join_type == JoinType::Round) { - double radius = abs_group_delta_; - group.path = Ellipse(path[0], radius, radius); + double radius = abs_delta; + int steps = static_cast<int>(std::ceil(steps_per_rad_ * 2 * PI)); //#617 + path_out = Ellipse(pt, radius, radius, steps); #ifdef USINGZ - for (auto& p : group.path) p.z = path[0].z; + for (auto& p : path_out) p.z = pt.z; #endif } else { - int d = (int)std::ceil(abs_group_delta_); - r = Rect64(path[0].x - d, path[0].y - d, path[0].x + d, path[0].y + d); - group.path = r.AsPath(); + int d = (int)std::ceil(abs_delta); + Rect64 r = Rect64(pt.x - d, pt.y - d, pt.x + d, pt.y + d); + path_out = r.AsPath(); #ifdef USINGZ - for (auto& p : group.path) p.z = path[0].z; + for (auto& p : path_out) p.z = pt.z; #endif } - group.paths_out.push_back(group.path); - } - else - { - if ((cnt == 2) && (group.end_type == EndType::Joined)) - { - if (group.join_type == JoinType::Round) - end_type_ = EndType::Round; - else - end_type_ = EndType::Square; - } + solution.push_back(path_out); + continue; + } // end of offsetting a single point + + // when shrinking outer paths, make sure they can shrink this far (#593) + // also when shrinking holes, make sure they too can shrink this far (#715) + if ((group_delta_ > 0) == ToggleBoolIf(*is_hole_it, group.is_reversed) && + (std::min(path_rect_it->Width(), path_rect_it->Height()) <= -group_delta_ * 2) ) + continue; + + if ((pathLen == 2) && (group.end_type == EndType::Joined)) + end_type_ = (group.join_type == JoinType::Round) ? + EndType::Round : + EndType::Square; + + BuildNormals(*path_in_it); + if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it); + else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, *path_in_it); + else OffsetOpenPath(group, *path_in_it); + } +} + + +size_t ClipperOffset::CalcSolutionCapacity() +{ + size_t result = 0; + for (const Group& g : groups_) + result += (g.end_type == EndType::Joined) ? g.paths_in.size() * 2 : g.paths_in.size(); + return result; +} - BuildNormals(path); - if (end_type_ == EndType::Polygon) OffsetPolygon(group, path); - else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, path); - else OffsetOpenPath(group, path); +bool ClipperOffset::CheckReverseOrientation() +{ + // nb: this assumes there's consistency in orientation between groups + bool is_reversed_orientation = false; + for (const Group& g : groups_) + if (g.end_type == EndType::Polygon) + { + is_reversed_orientation = g.is_reversed; + break; } - } - solution.reserve(solution.size() + group.paths_out.size()); - copy(group.paths_out.begin(), group.paths_out.end(), back_inserter(solution)); - group.paths_out.clear(); + return is_reversed_orientation; } void ClipperOffset::ExecuteInternal(double delta) @@ -539,29 +637,29 @@ void ClipperOffset::ExecuteInternal(double delta) error_code_ = 0; solution.clear(); if (groups_.size() == 0) return; + solution.reserve(CalcSolutionCapacity()); - if (std::abs(delta) < 0.5) + if (std::abs(delta) < 0.5) // ie: offset is insignificant { + Paths64::size_type sol_size = 0; + for (const Group& group : groups_) sol_size += group.paths_in.size(); + solution.reserve(sol_size); for (const Group& group : groups_) - { - solution.reserve(solution.size() + group.paths_in.size()); copy(group.paths_in.begin(), group.paths_in.end(), back_inserter(solution)); - } - } - else - { - temp_lim_ = (miter_limit_ <= 1) ? - 2.0 : - 2.0 / (miter_limit_ * miter_limit_); + return; + } - delta_ = delta; - std::vector<Group>::iterator git; - for (git = groups_.begin(); git != groups_.end(); ++git) - { - DoGroupOffset(*git); - if (!error_code_) continue; // all OK - solution.clear(); - } + temp_lim_ = (miter_limit_ <= 1) ? + 2.0 : + 2.0 / (miter_limit_ * miter_limit_); + + delta_ = delta; + std::vector<Group>::iterator git; + for (git = groups_.begin(); git != groups_.end(); ++git) + { + DoGroupOffset(*git); + if (!error_code_) continue; // all OK + solution.clear(); } } @@ -572,19 +670,17 @@ void ClipperOffset::Execute(double delta, Paths64& paths) ExecuteInternal(delta); if (!solution.size()) return; - paths = solution; + bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... Clipper64 c; - c.PreserveCollinear = false; + c.PreserveCollinear(false); //the solution should retain the orientation of the input - c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed; + c.ReverseSolution(reverse_solution_ != paths_reversed); #ifdef USINGZ - if (zCallback64_) { - c.SetZCallback(zCallback64_); - } + if (zCallback64_) { c.SetZCallback(zCallback64_); } #endif c.AddSubject(solution); - if (groups_[0].is_reversed) + if (paths_reversed) c.Execute(ClipType::Union, FillRule::Negative, paths); else c.Execute(ClipType::Union, FillRule::Positive, paths); @@ -598,21 +694,30 @@ void ClipperOffset::Execute(double delta, PolyTree64& polytree) ExecuteInternal(delta); if (!solution.size()) return; + bool paths_reversed = CheckReverseOrientation(); //clean up self-intersections ... Clipper64 c; - c.PreserveCollinear = false; + c.PreserveCollinear(false); //the solution should retain the orientation of the input - c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed; + c.ReverseSolution (reverse_solution_ != paths_reversed); #ifdef USINGZ if (zCallback64_) { c.SetZCallback(zCallback64_); } #endif c.AddSubject(solution); - if (groups_[0].is_reversed) + + + if (paths_reversed) c.Execute(ClipType::Union, FillRule::Negative, polytree); else c.Execute(ClipType::Union, FillRule::Positive, polytree); } +void ClipperOffset::Execute(DeltaCallback64 delta_cb, Paths64& paths) +{ + deltaCallback64_ = delta_cb; + Execute(1.0, paths); +} + } // namespace diff --git a/thirdparty/clipper2/src/clipper.rectclip.cpp b/thirdparty/clipper2/src/clipper.rectclip.cpp index 959972b440..9aa0fc0f76 100644 --- a/thirdparty/clipper2/src/clipper.rectclip.cpp +++ b/thirdparty/clipper2/src/clipper.rectclip.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 14 February 2023 * +* Date : 8 September 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : FAST rectangular clipping * @@ -24,11 +24,11 @@ namespace Clipper2Lib { for (const Point64& pt : path2) { PointInPolygonResult pip = PointInPolygon(pt, path1); - switch (pip) + switch (pip) { case PointInPolygonResult::IsOutside: ++io_count; break; - case PointInPolygonResult::IsInside: --io_count; break; - default: continue; + case PointInPolygonResult::IsInside: --io_count; break; + default: continue; } if (std::abs(io_count) > 1) break; } @@ -66,6 +66,56 @@ namespace Clipper2Lib { return true; } + inline bool IsHorizontal(const Point64& pt1, const Point64& pt2) + { + return pt1.y == pt2.y; + } + + inline bool GetSegmentIntersection(const Point64& p1, + const Point64& p2, const Point64& p3, const Point64& p4, Point64& ip) + { + double res1 = CrossProduct(p1, p3, p4); + double res2 = CrossProduct(p2, p3, p4); + if (res1 == 0) + { + ip = p1; + if (res2 == 0) return false; // segments are collinear + else if (p1 == p3 || p1 == p4) return true; + //else if (p2 == p3 || p2 == p4) { ip = p2; return true; } + else if (IsHorizontal(p3, p4)) return ((p1.x > p3.x) == (p1.x < p4.x)); + else return ((p1.y > p3.y) == (p1.y < p4.y)); + } + else if (res2 == 0) + { + ip = p2; + if (p2 == p3 || p2 == p4) return true; + else if (IsHorizontal(p3, p4)) return ((p2.x > p3.x) == (p2.x < p4.x)); + else return ((p2.y > p3.y) == (p2.y < p4.y)); + } + if ((res1 > 0) == (res2 > 0)) return false; + + double res3 = CrossProduct(p3, p1, p2); + double res4 = CrossProduct(p4, p1, p2); + if (res3 == 0) + { + ip = p3; + if (p3 == p1 || p3 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p3.x > p1.x) == (p3.x < p2.x)); + else return ((p3.y > p1.y) == (p3.y < p2.y)); + } + else if (res4 == 0) + { + ip = p4; + if (p4 == p1 || p4 == p2) return true; + else if (IsHorizontal(p1, p2)) return ((p4.x > p1.x) == (p4.x < p2.x)); + else return ((p4.y > p1.y) == (p4.y < p2.y)); + } + if ((res3 > 0) == (res4 > 0)) return false; + + // segments must intersect to get here + return GetIntersectPoint(p1, p2, p3, p4, ip); + } + inline bool GetIntersection(const Path64& rectPath, const Point64& p, const Point64& p2, Location& loc, Point64& ip) { @@ -74,100 +124,84 @@ namespace Clipper2Lib { switch (loc) { case Location::Left: - if (SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); - else if (p.y < rectPath[0].y && - SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) return true; + else if ((p.y < rectPath[0].y) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); loc = Location::Top; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); loc = Location::Bottom; + return true; } else return false; - break; case Location::Top: - if (SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); - else if (p.x < rectPath[0].x && - SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) return true; + else if ((p.x < rectPath[0].x) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); loc = Location::Left; + return true; } - else if (p.x > rectPath[1].x && - SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) { - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); loc = Location::Right; + return true; } else return false; - break; case Location::Right: - if (SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); - else if (p.y < rectPath[0].y && - SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) return true; + else if ((p.y < rectPath[1].y) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); loc = Location::Top; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); loc = Location::Bottom; + return true; } else return false; - break; case Location::Bottom: - if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); - else if (p.x < rectPath[3].x && - SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) return true; + else if ((p.x < rectPath[3].x) && GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); loc = Location::Left; + return true; } - else if (p.x > rectPath[2].x && - SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) { - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); loc = Location::Right; + return true; } else return false; - break; default: // loc == rInside - if (SegmentsIntersect(p, p2, rectPath[0], rectPath[3], true)) + if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[3], ip); loc = Location::Left; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[0], rectPath[1], true)) + else if (GetSegmentIntersection(p, p2, rectPath[0], rectPath[1], ip)) { - GetIntersectPoint(p, p2, rectPath[0], rectPath[1], ip); loc = Location::Top; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[1], rectPath[2], true)) + else if (GetSegmentIntersection(p, p2, rectPath[1], rectPath[2], ip)) { - GetIntersectPoint(p, p2, rectPath[1], rectPath[2], ip); loc = Location::Right; + return true; } - else if (SegmentsIntersect(p, p2, rectPath[2], rectPath[3], true)) + else if (GetSegmentIntersection(p, p2, rectPath[2], rectPath[3], ip)) { - GetIntersectPoint(p, p2, rectPath[2], rectPath[3], ip); loc = Location::Bottom; + return true; } else return false; - break; } - return true; } inline Location GetAdjacentLocation(Location loc, bool isClockwise) @@ -281,7 +315,7 @@ namespace Clipper2Lib { // RectClip64 //---------------------------------------------------------------------------- - OutPt2* RectClip::Add(Point64 pt, bool start_new) + OutPt2* RectClip64::Add(Point64 pt, bool start_new) { // this method is only called by InternalExecute. // Later splitting & rejoining won't create additional op's, @@ -312,7 +346,7 @@ namespace Clipper2Lib { return result; } - void RectClip::AddCorner(Location prev, Location curr) + void RectClip64::AddCorner(Location prev, Location curr) { if (HeadingClockwise(prev, curr)) Add(rect_as_path_[static_cast<int>(prev)]); @@ -320,7 +354,7 @@ namespace Clipper2Lib { Add(rect_as_path_[static_cast<int>(curr)]); } - void RectClip::AddCorner(Location& loc, bool isClockwise) + void RectClip64::AddCorner(Location& loc, bool isClockwise) { if (isClockwise) { @@ -334,7 +368,7 @@ namespace Clipper2Lib { } } - void RectClip::GetNextLocation(const Path64& path, + void RectClip64::GetNextLocation(const Path64& path, Location& loc, int& i, int highI) { switch (loc) @@ -389,7 +423,7 @@ namespace Clipper2Lib { } //switch } - void RectClip::ExecuteInternal(const Path64& path) + void RectClip64::ExecuteInternal(const Path64& path) { int i = 0, highI = static_cast<int>(path.size()) - 1; Location prev = Location::Inside, loc; @@ -474,7 +508,7 @@ namespace Clipper2Lib { // intersect pt but we'll also need the first intersect pt (ip2) loc = prev; GetIntersection(rect_as_path_, prev_pt, path[i], loc, ip2); - if (crossing_prev != Location::Inside) + if (crossing_prev != Location::Inside && crossing_prev != loc) //579 AddCorner(crossing_prev, loc); if (first_cross_ == Location::Inside) @@ -546,7 +580,7 @@ namespace Clipper2Lib { } } - void RectClip::CheckEdges() + void RectClip64::CheckEdges() { for (size_t i = 0; i < results_.size(); ++i) { @@ -606,7 +640,7 @@ namespace Clipper2Lib { } } - void RectClip::TidyEdges(int idx, OutPt2List& cw, OutPt2List& ccw) + void RectClip64::TidyEdges(int idx, OutPt2List& cw, OutPt2List& ccw) { if (ccw.empty()) return; bool isHorz = ((idx == 1) || (idx == 3)); @@ -619,7 +653,7 @@ namespace Clipper2Lib { p1 = cw[i]; if (!p1 || p1->next == p1->prev) { - cw[i++]->edge = nullptr; + cw[i++] = nullptr; j = 0; continue; } @@ -784,7 +818,7 @@ namespace Clipper2Lib { } } - Path64 RectClip::GetPath(OutPt2*& op) + Path64 RectClip64::GetPath(OutPt2*& op) { if (!op || op->next == op->prev) return Path64(); @@ -814,13 +848,13 @@ namespace Clipper2Lib { return result; } - Paths64 RectClip::Execute(const Paths64& paths, bool convex_only) + Paths64 RectClip64::Execute(const Paths64& paths) { Paths64 result; if (rect_.IsEmpty()) return result; - for (const auto& path : paths) - { + for (const Path64& path : paths) + { if (path.size() < 3) continue; path_bounds_ = GetBounds(path); if (!rect_.Intersects(path_bounds_)) @@ -833,13 +867,10 @@ namespace Clipper2Lib { } ExecuteInternal(path); - if (!convex_only) - { - CheckEdges(); - for (int i = 0; i < 4; ++i) - TidyEdges(i, edges_[i * 2], edges_[i * 2 + 1]); - } - + CheckEdges(); + for (int i = 0; i < 4; ++i) + TidyEdges(i, edges_[i * 2], edges_[i * 2 + 1]); + for (OutPt2*& op : results_) { Path64 tmp = GetPath(op); @@ -850,26 +881,24 @@ namespace Clipper2Lib { //clean up after every loop op_container_ = std::deque<OutPt2>(); results_.clear(); - for (OutPt2List edge : edges_) edge.clear(); + for (OutPt2List &edge : edges_) edge.clear(); start_locs_.clear(); } return result; } //------------------------------------------------------------------------------ - // RectClipLines + // RectClipLines64 //------------------------------------------------------------------------------ - Paths64 RectClipLines::Execute(const Paths64& paths) + Paths64 RectClipLines64::Execute(const Paths64& paths) { Paths64 result; if (rect_.IsEmpty()) return result; for (const auto& path : paths) { - if (path.size() < 2) continue; Rect64 pathrec = GetBounds(path); - if (!rect_.Intersects(pathrec)) continue; ExecuteInternal(path); @@ -888,7 +917,7 @@ namespace Clipper2Lib { return result; } - void RectClipLines::ExecuteInternal(const Path64& path) + void RectClipLines64::ExecuteInternal(const Path64& path) { if (rect_.IsEmpty() || path.size() < 2) return; @@ -958,7 +987,7 @@ namespace Clipper2Lib { /////////////////////////////////////////////////// } - Path64 RectClipLines::GetPath(OutPt2*& op) + Path64 RectClipLines64::GetPath(OutPt2*& op) { Path64 result; if (!op || op == op->next) return result; |