Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions ink/strokes/internal/jni/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ filegroup(
cc_library(
name = "cinterop",
deps = [
":mesh_creation_native",
":stroke_input_batch_native",
],
)
Expand Down Expand Up @@ -237,27 +238,37 @@ 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",
"//ink/geometry:point",
"//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,
],
)
126 changes: 5 additions & 121 deletions ink/strokes/internal/jni/mesh_creation_jni.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,136 +14,20 @@

#include <jni.h>

#include <algorithm>
#include <cstddef>
#include <limits>
#include <vector>

#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<float>::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<Point> points;
points.reserve(input.Size());
for (size_t i = 0; i < input.Size(); ++i) {
points.push_back(input.Get(i).position);
}

std::vector<Point> processed_points =
ink::geometry_internal::CreateClosedShape(points);

absl::StatusOr<Mesh> 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<float> x_values;
std::vector<float> 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<float> x_values = {min_x, min_x, max_x};
std::vector<float> y_values =
slope < 0 ? std::vector<float>{max_y, max_y, min_y}
: std::vector<float>{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<PartitionedMesh> 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<void*>(env), stroke_input_batch_native_pointer,
&ThrowExceptionFromStatusCallback);
}

} // extern "C
153 changes: 153 additions & 0 deletions ink/strokes/internal/jni/mesh_creation_native.cc
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <vector>

#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<float>::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<int>(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<Point> points;
points.reserve(input.Size());
for (size_t i = 0; i < input.Size(); ++i) {
points.push_back(input.Get(i).position);
}

std::vector<Point> processed_points =
ink::geometry_internal::CreateClosedShape(points);

absl::StatusOr<Mesh> 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<float> x_values;
std::vector<float> 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<float> x_values = {min_x, min_x, max_x};
std::vector<float> y_values =
slope < 0 ? std::vector<float>{max_y, max_y, min_y}
: std::vector<float>{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<PartitionedMesh> 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
34 changes: 34 additions & 0 deletions ink/strokes/internal/jni/mesh_creation_native.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

#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_
Loading