diff --git a/ink/strokes/internal/jni/BUILD.bazel b/ink/strokes/internal/jni/BUILD.bazel index 3bd546ef..e47f080d 100644 --- a/ink/strokes/internal/jni/BUILD.bazel +++ b/ink/strokes/internal/jni/BUILD.bazel @@ -39,6 +39,7 @@ filegroup( cc_library( name = "cinterop", deps = [ + ":mesh_creation_native", ":stroke_input_batch_native", ], ) @@ -237,9 +238,25 @@ cc_library( name = "mesh_creation_jni", srcs = ["mesh_creation_jni.cc"], tags = ["keep_dep"], + deps = [ + ":mesh_creation_native", + "//ink/jni/internal:jni_defines", + "//ink/jni/internal:status_jni_helper", + ] + select({ + "@platforms//os:android": [], + "//conditions:default": [ + "@rules_jni//jni", + ], + }), + alwayslink = 1, +) + +cc_library( + name = "mesh_creation_native", + srcs = ["mesh_creation_native.cc"], + hdrs = ["mesh_creation_native.h"], deps = [ ":stroke_input_batch_native_helper", - ":stroke_input_jni_helper", "//ink/geometry:mesh", "//ink/geometry:mesh_format", "//ink/geometry:partitioned_mesh", @@ -247,17 +264,11 @@ cc_library( "//ink/geometry:tessellator", "//ink/geometry/internal:polyline_processing", "//ink/geometry/internal/jni:partitioned_mesh_native_helper", - "//ink/geometry/internal/jni:vec_jni_helper", "//ink/jni/internal:jni_defines", "//ink/jni/internal:status_jni_helper", "//ink/strokes/input:stroke_input_batch", + "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/types:span", - ] + select({ - "@platforms//os:android": [], - "//conditions:default": [ - "@rules_jni//jni", - ], - }), - alwayslink = 1, + ], ) diff --git a/ink/strokes/internal/jni/mesh_creation_jni.cc b/ink/strokes/internal/jni/mesh_creation_jni.cc index 82c57ced..3154e939 100644 --- a/ink/strokes/internal/jni/mesh_creation_jni.cc +++ b/ink/strokes/internal/jni/mesh_creation_jni.cc @@ -14,136 +14,20 @@ #include -#include -#include -#include -#include - -#include "absl/status/statusor.h" -#include "absl/types/span.h" -#include "ink/geometry/internal/jni/partitioned_mesh_native_helper.h" -#include "ink/geometry/internal/polyline_processing.h" -#include "ink/geometry/mesh.h" -#include "ink/geometry/mesh_format.h" -#include "ink/geometry/partitioned_mesh.h" -#include "ink/geometry/point.h" -#include "ink/geometry/tessellator.h" #include "ink/jni/internal/jni_defines.h" #include "ink/jni/internal/status_jni_helper.h" -#include "ink/strokes/input/stroke_input_batch.h" -#include "ink/strokes/internal/jni/stroke_input_batch_native_helper.h" - -namespace { - -using ::ink::Mesh; -using ::ink::PartitionedMesh; -using ::ink::Point; -using ::ink::StrokeInputBatch; -using ::ink::jni::ThrowExceptionFromStatus; -using ::ink::native::CastToStrokeInputBatch; -using ::ink::native::NewNativePartitionedMesh; - -// private method to calculate the slope of a line segment. If the slope is -// infinite, return float::infinity. -float calculateSlope(Point p1, Point p2) { - if (p2.x == p1.x) { - return std::numeric_limits::infinity(); - } - return (p2.y - p1.y) / (p2.x - p1.x); -} +#include "ink/strokes/internal/jni/mesh_creation_native.h" -} // namespace +using ::ink::jni::ThrowExceptionFromStatusCallback; extern "C" { JNI_METHOD(strokes, MeshCreationNative, jlong, createClosedShapeFromStrokeInputBatch) (JNIEnv* env, jobject object, jlong stroke_input_batch_native_pointer) { - const StrokeInputBatch& input = - CastToStrokeInputBatch(stroke_input_batch_native_pointer); - - // If the input is empty then this will return an empty PartitionedMesh with - // no location and no area. This will not intersect with anything if used for - // hit testing. - if (input.IsEmpty()) { - return NewNativePartitionedMesh(); - } - - std::vector points; - points.reserve(input.Size()); - for (size_t i = 0; i < input.Size(); ++i) { - points.push_back(input.Get(i).position); - } - - std::vector processed_points = - ink::geometry_internal::CreateClosedShape(points); - - absl::StatusOr mesh; - // If there are fewer than 3 points the tessellator can't be used to create a - // mesh. Instead, the mesh is created with a single triangle that has repeated - // and overlapping points. This effectively creates a point-like or - // segment-like mesh. The resulting mesh will have an area of 0 but can still - // be used for hit testing via intersection. - if (processed_points.size() < 3) { - const size_t size = processed_points.size(); - std::vector x_values; - std::vector y_values; - x_values.reserve(3); - y_values.reserve(3); - for (int i = 0; i < 3; ++i) { - // If there are 2 points remaining then the first point will appear twice. - // If there is only 1 point all 3 points will be the same. - x_values.push_back(processed_points[i % size].x); - y_values.push_back(processed_points[i % size].y); - } - mesh = Mesh::Create(ink::MeshFormat(), {x_values, y_values}, {0, 1, 2}); - } else { - mesh = ink::CreateMeshFromPolyline(processed_points); - } - if (!mesh.ok() && processed_points.size() >= 2) { - // determine if input points are colinear - float min_x = std::min(processed_points[0].x, processed_points[1].x); - float max_x = std::max(processed_points[0].x, processed_points[1].x); - float min_y = std::min(processed_points[0].y, processed_points[1].y); - float max_y = std::max(processed_points[0].y, processed_points[1].y); - float slope = calculateSlope(processed_points[0], processed_points[1]); - bool is_colinear = true; - for (size_t i = 2; i < processed_points.size(); ++i) { - if (slope != - calculateSlope(processed_points[i - 1], processed_points[i])) { - is_colinear = false; - break; - } - max_x = std::max(max_x, processed_points[i].x); - min_x = std::min(min_x, processed_points[i].x); - max_y = std::max(max_y, processed_points[i].y); - min_y = std::min(min_y, processed_points[i].y); - } - // if so, create a mesh with a single triangle that has repeated and - // overlapping points. This effectively creates a point-like or - // segment-like mesh. The resulting mesh will have an area of 0 but can - // still be used for hit testing via intersection. - // if not, return an error. - if (is_colinear) { - std::vector x_values = {min_x, min_x, max_x}; - std::vector y_values = - slope < 0 ? std::vector{max_y, max_y, min_y} - : std::vector{min_y, min_y, max_y}; - mesh = Mesh::Create(ink::MeshFormat(), {x_values, y_values}, {0, 1, 2}); - } - } - if (!mesh.ok()) { - ThrowExceptionFromStatus(env, mesh.status()); - return 0; - } - - absl::StatusOr partitioned_mesh = - PartitionedMesh::FromMeshes(absl::MakeSpan(&mesh.value(), 1)); - if (!partitioned_mesh.ok()) { - ThrowExceptionFromStatus(env, partitioned_mesh.status()); - return 0; - } - return NewNativePartitionedMesh(*partitioned_mesh); + return MeshCreationNative_createClosedShapeFromStrokeInputBatch( + static_cast(env), stroke_input_batch_native_pointer, + &ThrowExceptionFromStatusCallback); } } // extern "C diff --git a/ink/strokes/internal/jni/mesh_creation_native.cc b/ink/strokes/internal/jni/mesh_creation_native.cc new file mode 100644 index 00000000..ab29e237 --- /dev/null +++ b/ink/strokes/internal/jni/mesh_creation_native.cc @@ -0,0 +1,153 @@ +// Copyright 2024-2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/types/span.h" +#include "ink/geometry/internal/jni/partitioned_mesh_native_helper.h" +#include "ink/geometry/internal/polyline_processing.h" +#include "ink/geometry/mesh.h" +#include "ink/geometry/mesh_format.h" +#include "ink/geometry/partitioned_mesh.h" +#include "ink/geometry/point.h" +#include "ink/geometry/tessellator.h" +#include "ink/strokes/input/stroke_input_batch.h" +#include "ink/strokes/internal/jni/stroke_input_batch_native_helper.h" + +namespace { + +using ::ink::Mesh; +using ::ink::PartitionedMesh; +using ::ink::Point; +using ::ink::StrokeInputBatch; +using ::ink::native::CastToStrokeInputBatch; +using ::ink::native::NewNativePartitionedMesh; + +// private method to calculate the slope of a line segment. If the slope is +// infinite, return float::infinity. +float CalculateSlope(Point p1, Point p2) { + if (p2.x == p1.x) { + return std::numeric_limits::infinity(); + } + return (p2.y - p1.y) / (p2.x - p1.x); +} + +} // namespace + +extern "C" { + +int64_t MeshCreationNative_createClosedShapeFromStrokeInputBatch( + void* jni_env_pass_through, int64_t stroke_input_batch_native_pointer, + void (*throw_from_status_callback)(void*, int, const char*)) { + auto throw_from_status = [throw_from_status_callback, + jni_env_pass_through](const absl::Status& status) { + throw_from_status_callback(jni_env_pass_through, + static_cast(status.code()), + status.ToString().c_str()); + return 0; + }; + + const StrokeInputBatch& input = + CastToStrokeInputBatch(stroke_input_batch_native_pointer); + + // If the input is empty then this will return an empty PartitionedMesh with + // no location and no area. This will not intersect with anything if used for + // hit testing. + if (input.IsEmpty()) { + return NewNativePartitionedMesh(); + } + + std::vector points; + points.reserve(input.Size()); + for (size_t i = 0; i < input.Size(); ++i) { + points.push_back(input.Get(i).position); + } + + std::vector processed_points = + ink::geometry_internal::CreateClosedShape(points); + + absl::StatusOr mesh; + // If there are fewer than 3 points the tessellator can't be used to create a + // mesh. Instead, the mesh is created with a single triangle that has repeated + // and overlapping points. This effectively creates a point-like or + // segment-like mesh. The resulting mesh will have an area of 0 but can still + // be used for hit testing via intersection. + if (processed_points.size() < 3) { + const size_t size = processed_points.size(); + std::vector x_values; + std::vector y_values; + x_values.reserve(3); + y_values.reserve(3); + for (int i = 0; i < 3; ++i) { + // If there are 2 points remaining then the first point will appear twice. + // If there is only 1 point all 3 points will be the same. + x_values.push_back(processed_points[i % size].x); + y_values.push_back(processed_points[i % size].y); + } + mesh = Mesh::Create(ink::MeshFormat(), {x_values, y_values}, {0, 1, 2}); + } else { + mesh = ink::CreateMeshFromPolyline(processed_points); + } + if (!mesh.ok() && processed_points.size() >= 2) { + // determine if input points are colinear + float min_x = std::min(processed_points[0].x, processed_points[1].x); + float max_x = std::max(processed_points[0].x, processed_points[1].x); + float min_y = std::min(processed_points[0].y, processed_points[1].y); + float max_y = std::max(processed_points[0].y, processed_points[1].y); + float slope = CalculateSlope(processed_points[0], processed_points[1]); + bool is_colinear = true; + for (size_t i = 2; i < processed_points.size(); ++i) { + if (slope != + CalculateSlope(processed_points[i - 1], processed_points[i])) { + is_colinear = false; + break; + } + max_x = std::max(max_x, processed_points[i].x); + min_x = std::min(min_x, processed_points[i].x); + max_y = std::max(max_y, processed_points[i].y); + min_y = std::min(min_y, processed_points[i].y); + } + // if so, create a mesh with a single triangle that has repeated and + // overlapping points. This effectively creates a point-like or + // segment-like mesh. The resulting mesh will have an area of 0 but can + // still be used for hit testing via intersection. + // if not, return an error. + if (is_colinear) { + std::vector x_values = {min_x, min_x, max_x}; + std::vector y_values = + slope < 0 ? std::vector{max_y, max_y, min_y} + : std::vector{min_y, min_y, max_y}; + mesh = Mesh::Create(ink::MeshFormat(), {x_values, y_values}, {0, 1, 2}); + } + } + if (!mesh.ok()) { + return throw_from_status(mesh.status()); + return 0; + } + + absl::StatusOr partitioned_mesh = + PartitionedMesh::FromMeshes(absl::MakeSpan(&mesh.value(), 1)); + if (!partitioned_mesh.ok()) { + return throw_from_status(partitioned_mesh.status()); + } + return NewNativePartitionedMesh(*partitioned_mesh); +} + +} // extern "C diff --git a/ink/strokes/internal/jni/mesh_creation_native.h b/ink/strokes/internal/jni/mesh_creation_native.h new file mode 100644 index 00000000..6738b8fe --- /dev/null +++ b/ink/strokes/internal/jni/mesh_creation_native.h @@ -0,0 +1,34 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef INK_STROKES_INTERNAL_JNI_MESH_CREATION_NATIVE_H_ +#define INK_STROKES_INTERNAL_JNI_MESH_CREATION_NATIVE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Creates a closed shape from the given stroke input batch and returns the +// native pointer to the resulting PartitionedMesh. +int64_t MeshCreationNative_createClosedShapeFromStrokeInputBatch( + void* jni_env_pass_through, int64_t stroke_input_batch_native_pointer, + void (*throw_from_status_callback)(void*, int, const char*)); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // INK_STROKES_INTERNAL_JNI_MESH_CREATION_NATIVE_H_