@symbiote-native/engine 0.1.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +17 -10
  2. package/build/action-sheet-ios/index.js +4 -1
  3. package/build/alert/index.ios.js +10 -0
  4. package/build/animated/animations/composition.js +3 -1
  5. package/build/animated/animations/spring-config.js +4 -1
  6. package/build/animated/animations/spring.js +23 -9
  7. package/build/animated/animations/timing.js +3 -2
  8. package/build/animated/bezier.js +2 -1
  9. package/build/animated/color.d.ts +2 -8
  10. package/build/animated/color.js +8 -47
  11. package/build/animated/easing.js +0 -8
  12. package/build/animated/event.js +7 -3
  13. package/build/animated/graph.d.ts +13 -5
  14. package/build/animated/graph.js +92 -13
  15. package/build/animated/index.d.ts +3 -3
  16. package/build/animated/index.js +3 -3
  17. package/build/animated/interpolation.js +8 -3
  18. package/build/animated/leaf-lifecycle.d.ts +7 -0
  19. package/build/animated/leaf-lifecycle.js +185 -0
  20. package/build/animated/mock.js +1 -1
  21. package/build/animated/native/native-animated.js +2 -1
  22. package/build/animated/operators.js +27 -6
  23. package/build/animated/props.js +1 -1
  24. package/build/animated/rgba.d.ts +8 -0
  25. package/build/animated/rgba.js +70 -0
  26. package/build/animated/style.js +13 -3
  27. package/build/animated/value-xy.d.ts +8 -5
  28. package/build/animated/value-xy.js +4 -1
  29. package/build/animated/value.js +12 -7
  30. package/build/appearance/index.js +4 -1
  31. package/build/commit.d.ts +13 -0
  32. package/build/commit.js +146 -14
  33. package/build/debug.d.ts +1 -1
  34. package/build/debug.js +10 -3
  35. package/build/events/index.js +19 -10
  36. package/build/fabric-props.js +8 -2
  37. package/build/image-loader.js +3 -2
  38. package/build/index.d.ts +23 -19
  39. package/build/index.js +19 -11
  40. package/build/interaction-manager/index.js +2 -1
  41. package/build/layout-animation/index.js +12 -5
  42. package/build/native-events.js +2 -1
  43. package/build/native-modules/index.js +3 -1
  44. package/build/node.d.ts +25 -0
  45. package/build/node.js +168 -2
  46. package/build/pan-responder/index.js +20 -9
  47. package/build/permissions-android/index.js +3 -1
  48. package/build/platform/index.android.d.ts +1 -1
  49. package/build/platform/index.android.js +1 -1
  50. package/build/platform/index.ios.d.ts +1 -1
  51. package/build/platform-color/index.js +3 -1
  52. package/build/post-commit.d.ts +1 -0
  53. package/build/post-commit.js +6 -0
  54. package/build/process-background-image/index.js +30 -9
  55. package/build/process-filter.js +11 -4
  56. package/build/process-font-variant.js +0 -1
  57. package/build/process-transform/index.js +13 -19
  58. package/build/registry.js +10 -2
  59. package/build/report-error.d.ts +5 -0
  60. package/build/report-error.js +63 -0
  61. package/build/share/index.android.js +1 -1
  62. package/build/share/index.ios.js +1 -1
  63. package/build/status-bar/index.android.js +8 -10
  64. package/build/status-bar/index.ios.js +1 -1
  65. package/build/style-registry/index.d.ts +9 -2
  66. package/build/style-registry/index.js +149 -95
  67. package/build/style-registry/scope.d.ts +15 -1
  68. package/build/style-registry/scope.js +29 -27
  69. package/build/style-sheet/index.js +4 -2
  70. package/build/styles.d.ts +11 -1
  71. package/build/surface.d.ts +1 -1
  72. package/build/surface.js +7 -1
  73. package/build/tags.d.ts +1 -0
  74. package/build/tags.js +31 -1
  75. package/build/touch-history.js +9 -2
  76. package/build/vibration/index.android.js +2 -3
  77. package/build/vibration/index.ios.js +1 -1
  78. package/build/view-config.js +14 -2
  79. package/package.json +2 -2
  80. package/build/animated/interpolation-node.d.ts +0 -15
  81. package/build/animated/interpolation-node.js +0 -58
  82. package/build/style-registry/scope/index.d.ts +0 -3
  83. package/build/style-registry/scope/index.js +0 -41
  84. package/build/touch-history/index.d.ts +0 -23
  85. package/build/touch-history/index.js +0 -150
  86. package/build/type-guards/index.d.ts +0 -4
  87. package/build/type-guards/index.js +0 -15
