summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--COPYRIGHT.txt2
-rw-r--r--core/io/resource.cpp19
-rw-r--r--core/io/resource.h2
-rw-r--r--core/io/stream_peer_gzip.cpp1
-rw-r--r--doc/classes/EditorSettings.xml3
-rw-r--r--doc/classes/NavigationMeshSourceGeometryData2D.xml7
-rw-r--r--doc/classes/NavigationMeshSourceGeometryData3D.xml7
-rw-r--r--doc/classes/Resource.xml12
-rw-r--r--doc/classes/XRInterface.xml22
-rwxr-xr-xdoc/tools/make_rst.py42
-rw-r--r--drivers/gles3/rasterizer_scene_gles3.cpp6
-rw-r--r--drivers/gles3/shaders/particles.glsl30
-rw-r--r--drivers/gles3/storage/particles_storage.cpp6
-rw-r--r--drivers/wasapi/audio_driver_wasapi.cpp9
-rw-r--r--editor/doc_tools.cpp2
-rw-r--r--editor/editor_file_system.cpp32
-rw-r--r--editor/editor_help.cpp537
-rw-r--r--editor/editor_help.h40
-rw-r--r--editor/editor_inspector.cpp4
-rw-r--r--editor/editor_node.cpp14
-rw-r--r--editor/editor_resource_preview.cpp4
-rw-r--r--editor/editor_settings.cpp1
-rw-r--r--editor/find_in_files.cpp23
-rw-r--r--editor/gui/scene_tree_editor.cpp87
-rw-r--r--editor/import/3d/scene_import_settings.cpp26
-rw-r--r--editor/plugins/abstract_polygon_2d_editor.cpp19
-rw-r--r--editor/plugins/abstract_polygon_2d_editor.h1
-rw-r--r--editor/plugins/canvas_item_editor_plugin.cpp44
-rw-r--r--editor/plugins/canvas_item_editor_plugin.h4
-rw-r--r--editor/plugins/cast_2d_editor_plugin.cpp22
-rw-r--r--editor/plugins/cast_2d_editor_plugin.h1
-rw-r--r--editor/plugins/collision_shape_2d_editor_plugin.cpp5
-rw-r--r--editor/plugins/collision_shape_2d_editor_plugin.h1
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp55
-rw-r--r--editor/plugins/node_3d_editor_plugin.h2
-rw-r--r--editor/plugins/packed_scene_translation_parser_plugin.cpp47
-rw-r--r--editor/plugins/packed_scene_translation_parser_plugin.h1
-rw-r--r--editor/plugins/path_2d_editor_plugin.cpp65
-rw-r--r--editor/plugins/path_2d_editor_plugin.h1
-rw-r--r--editor/plugins/visual_shader_editor_plugin.cpp1
-rw-r--r--editor/project_manager.cpp7
-rw-r--r--editor/project_manager/project_dialog.cpp1047
-rw-r--r--editor/project_manager/project_dialog.h58
-rw-r--r--editor/themes/editor_theme_manager.cpp3
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml12
-rw-r--r--modules/gdscript/gdscript_parser.cpp24
-rw-r--r--modules/gdscript/language_server/gdscript_language_protocol.cpp26
-rw-r--r--modules/gdscript/language_server/gdscript_language_protocol.h2
-rw-r--r--modules/gdscript/language_server/gdscript_language_server.cpp9
-rw-r--r--modules/gdscript/language_server/gdscript_language_server.h1
-rw-r--r--modules/navigation/2d/nav_mesh_generator_2d.cpp4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java132
-rw-r--r--platform/windows/display_server_windows.cpp2
-rw-r--r--scene/resources/2d/tile_set.h4
-rw-r--r--scene/resources/navigation_mesh_source_geometry_data_2d.cpp8
-rw-r--r--scene/resources/navigation_mesh_source_geometry_data_2d.h2
-rw-r--r--scene/resources/navigation_mesh_source_geometry_data_3d.cpp12
-rw-r--r--scene/resources/navigation_mesh_source_geometry_data_3d.h2
-rw-r--r--scene/resources/visual_shader_nodes.h24
-rw-r--r--scene/resources/visual_shader_particle_nodes.h8
-rw-r--r--scene/resources/visual_shader_sdf_nodes.h10
-rw-r--r--servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp10
-rw-r--r--servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp10
-rw-r--r--servers/rendering/renderer_rd/renderer_scene_render_rd.cpp9
-rw-r--r--servers/rendering/renderer_rd/storage_rd/particles_storage.cpp5
-rw-r--r--servers/rendering/renderer_scene_cull.cpp2
-rw-r--r--servers/rendering/rendering_device_commons.h2
-rw-r--r--tests/servers/test_navigation_server_3d.h47
-rw-r--r--thirdparty/README.md7
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.core.h188
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.engine.h69
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.export.h853
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.h289
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.minkowski.h4
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.offset.h42
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.rectclip.h19
-rw-r--r--thirdparty/clipper2/include/clipper2/clipper.version.h6
-rw-r--r--thirdparty/clipper2/patches/clipper2-exceptions.patch (renamed from thirdparty/clipper2/clipper2-exceptions.patch)11
-rw-r--r--thirdparty/clipper2/src/clipper.engine.cpp755
-rw-r--r--thirdparty/clipper2/src/clipper.offset.cpp561
-rw-r--r--thirdparty/clipper2/src/clipper.rectclip.cpp179
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 = &empty;
render_data.reflection_probes = &empty;
render_data.voxel_gi_instances = &empty;
}
+ 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 = &empty;
+ }
+
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;