diff --git a/packages/pointer-native-drawing/ios/PencilGestureRecognizer.h b/packages/pointer-native-drawing/ios/PencilGestureRecognizer.h new file mode 100644 index 00000000..978fe8b3 --- /dev/null +++ b/packages/pointer-native-drawing/ios/PencilGestureRecognizer.h @@ -0,0 +1,55 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@class PencilGestureRecognizer; + +/// Data collected from a single gesture recognizer callback, +/// including all coalesced touch samples. +@interface PencilTouchData : NSObject + +@property (nonatomic, readonly) NSArray *coalescedTouches; +@property (nonatomic, readonly) NSArray *predictedTouches; +@property (nonatomic, readonly) UIView *referenceView; +@property (nonatomic, readonly) UITouchType touchType; + +- (instancetype)initWithCoalescedTouches:(NSArray *)coalesced + predictedTouches:(NSArray *)predicted + referenceView:(UIView *)view + touchType:(UITouchType)touchType; + +@end + +/// Protocol for receiving structured pencil touch data with coalesced samples. +@protocol PencilGestureRecognizerDelegate + +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer + touchBeganWith:(PencilTouchData *)data; +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer + touchMovedWith:(PencilTouchData *)data; +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer + touchEndedWith:(PencilTouchData *)data; +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer + touchCancelledWith:(PencilTouchData *)data; + +@end + +/// A gesture recognizer that recognizes Apple Pencil touches and, +/// when acceptsFingerInput is YES, also direct (finger) touches. +/// When finger input is disabled, finger touches are immediately +/// failed so they fall through to underlying gesture recognizers +/// (e.g. RNGH PanGestureHandler). +/// +/// Coalesced touch data is forwarded via the pencilDelegate because +/// UIGestureRecognizer action selectors do not receive the UIEvent. +@interface PencilGestureRecognizer : UIGestureRecognizer + +@property (nonatomic, weak, nullable) id pencilDelegate; + +/// When YES, finger (UITouchTypeDirect) touches are accepted in +/// addition to pencil touches. Default is NO (pencil only). +@property (nonatomic) BOOL acceptsFingerInput; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/pointer-native-drawing/ios/PencilGestureRecognizer.m b/packages/pointer-native-drawing/ios/PencilGestureRecognizer.m new file mode 100644 index 00000000..cea3e22a --- /dev/null +++ b/packages/pointer-native-drawing/ios/PencilGestureRecognizer.m @@ -0,0 +1,191 @@ +#import "PencilGestureRecognizer.h" +#import + +@implementation PencilTouchData + +- (instancetype)initWithCoalescedTouches:(NSArray *)coalesced + predictedTouches:(NSArray *)predicted + referenceView:(UIView *)view + touchType:(UITouchType)touchType +{ + if (self = [super init]) { + _coalescedTouches = [coalesced copy]; + _predictedTouches = [predicted copy]; + _referenceView = view; + _touchType = touchType; + } + return self; +} + +@end + +@implementation PencilGestureRecognizer { + UITouch *_trackedTouch; + UITouchType _trackedTouchType; + PencilTouchData *_pendingBeganData; // deferred began for finger input +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_trackedTouch != nil) { + // Already tracking a touch — handle additional fingers + if (self.acceptsFingerInput) { + if (_pendingBeganData) { + // Drawing never actually started — silently fail so RNGH zoom/pan takes over + _pendingBeganData = nil; + _trackedTouch = nil; + self.state = UIGestureRecognizerStateFailed; + return; + } + // Drawing already started — cancel it + PencilTouchData *data = [[PencilTouchData alloc] + initWithCoalescedTouches:@[_trackedTouch] + predictedTouches:@[] + referenceView:self.view + touchType:_trackedTouchType]; + [self.pencilDelegate pencilRecognizer:self touchCancelledWith:data]; + self.state = UIGestureRecognizerStateCancelled; + _trackedTouch = nil; + return; + } + // Pencil-only mode: ignore extra touches (existing behavior) + for (UITouch *touch in touches) { + [self ignoreTouch:touch forEvent:event]; + } + return; + } + + for (UITouch *touch in touches) { + BOOL isAccepted = (touch.type == UITouchTypePencil) || + (self.acceptsFingerInput && touch.type == UITouchTypeDirect); + if (isAccepted) { + _trackedTouch = touch; + _trackedTouchType = touch.type; + + NSArray *coalesced = [event coalescedTouchesForTouch:touch] ?: @[touch]; + NSArray *predicted = [event predictedTouchesForTouch:touch] ?: @[]; + PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced + predictedTouches:predicted + referenceView:self.view + touchType:_trackedTouchType]; + + if (touch.type == UITouchTypeDirect) { + // Finger: defer Began until first touchesMoved — gives time for + // a 2nd finger to arrive (zoom/pan intent) before committing to draw. + // Stay in Possible so we can transition to Failed if 2nd finger arrives. + _pendingBeganData = data; + return; + } + + // Pencil: immediate began (always drawing intent) + self.state = UIGestureRecognizerStateBegan; + [self.pencilDelegate pencilRecognizer:self touchBeganWith:data]; + return; + } + } + + self.state = UIGestureRecognizerStateFailed; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_trackedTouch == nil) { + return; + } + + for (UITouch *touch in touches) { + if (touch == _trackedTouch) { + // Flush deferred began before emitting moved + if (_pendingBeganData) { + self.state = UIGestureRecognizerStateBegan; + [self.pencilDelegate pencilRecognizer:self touchBeganWith:_pendingBeganData]; + _pendingBeganData = nil; + } else { + self.state = UIGestureRecognizerStateChanged; + } + + NSArray *coalesced = [event coalescedTouchesForTouch:touch] ?: @[touch]; + NSArray *predicted = [event predictedTouchesForTouch:touch] ?: @[]; + PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced + predictedTouches:predicted + referenceView:self.view + touchType:_trackedTouchType]; + [self.pencilDelegate pencilRecognizer:self touchMovedWith:data]; + return; + } + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_trackedTouch == nil) { + return; + } + + for (UITouch *touch in touches) { + if (touch == _trackedTouch) { + // Flush deferred began if finger was tapped without moving + if (_pendingBeganData) { + self.state = UIGestureRecognizerStateBegan; + [self.pencilDelegate pencilRecognizer:self touchBeganWith:_pendingBeganData]; + _pendingBeganData = nil; + } + + NSArray *coalesced = [event coalescedTouchesForTouch:touch] ?: @[touch]; + NSArray *predicted = [event predictedTouchesForTouch:touch] ?: @[]; + PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced + predictedTouches:predicted + referenceView:self.view + touchType:_trackedTouchType]; + [self.pencilDelegate pencilRecognizer:self touchEndedWith:data]; + + self.state = UIGestureRecognizerStateEnded; + _trackedTouch = nil; + return; + } + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + if (_trackedTouch == nil) { + return; + } + + for (UITouch *touch in touches) { + if (touch == _trackedTouch) { + NSArray *coalesced = @[touch]; + PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced + predictedTouches:@[] + referenceView:self.view + touchType:_trackedTouchType]; + [self.pencilDelegate pencilRecognizer:self touchCancelledWith:data]; + + self.state = UIGestureRecognizerStateCancelled; + _trackedTouch = nil; + return; + } + } +} + +// Allow RNGH gesture recognizers to run simultaneously — the native +// recognizer must not block 2-finger zoom/pan on the parent view. +- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer +{ + return NO; +} + +- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer +{ + return NO; +} + +- (void)reset +{ + [super reset]; + _trackedTouch = nil; + _trackedTouchType = UITouchTypeDirect; + _pendingBeganData = nil; +} + +@end diff --git a/packages/pointer-native-drawing/ios/StylusInputView.h b/packages/pointer-native-drawing/ios/StylusInputView.h new file mode 100644 index 00000000..75a2a018 --- /dev/null +++ b/packages/pointer-native-drawing/ios/StylusInputView.h @@ -0,0 +1,8 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface StylusInputView : RCTViewComponentView +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/pointer-native-drawing/ios/StylusInputView.mm b/packages/pointer-native-drawing/ios/StylusInputView.mm new file mode 100644 index 00000000..1f9d04e7 --- /dev/null +++ b/packages/pointer-native-drawing/ios/StylusInputView.mm @@ -0,0 +1,147 @@ +#import "StylusInputView.h" +#import "PencilGestureRecognizer.h" + +#import +#import +#import +#import + +using namespace facebook::react; + +// --------------------------------------------------------------------------- +// StylusInputView — Fabric component for Apple Pencil touch capture. +// Native path building (CatmullRomPathBuilder, NativeDrawingSession) is +// added separately in MAT-362. +// --------------------------------------------------------------------------- + +@interface StylusInputView () +@end + +@implementation StylusInputView { + PencilGestureRecognizer *_pencilRecognizer; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &newProps = static_cast(*props); + _pencilRecognizer.acceptsFingerInput = newProps.acceptFingerInput; + [super updateProps:props oldProps:oldProps]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.userInteractionEnabled = YES; + self.backgroundColor = [UIColor clearColor]; + + _pencilRecognizer = [[PencilGestureRecognizer alloc] initWithTarget:nil action:nil]; + _pencilRecognizer.pencilDelegate = self; + _pencilRecognizer.cancelsTouchesInView = NO; + _pencilRecognizer.delaysTouchesBegan = NO; + [self addGestureRecognizer:_pencilRecognizer]; + } + return self; +} + +#pragma mark - PencilGestureRecognizerDelegate + +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer touchBeganWith:(PencilTouchData *)data +{ + [self _emitPhase:0 withData:data]; +} + +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer touchMovedWith:(PencilTouchData *)data +{ + [self _emitPhase:1 withData:data]; +} + +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer touchEndedWith:(PencilTouchData *)data +{ + [self _emitPhase:2 withData:data]; +} + +- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer touchCancelledWith:(PencilTouchData *)data +{ + [self _emitPhase:3 withData:data]; +} + +#pragma mark - Event emission + +- (void)_emitPhase:(int)phase withData:(PencilTouchData *)data +{ + auto eventEmitter = [self _stylusTouchEmitter]; + if (!eventEmitter) return; + + UIView *refView = data.referenceView; + NSArray *touches = data.coalescedTouches; + NSUInteger count = touches.count; + if (count == 0) return; + + std::vector xs(count), ys(count), timestamps(count), + forces(count), altitudes(count), azimuths(count); + + for (NSUInteger i = 0; i < count; i++) { + UITouch *t = touches[i]; + CGPoint loc = [t preciseLocationInView:refView]; + xs[i] = loc.x; + ys[i] = loc.y; + timestamps[i] = t.timestamp * 1000.0; + forces[i] = t.maximumPossibleForce > 0 + ? t.force / t.maximumPossibleForce : 0.0; + altitudes[i] = t.altitudeAngle; + azimuths[i] = [t azimuthAngleInView:refView]; + } + + NSArray *predicted = data.predictedTouches; + NSUInteger pCount = predicted.count; + + std::vector pxs(pCount), pys(pCount), pts(pCount), + pforces(pCount), palts(pCount), pazs(pCount); + + for (NSUInteger i = 0; i < pCount; i++) { + UITouch *t = predicted[i]; + CGPoint loc = [t preciseLocationInView:refView]; + pxs[i] = loc.x; + pys[i] = loc.y; + pts[i] = t.timestamp * 1000.0; + pforces[i] = t.maximumPossibleForce > 0 + ? t.force / t.maximumPossibleForce : 0.0; + palts[i] = t.altitudeAngle; + pazs[i] = [t azimuthAngleInView:refView]; + } + + StylusInputViewEventEmitter::OnStylusTouch event; + event.phase = phase; + event.pointerType = (data.touchType == UITouchTypePencil) ? 1 : 0; + event.xs = {xs.begin(), xs.end()}; + event.ys = {ys.begin(), ys.end()}; + event.timestamps = {timestamps.begin(), timestamps.end()}; + event.forces = {forces.begin(), forces.end()}; + event.altitudes = {altitudes.begin(), altitudes.end()}; + event.azimuths = {azimuths.begin(), azimuths.end()}; + event.predictedXs = {pxs.begin(), pxs.end()}; + event.predictedYs = {pys.begin(), pys.end()}; + event.predictedTimestamps = {pts.begin(), pts.end()}; + event.predictedForces = {pforces.begin(), pforces.end()}; + event.predictedAltitudes = {palts.begin(), palts.end()}; + event.predictedAzimuths = {pazs.begin(), pazs.end()}; + eventEmitter->onStylusTouch(event); +} + +- (const StylusInputViewEventEmitter *)_stylusTouchEmitter +{ + if (!_eventEmitter) return nullptr; + return static_cast(_eventEmitter.get()); +} + +@end + +Class StylusInputViewCls(void) +{ + return StylusInputView.class; +} diff --git a/packages/pointer-native-drawing/package.json b/packages/pointer-native-drawing/package.json index 98f89420..9e3ef6ee 100644 --- a/packages/pointer-native-drawing/package.json +++ b/packages/pointer-native-drawing/package.json @@ -16,9 +16,20 @@ }, "./package.json": "./package.json" }, + "codegenConfig": { + "name": "PointerNativeDrawingSpec", + "type": "components", + "jsSrcsDir": "src/specs", + "ios": { + "componentProvider": { + "StylusInputView": "StylusInputView" + } + } + }, "files": [ "dist", - "src" + "src", + "ios" ], "license": "MIT", "scripts": { diff --git a/packages/pointer-native-drawing/pointer-native-drawing.podspec b/packages/pointer-native-drawing/pointer-native-drawing.podspec new file mode 100644 index 00000000..0c070249 --- /dev/null +++ b/packages/pointer-native-drawing/pointer-native-drawing.podspec @@ -0,0 +1,17 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "pointer-native-drawing" + s.version = package["version"] + s.summary = package["description"] + s.homepage = "https://github.com/team-ppointer/pointer-native-drawing" + s.license = package["license"] + s.author = "team-ppointer" + s.platforms = { :ios => "15.1" } + s.source = { :git => "https://github.com/team-ppointer/pointer-native-drawing.git", :tag => s.version.to_s } + s.source_files = "ios/**/*.{h,m,mm,cpp}" + + install_modules_dependencies(s) +end diff --git a/packages/pointer-native-drawing/src/DrawingCanvas.tsx b/packages/pointer-native-drawing/src/DrawingCanvas.tsx index 4025cc28..cc959b9d 100644 --- a/packages/pointer-native-drawing/src/DrawingCanvas.tsx +++ b/packages/pointer-native-drawing/src/DrawingCanvas.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useMemo, } from 'react'; -import { View, StyleSheet, ScrollView } from 'react-native'; +import { View, StyleSheet, ScrollView, Platform } from 'react-native'; import { Path, type SkPath, Skia, Circle, Group } from '@shopify/react-native-skia'; import { Gesture, GestureDetector, PointerType } from 'react-native-gesture-handler'; import { useSharedValue } from 'react-native-reanimated'; @@ -24,6 +24,7 @@ import { import { computeStrokeBounds, safeMax } from './model/strokeUtils'; import { HistoryManager } from './engine/HistoryManager'; import { type DrawingInputCallbacks } from './input/inputTypes'; +import { useNativeStylusAdapter } from './input/nativeStylusAdapter'; import { useRnghPanAdapter } from './input/rnghPanAdapter'; import { SkiaDrawingCanvasSurface } from './render/skia/SkiaDrawingCanvasSurface'; import { useSkiaDrawingRenderer } from './render/skia/useSkiaDrawingRenderer'; @@ -354,11 +355,21 @@ const DrawingCanvas = forwardRef( ] ); + // iOS: native UIPencil 입력 (240Hz coalesced + predicted touches). rngh pan 비활성화. + // Android/Web: rngh pan adapter (native module 없음). + const useNativeStylus = Platform.OS === 'ios'; + const inputAdapter = useRnghPanAdapter({ eraserMode, pencilOnly: true, minDistance: 1, callbacks: drawingCallbacks, + enabled: !useNativeStylus, + }); + + const nativeStylusAdapter = useNativeStylusAdapter({ + eraserMode, + callbacks: drawingCallbacks, }); const hoverGesture = useMemo( @@ -441,6 +452,7 @@ const DrawingCanvas = forwardRef( /> + {nativeStylusAdapter?.overlay} diff --git a/packages/pointer-native-drawing/src/input/nativeStylusAdapter.tsx b/packages/pointer-native-drawing/src/input/nativeStylusAdapter.tsx new file mode 100644 index 00000000..fddb4b52 --- /dev/null +++ b/packages/pointer-native-drawing/src/input/nativeStylusAdapter.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useMemo, useRef } from 'react'; +import { Platform, StyleSheet } from 'react-native'; + +import { type InputEvent } from '../model/drawingTypes'; +import StylusInputView from '../specs/StylusInputViewNativeComponent'; + +import { type DrawingInputCallbacks } from './inputTypes'; +import { type InputOverlayAdapter } from './inputAdapterTypes'; + +type StylusTouchPayload = { + phase: number; + pointerType: number; + xs: readonly number[]; + ys: readonly number[]; + timestamps: readonly number[]; + forces: readonly number[]; + altitudes: readonly number[]; + azimuths: readonly number[]; + predictedXs: readonly number[]; + predictedYs: readonly number[]; + predictedTimestamps: readonly number[]; + predictedForces: readonly number[]; + predictedAltitudes: readonly number[]; + predictedAzimuths: readonly number[]; +}; + +const RAD_TO_DEG = 180 / Math.PI; + +// UITouch.timestamp (uptime ms) → epoch ms 변환 +const BOOT_TIME_OFFSET_MS = Date.now() - performance.now(); + +function uptimeMsToEpochMs(uptimeMs: number): number { + return uptimeMs + BOOT_TIME_OFFSET_MS; +} + +function unpackTouches( + xs: readonly number[], + ys: readonly number[], + timestamps: readonly number[], + forces: readonly number[], + altitudes: readonly number[], + azimuths: readonly number[], + pointerType: 'pen' | 'touch' +): InputEvent[] { + const count = xs.length; + const events: InputEvent[] = new Array(count); + + for (let i = 0; i < count; i++) { + const alt = altitudes[i]; + const az = azimuths[i]; + let tiltX: number; + let tiltY: number; + + if (alt >= Math.PI / 2) { + tiltX = 0; + tiltY = 0; + } else if (alt <= 0) { + tiltX = Math.round(Math.atan2(Math.cos(az), 0) * RAD_TO_DEG); + tiltY = Math.round(Math.atan2(Math.sin(az), 0) * RAD_TO_DEG); + } else { + const tanAlt = Math.tan(alt); + tiltX = Math.round(Math.atan2(Math.cos(az), tanAlt) * RAD_TO_DEG); + tiltY = Math.round(Math.atan2(Math.sin(az), tanAlt) * RAD_TO_DEG); + } + + events[i] = { + x: xs[i], + y: ys[i], + timestamp: uptimeMsToEpochMs(timestamps[i]), + pressure: forces[i], + pointerType, + tiltX, + tiltY, + }; + } + + return events; +} + +export type NativeStylusAdapterConfig = { + callbacks: DrawingInputCallbacks; + eraserMode: boolean; +}; + +export function useNativeStylusAdapter( + config: NativeStylusAdapterConfig +): InputOverlayAdapter | null { + const configRef = useRef(config); + configRef.current = config; + + const handleStylusTouch = useCallback((event: { nativeEvent: StylusTouchPayload }) => { + const { nativeEvent } = event; + const { callbacks, eraserMode } = configRef.current; + const isPencil = nativeEvent.pointerType === 1; + const ptrType = isPencil ? ('pen' as const) : ('touch' as const); + + const inputs = unpackTouches( + nativeEvent.xs, + nativeEvent.ys, + nativeEvent.timestamps, + nativeEvent.forces, + nativeEvent.altitudes, + nativeEvent.azimuths, + ptrType + ); + + if (inputs.length === 0) return; + + switch (nativeEvent.phase) { + case 0: { + callbacks.onInteractionBegin(); + if (eraserMode) { + callbacks.onEraseStart(inputs[0]); + for (let i = 1; i < inputs.length; i++) callbacks.onEraseMove(inputs[i]); + } else { + callbacks.onDrawStart(inputs[0]); + for (let i = 1; i < inputs.length; i++) callbacks.onDrawMove(inputs[i]); + } + break; + } + case 1: { + if (eraserMode) { + for (let i = 0; i < inputs.length; i++) callbacks.onEraseMove(inputs[i]); + } else { + for (let i = 0; i < inputs.length; i++) callbacks.onDrawMove(inputs[i]); + } + break; + } + case 2: { + if (eraserMode) { + for (let i = 0; i < inputs.length; i++) callbacks.onEraseMove(inputs[i]); + } else { + for (let i = 0; i < inputs.length; i++) callbacks.onDrawMove(inputs[i]); + callbacks.onDrawEnd(); + } + callbacks.onInteractionFinalize(); + break; + } + case 3: { + if (!eraserMode) { + callbacks.onDrawCancel('interrupted'); + } + callbacks.onInteractionFinalize(); + break; + } + } + }, []); + + const overlay = useMemo( + () => + Platform.OS === 'ios' ? ( + + ) : null, + [handleStylusTouch] + ); + + if (Platform.OS !== 'ios') return null; + + return { overlay }; +} diff --git a/packages/pointer-native-drawing/src/specs/StylusInputViewNativeComponent.ts b/packages/pointer-native-drawing/src/specs/StylusInputViewNativeComponent.ts new file mode 100644 index 00000000..275335e3 --- /dev/null +++ b/packages/pointer-native-drawing/src/specs/StylusInputViewNativeComponent.ts @@ -0,0 +1,32 @@ +import type { ViewProps } from 'react-native'; +import type { DirectEventHandler, Double, Int32 } from 'react-native/Libraries/Types/CodegenTypes'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +type StylusTouchEvent = Readonly<{ + phase: Int32; // 0=began, 1=moved, 2=ended, 3=cancelled + pointerType: Int32; // 0=touch (finger), 1=pencil + // Parallel arrays — one entry per coalesced touch sample. + // Using parallel arrays to work around react-native#47113 + // (codegen nested-object-in-array bug). + xs: Double[]; + ys: Double[]; + timestamps: Double[]; // ms since system boot (UITouch.timestamp * 1000) + forces: Double[]; // 0..1 normalized + altitudes: Double[]; // radians, 0=parallel π/2=perpendicular + azimuths: Double[]; // radians, UIKit convention (0=right, increases CW) + // Predicted touches — iOS-estimated future positions (1-2 samples ahead). + // Used for rendering only, NOT committed to stroke model. + predictedXs: Double[]; + predictedYs: Double[]; + predictedTimestamps: Double[]; + predictedForces: Double[]; + predictedAltitudes: Double[]; + predictedAzimuths: Double[]; +}>; + +export interface NativeProps extends ViewProps { + acceptFingerInput?: boolean; + onStylusTouch: DirectEventHandler; +} + +export default codegenNativeComponent('StylusInputView');