package/build/styles.d.ts CHANGED
@@ -187,7 +187,11 @@ export interface IViewStyle {
187
187
  shadowRadius?: number;
188
188
  elevation?: number;
189
189
  transform?: ITransformProp[];
190
- transformOrigin?: [string | number, string | number, string | number] | string;
190
+ transformOrigin?: [
191
+ string | number,
192
+ string | number,
193
+ string | number
194
+ ] | string;
191
195
  boxShadow?: IBoxShadowValue[] | string;
192
196
  filter?: IFilterFunction[] | string;
193
197
  experimental_backgroundImage?: IBackgroundImageValue[] | string;
@@ -207,6 +211,12 @@ export interface ITextStyle extends IViewStyle {
207
211
  textDecorationLine?: 'none' | 'underline' | 'line-through' | 'underline line-through';
208
212
  textDecorationColor?: IColorValue;
209
213
  textDecorationStyle?: 'solid' | 'double' | 'dotted' | 'dashed';
214
+ textShadowColor?: IColorValue;
215
+ textShadowOffset?: {
216
+ width: number;
217
+ height: number;
218
+ };
219
+ textShadowRadius?: number;
210
220
  fontVariant?: string[];
211
221
  writingDirection?: 'auto' | 'ltr' | 'rtl';
212
222
  includeFontPadding?: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { IRootTag } from './fabric';
2
- import type { ISymbioteNode } from './node';
2
+ import { type ISymbioteNode } from './node';
3
3
  export declare class SymbioteSurface {
4
4
  readonly rootTag: IRootTag;
5
5
  readonly children: ISymbioteNode[];
package/build/surface.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { commitChildren } from './commit.js';
5
5
  import { dlog } from './debug.js';
6
6
  import { installEventHandler } from './events/index.js';
7
+ import { markDirty } from './node.js';
7
8
  export class SymbioteSurface {
8
9
  rootTag;
9
10
  children = [];
@@ -46,6 +47,9 @@ export class SymbioteSurface {
46
47
  this.commit();
47
48
  });
48
49
  }
50
+ // Splices `parent.children` directly instead of going through node.ts's removeChild, so it
51
+ // owes the same dirty-mark - otherwise a node pulled out of a subtree here leaves that subtree
52
+ // looking clean and the commit walk skips right over the hole.
49
53
  detach(child) {
50
54
  const parent = child.parent;
51
55
  if (parent) {
@@ -53,6 +57,7 @@ export class SymbioteSurface {
53
57
  if (index >= 0)
54
58
  parent.children.splice(index, 1);
55
59
  child.parent = undefined;
60
+ markDirty(parent);
56
61
  return;
57
62
  }
58
63
  const topIndex = this.children.indexOf(child);
@@ -62,6 +67,7 @@ export class SymbioteSurface {
62
67
  }
63
68
  export function createSurface(rootTag) {
64
69
  installEventHandler();
70
+ const surface = new SymbioteSurface(rootTag);
65
71
  dlog(`surface created root=${rootTag}`);
66
- return new SymbioteSurface(rootTag);
72
+ return surface;
67
73
  }
package/build/tags.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export declare function nextTag(): number;
2
+ export declare const FIRST_TAG: number;
package/build/tags.js CHANGED
@@ -2,9 +2,39 @@
2
2
  // for root tags). A tag is minted once when a node is first created and then
3
3
  // stays with it across clone-on-write commits (the clone keeps the family), so
4
4
  // this counter only ever moves forward: one tag per node, not per commit.
5
- let next = 2;
5
+ import { dlog } from './debug.js';
6
+ // WHY IT DOES NOT START AT 2. React's own Fabric renderer allocates tags the exact same way and
7
+ // from the exact same seed — `let nextReactTag = 2; ... nextReactTag += 2` in
8
+ // react-native-renderer/src/ReactFiberConfigFabric.js. We replace React as the APP's renderer, but
9
+ // React is still in the bundle and still drives one Fabric surface of its own: **LogBox**. So the
10
+ // first JS error in a symbiote app opens a redbox, React mints tag 2 for it, and Fabric aborts the
11
+ // process on a Create for a tag the engine registered at app start:
12
+ //
13
+ // *** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
14
+ // reason: 'RCTComponentViewRegistry: Attempt to dequeue already registered component.'
15
+ // -[RCTComponentViewRegistry dequeueComponentViewWithComponentHandle:tag:]
16
+ // RCTPerformMountInstructions(...)
17
+ //
18
+ // `RCTComponentViewRegistry` is process-wide, not per-surface, so the two allocators must not
19
+ // overlap. Diagnosed 2026-08-20 on examples/svelte: an `effect_update_depth_exceeded` on the API
20
+ // Playground screen turned into a SIGABRT with no JS stack, because the redbox that would have
21
+ // named the file killed the app instead of showing. Starting a whole surface's worth of tags above
22
+ // React's seed keeps the redbox a redbox: LogBox needs a few hundred nodes, so it can never walk
23
+ // 500k allocations up into this range.
24
+ const REACT_LOGBOX_TAG_RESERVE = 1_000_000;
25
+ let next = REACT_LOGBOX_TAG_RESERVE + 2;
26
+ let announced = false;
6
27
  export function nextTag() {
28
+ if (!announced) {
29
+ announced = true;
30
+ // Printed once, next to the first `commit ... createNode tag=` line: a device log that shows a
31
+ // Fabric tag below this base was minted by React (LogBox), not by us.
32
+ dlog(`tag allocator base=${next} (React's own Fabric tags stay below it)`);
33
+ }
7
34
  const tag = next;
8
35
  next += 2;
9
36
  return tag;
10
37
  }
38
+ // The lowest tag this allocator can ever hand out. Exported for the regression test that pins the
39
+ // no-overlap invariant against React's own allocator; nothing at runtime reads it.
40
+ export const FIRST_TAG = REACT_LOGBOX_TAG_RESERVE + 2;
@@ -21,7 +21,9 @@ export const touchHistory = {
21
21
  mostRecentTimeStamp: 0,
22
22
  };
23
23
  function toFiniteNumber(value) {
24
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
24
+ return typeof value === 'number' && Number.isFinite(value)
25
+ ? value
26
+ : undefined;
25
27
  }
26
28
  // Pull a recordable touch out of an untyped entry. RN's getTouchIdentifier throws on
27
29
  // a null id; we skip instead, so events without touch geometry leave the bank untouched.
@@ -35,7 +37,12 @@ function normalizeTouch(raw) {
35
37
  return undefined;
36
38
  if (identifier < 0 || identifier > MAX_TOUCH_BANK)
37
39
  return undefined;
38
- return { identifier, pageX, pageY, timestamp: toFiniteNumber(raw.timestamp) ?? 0 };
40
+ return {
41
+ identifier,
42
+ pageX,
43
+ pageY,
44
+ timestamp: toFiniteNumber(raw.timestamp) ?? 0,
45
+ };
39
46
  }
40
47
  // The changed touches for this frame (start/move/end), defensively read.
41
48
  function changedTouchesOf(nativeEvent) {
@@ -1,7 +1,6 @@
1
1
  // Vibration: Android build. Android has a native pattern scheduler, so the whole pattern
2
- // goes to native `vibrateByPattern(pattern, repeat)`: RN encodes "do not repeat" as -1
3
- // and "repeat from start" as 0. Everything else is the shared core. Metro picks this file
4
- // on an Android host.
2
+ // goes to native `vibrateByPattern(pattern, repeat)` (repeat encoding below). Everything
3
+ // else is the shared core. Metro picks this file on an Android host.
5
4
  //
6
5
  // device-verify-pending: the `Vibration` name and the `vibrateByPattern` routing are
7
6
  // confirmed from RN source but not yet exercised on a real Android host: only a
@@ -3,7 +3,7 @@
3
3
  // Everything else (the single-number buzz, cancel, the module resolver) is the shared
4
4
  // core. Metro picks this file on an iOS host; the base vibration.ts re-exports it for
5
5
  // web/headless.
6
- import { createVibration, DEFAULT_VIBRATION_LENGTH } from './shared.js';
6
+ import { createVibration, DEFAULT_VIBRATION_LENGTH, } from './shared.js';
7
7
  // JS scheduler state, mirroring RN's _vibrating / _id. `scheduleId` guards against a
8
8
  // stale timer from a previous run firing into a new one.
9
9
  let vibrating = false;
@@ -55,7 +55,12 @@ const TEXT_INPUT_EVENTS = [
55
55
  'selectionChange',
56
56
  'contentSizeChange',
57
57
  ];
58
- const MODAL_EVENTS = ['show', 'dismiss', 'requestClose', 'orientationChange'];
58
+ const MODAL_EVENTS = [
59
+ 'show',
60
+ 'dismiss',
61
+ 'requestClose',
62
+ 'orientationChange',
63
+ ];
59
64
  // A scroll view's events are the same on both axes and both platforms; only the native
60
65
  // NAME differs (iOS RCTScrollView for both; Android RCTScrollView vertical vs
61
66
  // AndroidHorizontalScrollView horizontal). Declared once, keyed under each name below. The
@@ -78,7 +83,14 @@ const TEXT_EVENTS = ['textLayout'];
78
83
  // SymbioteNode.component (what createNode is called with). A component absent here
79
84
  // still gets BASE_EVENTS, so a new primitive has working press/layout for free.
80
85
  const COMPONENT_EVENTS = {
81
- RCTImageView: ['loadStart', 'load', 'loadEnd', 'error', 'progress', 'partialLoad'],
86
+ RCTImageView: [
87
+ 'loadStart',
88
+ 'load',
89
+ 'loadEnd',
90
+ 'error',
91
+ 'progress',
92
+ 'partialLoad',
93
+ ],
82
94
  RCTScrollView: SCROLL_EVENTS,
83
95
  AndroidHorizontalScrollView: SCROLL_EVENTS,
84
96
  RCTSinglelineTextInputView: TEXT_INPUT_EVENTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/engine",
3
- "version": "0.1.7",
3
+ "version": "0.3.0",
4
4
  "description": "SymbioteNative's retained shadow-tree engine — clone-on-write commit path + event normalization over React Native Fabric, shared by every framework adapter.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "react-native": ">=0.86"
34
34
  },
35
35
  "devDependencies": {
36
- "@symbiote-native/test-utils": "0.1.6"
36
+ "@symbiote-native/test-utils": "0.2.0"
37
37
  },
38
38
  "scripts": {
39
39
  "typecheck": "tsc --build",
@@ -1,15 +0,0 @@
1
- import { AnimatedNode, AnimatedWithChildren } from './graph';
2
- import { type IInterpolationConfig } from './interpolation';
3
- import type { INativeNodeConfig, IPlatformConfig } from './native/native-animated';
4
- export declare class AnimatedInterpolation extends AnimatedWithChildren {
5
- private readonly parent;
6
- private readonly config;
7
- private interpolation;
8
- constructor(parent: AnimatedNode, config: IInterpolationConfig);
9
- private getInterpolation;
10
- __getValue(): number | string;
11
- __attach(): void;
12
- __detach(): void;
13
- __makeNative(platformConfig?: IPlatformConfig): void;
14
- __getNativeConfig(): INativeNodeConfig;
15
- }
@@ -1,58 +0,0 @@
1
- // AnimatedInterpolation: a graph node that maps its parent's numeric value
2
- // through an interpolation. Ported from RN's AnimatedInterpolation.js: numeric,
3
- // string-with-units, and color output ranges (the value graph; native config
4
- // removed). Platform (Native) colors stay out of scope, color.ts defers them.
5
- import { AnimatedNode, AnimatedWithChildren, registerInterpolationFactory } from './graph.js';
6
- import { checkValidRanges, createInterpolation } from './interpolation.js';
7
- export class AnimatedInterpolation extends AnimatedWithChildren {
8
- parent;
9
- config;
10
- interpolation;
11
- constructor(parent, config) {
12
- super();
13
- this.parent = parent;
14
- this.config = config;
15
- // Validate eagerly so a bad range fails at construction, not first frame.
16
- checkValidRanges(config.inputRange, config.outputRange);
17
- }
18
- getInterpolation() {
19
- if (this.interpolation === undefined) {
20
- this.interpolation = createInterpolation(this.config);
21
- }
22
- return this.interpolation;
23
- }
24
- __getValue() {
25
- const parentValue = this.parent.__getValue();
26
- if (typeof parentValue !== 'number') {
27
- throw new Error('Cannot interpolate an input which is not a number');
28
- }
29
- return this.getInterpolation()(parentValue);
30
- }
31
- __attach() {
32
- this.parent.__addChild(this);
33
- super.__attach();
34
- }
35
- __detach() {
36
- this.parent.__removeChild(this);
37
- super.__detach();
38
- }
39
- // Make the upstream value native first, so the parent->interpolation edge can be
40
- // wired when this node is reached from a leaf rather than from the value.
41
- __makeNative(platformConfig) {
42
- this.parent.__makeNative(platformConfig);
43
- super.__makeNative(platformConfig);
44
- }
45
- __getNativeConfig() {
46
- return {
47
- type: 'interpolation',
48
- inputRange: this.config.inputRange,
49
- outputRange: this.config.outputRange,
50
- extrapolateLeft: this.config.extrapolateLeft ?? this.config.extrapolate ?? 'extend',
51
- extrapolateRight: this.config.extrapolateRight ?? this.config.extrapolate ?? 'extend',
52
- };
53
- }
54
- }
55
- // Hand graph.ts's AnimatedNode.interpolate() the real constructor at module load,
56
- // so every node subclass's base-class interpolate() produces an actual
57
- // AnimatedInterpolation without graph.ts ever value-importing this module.
58
- registerInterpolationFactory((parent, config) => new AnimatedInterpolation(parent, config));
@@ -1,3 +0,0 @@
1
- export type IClassToggleMap = Record<string, boolean | undefined>;
2
- export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
3
- export declare function scopeClassName(value: IScopableClassValue, localNames: ReadonlySet<string>, scopeId: string): IScopableClassValue;
@@ -1,41 +0,0 @@
1
- // Vue `<style scoped>` class-name rewriter. Distinct responsibility from the sibling
2
- // ./index.ts (the CSS class -> style registry): this module does pure NAME rewriting,
3
- // no registry lookup, no CSS parsing. It runs at the compiled call site of a Vue SFC's
4
- // scoped-style template - `adapters/vue/metro-vue-transformer.cjs` emits calls to
5
- // scopeClassName (imported there as `__scopeClass`) - BEFORE Vue's own normalizeClass()
6
- // collapses string/object/array `class` values to a final string, so it must pre-process
7
- // all three shapes normalizeClass understands. resolveClassName in ./index.ts still does
8
- // the actual style lookup, unchanged, against the rewritten (possibly suffixed) name.
9
- import { kebabToCamel } from '../index.js';
10
- // Suffixes every class token that this file's scoped block locally defines with
11
- // `__${scopeId}`, leaving unrecognized tokens (globals, external classes) untouched.
12
- export function scopeClassName(value, localNames, scopeId) {
13
- if (value === undefined || value === null)
14
- return value;
15
- if (Array.isArray(value)) {
16
- return value.map(item => scopeClassEntry(item, localNames, scopeId));
17
- }
18
- return scopeClassEntry(value, localNames, scopeId);
19
- }
20
- // A token arrives as either the camelCase registry key (`sectionLabel`) or its kebab-case
21
- // authoring form (`section-label`) - normalize to camelCase FIRST, then decide scoping, so
22
- // `localNames` (always camelCase, built from the css-parser's registered keys) recognizes a
23
- // kebab-written token. The emitted (possibly suffixed) name is always the camelCase form.
24
- function scopeToken(token, localNames, scopeId) {
25
- const camelToken = kebabToCamel(token);
26
- return localNames.has(camelToken) ? `${camelToken}__${scopeId}` : camelToken;
27
- }
28
- function scopeClassEntry(value, localNames, scopeId) {
29
- if (typeof value === 'object') {
30
- const scoped = {};
31
- for (const [name, enabled] of Object.entries(value)) {
32
- scoped[scopeToken(name, localNames, scopeId)] = enabled;
33
- }
34
- return scoped;
35
- }
36
- return value
37
- .split(/\s+/)
38
- .filter(Boolean)
39
- .map(token => scopeToken(token, localNames, scopeId))
40
- .join(' ');
41
- }
@@ -1,23 +0,0 @@
1
- interface ITouchRecord {
2
- touchActive: boolean;
3
- startPageX: number;
4
- startPageY: number;
5
- startTimeStamp: number;
6
- currentPageX: number;
7
- currentPageY: number;
8
- currentTimeStamp: number;
9
- previousPageX: number;
10
- previousPageY: number;
11
- previousTimeStamp: number;
12
- }
13
- interface ITouchHistory {
14
- touchBank: ITouchRecord[];
15
- numberActiveTouches: number;
16
- indexOfSingleActiveTouch: number;
17
- mostRecentTimeStamp: number;
18
- }
19
- export declare const touchHistory: ITouchHistory;
20
- export declare function recordTouchTrack(kind: 'start' | 'move' | 'end', nativeEvent: Record<string, unknown>): void;
21
- export declare function resetTouchHistory(): void;
22
- export declare function attachTouchHistory(nativeEvent: Record<string, unknown>): void;
23
- export {};
@@ -1,150 +0,0 @@
1
- // Per-touch position/time tracking, ported from RN's
2
- // react-native-renderer/.../legacy-events/ResponderTouchHistoryStore.js. PanResponder's
3
- // multitouch dx/vx math needs each touch's own previous->current delta (RN counts only
4
- // touches that moved since `_accountsForMovesUpTo`), which a grant-relative centroid of
5
- // ALL live touches cannot reconstruct. We maintain the bank as touches flow and ATTACH
6
- // `touchHistory` onto the nativeEvent reaching responder handlers, exactly how
7
- // ResponderEventPlugin.js sets `*.touchHistory`.
8
- //
9
- // events/index.ts consumes only this file's public surface (recordTouchTrack,
10
- // attachTouchHistory, resetTouchHistory, touchHistory); everything else here is a
11
- // private implementation detail of the bank.
12
- import { isRecord } from '../type-guards.js';
13
- // RN's bank is indexed by touch identifier and warns above 20; we never warn (headless
14
- // events may carry larger or absent ids), we just skip anything out of a sane range.
15
- const MAX_TOUCH_BANK = 20;
16
- const touchBank = [];
17
- export const touchHistory = {
18
- touchBank,
19
- numberActiveTouches: 0,
20
- indexOfSingleActiveTouch: -1,
21
- mostRecentTimeStamp: 0,
22
- };
23
- function toFiniteNumber(value) {
24
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
25
- }
26
- // Pull a recordable touch out of an untyped entry. RN's getTouchIdentifier throws on
27
- // a null id; we skip instead, so events without touch geometry leave the bank untouched.
28
- function normalizeTouch(raw) {
29
- if (!isRecord(raw))
30
- return undefined;
31
- const identifier = toFiniteNumber(raw.identifier);
32
- const pageX = toFiniteNumber(raw.pageX);
33
- const pageY = toFiniteNumber(raw.pageY);
34
- if (identifier === undefined || pageX === undefined || pageY === undefined)
35
- return undefined;
36
- if (identifier < 0 || identifier > MAX_TOUCH_BANK)
37
- return undefined;
38
- return { identifier, pageX, pageY, timestamp: toFiniteNumber(raw.timestamp) ?? 0 };
39
- }
40
- // The changed touches for this frame (start/move/end), defensively read.
41
- function changedTouchesOf(nativeEvent) {
42
- const raw = nativeEvent.changedTouches;
43
- if (!Array.isArray(raw))
44
- return [];
45
- const out = [];
46
- for (const entry of raw) {
47
- const touch = normalizeTouch(entry);
48
- if (touch !== undefined)
49
- out.push(touch);
50
- }
51
- return out;
52
- }
53
- // Count of all touches still down (RN reads nativeEvent.touches.length directly).
54
- function activeTouchCount(nativeEvent) {
55
- const raw = nativeEvent.touches;
56
- return Array.isArray(raw) ? raw.length : 0;
57
- }
58
- function recordTouchStart(touch) {
59
- const record = touchBank[touch.identifier];
60
- if (record) {
61
- record.touchActive = true;
62
- record.startPageX = touch.pageX;
63
- record.startPageY = touch.pageY;
64
- record.startTimeStamp = touch.timestamp;
65
- record.currentPageX = touch.pageX;
66
- record.currentPageY = touch.pageY;
67
- record.currentTimeStamp = touch.timestamp;
68
- record.previousPageX = touch.pageX;
69
- record.previousPageY = touch.pageY;
70
- record.previousTimeStamp = touch.timestamp;
71
- }
72
- else {
73
- touchBank[touch.identifier] = {
74
- touchActive: true,
75
- startPageX: touch.pageX,
76
- startPageY: touch.pageY,
77
- startTimeStamp: touch.timestamp,
78
- currentPageX: touch.pageX,
79
- currentPageY: touch.pageY,
80
- currentTimeStamp: touch.timestamp,
81
- previousPageX: touch.pageX,
82
- previousPageY: touch.pageY,
83
- previousTimeStamp: touch.timestamp,
84
- };
85
- }
86
- touchHistory.mostRecentTimeStamp = touch.timestamp;
87
- }
88
- // Move and end share the previous<-current shift; only `touchActive` differs.
89
- function shiftTouchRecord(touch, active) {
90
- const record = touchBank[touch.identifier];
91
- if (!record)
92
- return;
93
- record.touchActive = active;
94
- record.previousPageX = record.currentPageX;
95
- record.previousPageY = record.currentPageY;
96
- record.previousTimeStamp = record.currentTimeStamp;
97
- record.currentPageX = touch.pageX;
98
- record.currentPageY = touch.pageY;
99
- record.currentTimeStamp = touch.timestamp;
100
- touchHistory.mostRecentTimeStamp = touch.timestamp;
101
- }
102
- function arrayFirst(value) {
103
- return Array.isArray(value) ? value[0] : undefined;
104
- }
105
- // Maintain the bank as a touch frame flows. Mirrors RN's recordTouchTrack: moveish
106
- // shifts records, startish records + recomputes numberActiveTouches, endish marks the
107
- // record inactive + rescans for the single remaining touch. `kind` is the touch phase.
108
- export function recordTouchTrack(kind, nativeEvent) {
109
- if (kind === 'move') {
110
- for (const touch of changedTouchesOf(nativeEvent))
111
- shiftTouchRecord(touch, true);
112
- return;
113
- }
114
- if (kind === 'start') {
115
- for (const touch of changedTouchesOf(nativeEvent))
116
- recordTouchStart(touch);
117
- touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
118
- if (touchHistory.numberActiveTouches === 1) {
119
- const first = normalizeTouch(arrayFirst(nativeEvent.touches));
120
- touchHistory.indexOfSingleActiveTouch = first?.identifier ?? -1;
121
- }
122
- return;
123
- }
124
- for (const touch of changedTouchesOf(nativeEvent))
125
- shiftTouchRecord(touch, false);
126
- touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
127
- if (touchHistory.numberActiveTouches === 1) {
128
- for (let i = 0; i < touchBank.length; i++) {
129
- const record = touchBank[i];
130
- if (record !== undefined && record.touchActive) {
131
- touchHistory.indexOfSingleActiveTouch = i;
132
- break;
133
- }
134
- }
135
- }
136
- }
137
- // Drop all touch state. Called on a fully-released / cancelled gesture so a stale bank
138
- // never leaks geometry into the next gesture's first frame.
139
- export function resetTouchHistory() {
140
- touchBank.length = 0;
141
- touchHistory.numberActiveTouches = 0;
142
- touchHistory.indexOfSingleActiveTouch = -1;
143
- touchHistory.mostRecentTimeStamp = 0;
144
- }
145
- // Attach the live touch history onto the event the responder handlers receive, matching
146
- // ResponderEventPlugin.js (`grantEvent.touchHistory = ...`, etc.). PanResponder reads
147
- // it for the per-touch dx/vx math; handlers that ignore it are unaffected.
148
- export function attachTouchHistory(nativeEvent) {
149
- nativeEvent.touchHistory = touchHistory;
150
- }
@@ -1,4 +0,0 @@
1
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
- export declare function isBoolean(value: unknown): value is boolean;
3
- export declare function isNumber(value: unknown): value is number;
4
- export declare function isString(value: unknown): value is string;
@@ -1,15 +0,0 @@
1
- // Runtime guards to narrow `unknown` at trust boundaries (native payloads, ViewConfig
2
- // attributes, style values) without an `as` cast. `isRecord` excludes arrays - most
3
- // call sites mean "a native payload keyed by string," never a list.
4
- export function isRecord(value) {
5
- return typeof value === 'object' && value !== null && !Array.isArray(value);
6
- }
7
- export function isBoolean(value) {
8
- return typeof value === 'boolean';
9
- }
10
- export function isNumber(value) {
11
- return typeof value === 'number';
12
- }
13
- export function isString(value) {
14
- return typeof value === 'string';
15
- }