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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to `@mathnotes/mobile-ink` will be documented here.

## [0.3.6] - 2026-10-02

- iOS: selection hit-testing, the selection toolbar and `onInkSelectionChange` now share one bounds helper that rejects non-finite geometry; the event sends `null` bounds instead of NaN. The selection toolbar ignores selection bounds that are not finite. Inverted or overflowing bounds previously produced a NaN frame and terminated the app with `CALayerInvalidGeometry` (seen in production after a pooled page was cleared with a live selection, which 0.3.5 also fixes at the source).
- The podspec now reads its version from `package.json`, so the CocoaPods version and source tag always match the published package.

## [0.3.5] - 2026-10-02

- Fixed a crash when deleting a selection after Undo, Redo or Clear. The selection kept stroke indices from before the history change, and deleting it underflowed a `size_t` and aborted the app with an uncaught `std::length_error`.
Expand Down
6 changes: 5 additions & 1 deletion MathNotesMobileInk.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
s.name = "MathNotesMobileInk"
s.version = "0.3.2"
s.version = package["version"]
s.summary = "Native Skia/Metal mobile ink engine for React Native"
s.homepage = "https://github.com/mathnotes-app/mobile-ink"
s.license = { :type => "Apache-2.0", :file => "LICENSE" }
Expand Down
63 changes: 20 additions & 43 deletions ios/MobileInkModule/MobileInkCanvasView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,13 @@ class MobileInkCanvasView: MTKView {
height: CGFloat(bounds[3] - bounds[1]) / scaleY
)

guard rect.width > 0, rect.height > 0 else {
// CGRect.width reports the standardized (absolute) width, so a rect
// built from inverted or overflowing bounds can pass a `> 0` check
// while its origin and midpoints are infinite or NaN. Setting such a
// frame raises CALayerInvalidGeometry and terminates the app.
guard rect.width > 0, rect.height > 0,
rect.minX.isFinite, rect.minY.isFinite,
rect.maxX.isFinite, rect.maxY.isFinite else {
return nil
}

Expand Down Expand Up @@ -588,36 +594,20 @@ class MobileInkCanvasView: MTKView {
}

private func selectionBoundsContain(_ location: CGPoint, engine: OpaquePointer, padding: CGFloat) -> Bool {
guard getSelectionCount(engine) > 0 else {
guard let selectionRect = selectionBoundsInView(engine: engine) else {
return false
}

var bounds: [Float] = [0, 0, 0, 0]
getSelectionBounds(engine, &bounds)
let selectionRect = CGRect(
x: CGFloat(bounds[0]) / scaleX - padding,
y: CGFloat(bounds[1]) / scaleY - padding,
width: CGFloat(bounds[2] - bounds[0]) / scaleX + padding * 2,
height: CGFloat(bounds[3] - bounds[1]) / scaleY + padding * 2
)

return selectionRect.contains(location)
return selectionRect.insetBy(dx: -padding, dy: -padding).contains(location)
}

private func selectionHandleHitTest(_ location: CGPoint, engine: OpaquePointer) -> Int32? {
guard getSelectionCount(engine) > 0 else {
return nil
}

var bounds: [Float] = [0, 0, 0, 0]
getSelectionBounds(engine, &bounds)
let minX = CGFloat(bounds[0]) / scaleX
let minY = CGFloat(bounds[1]) / scaleY
let maxX = CGFloat(bounds[2]) / scaleX
let maxY = CGFloat(bounds[3]) / scaleY
guard maxX > minX, maxY > minY else {
guard let selectionRect = selectionBoundsInView(engine: engine) else {
return nil
}
let minX = selectionRect.minX
let minY = selectionRect.minY
let maxX = selectionRect.maxX
let maxY = selectionRect.maxY

let centerX = (minX + maxX) * 0.5
let centerY = (minY + maxY) * 0.5
Expand Down Expand Up @@ -737,18 +727,7 @@ class MobileInkCanvasView: MTKView {
cancelHoldToShapePreview()
isHoldToShapeStrokeActive = false
// Check if we're tapping inside an existing selection
let hasExistingSelection = getSelectionCount(engine) > 0
var tappedInsideSelection = false

if hasExistingSelection {
var bounds: [Float] = [0, 0, 0, 0]
getSelectionBounds(engine, &bounds)
let selectionRect = CGRect(x: CGFloat(bounds[0]) / scaleX,
y: CGFloat(bounds[1]) / scaleY,
width: CGFloat(bounds[2] - bounds[0]) / scaleX,
height: CGFloat(bounds[3] - bounds[1]) / scaleY)
tappedInsideSelection = selectionRect.contains(location)
}
let tappedInsideSelection = selectionBoundsInView(engine: engine)?.contains(location) ?? false

if tappedInsideSelection {
// Start moving the existing selection
Expand Down Expand Up @@ -1519,14 +1498,12 @@ class MobileInkCanvasView: MTKView {
guard let engine = drawingEngine else { return }
let count = Int(getSelectionCount(engine))
var payload: [String: Any] = ["count": count]
if count > 0 {
var bounds: [Float] = [0, 0, 0, 0]
getSelectionBounds(engine, &bounds)
if count > 0, let selectionRect = selectionBoundsInView(engine: engine) {
payload["bounds"] = [
"x": CGFloat(bounds[0]) / scaleX,
"y": CGFloat(bounds[1]) / scaleY,
"width": CGFloat(bounds[2] - bounds[0]) / scaleX,
"height": CGFloat(bounds[3] - bounds[1]) / scaleY,
"x": selectionRect.minX,
"y": selectionRect.minY,
"width": selectionRect.width,
"height": selectionRect.height,
]
} else {
payload["bounds"] = NSNull()
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mathnotes/mobile-ink",
"version": "0.3.5",
"version": "0.3.6",
"description": "Production-grade React Native ink engine with native Skia drawing and continuous canvas primitives.",
"license": "Apache-2.0",
"author": "BuilderPro LLC",
Expand Down
Loading