@omnipad/core 0.1.1-alpha.0 → 0.2.0-alpha.2

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.
package/dist/index.js CHANGED
@@ -24,17 +24,19 @@ var index_exports = {};
24
24
  __export(index_exports, {
25
25
  ACTION_TYPES: () => ACTION_TYPES,
26
26
  BaseEntity: () => BaseEntity,
27
+ CMP_TYPES: () => CMP_TYPES,
27
28
  CONTEXT: () => CONTEXT,
28
29
  InputManager: () => InputManager,
29
30
  InputZoneCore: () => InputZoneCore,
30
31
  KEYS: () => KEYS,
31
32
  KeyboardButtonCore: () => KeyboardButtonCore,
33
+ MouseButtonCore: () => MouseButtonCore,
32
34
  OmniPad: () => OmniPad,
33
35
  Registry: () => Registry,
34
36
  RootLayerCore: () => RootLayerCore,
35
37
  SimpleEmitter: () => SimpleEmitter,
36
- TYPES: () => TYPES,
37
38
  TargetZoneCore: () => TargetZoneCore,
39
+ TrackpadCore: () => TrackpadCore,
38
40
  addVec: () => addVec,
39
41
  applyAxialDeadzone: () => applyAxialDeadzone,
40
42
  applyRadialDeadzone: () => applyRadialDeadzone,
@@ -187,7 +189,7 @@ var STANDARD_KEYS = {
187
189
  var KEYS = STANDARD_KEYS;
188
190
 
189
191
  // src/types/index.ts
190
- var TYPES = {
192
+ var CMP_TYPES = {
191
193
  // --- Zones ---
192
194
  /** Area responsible for capturing touches and spawning dynamic widgets */
193
195
  INPUT_ZONE: "input-zone",
@@ -322,14 +324,17 @@ var applyAxialDeadzone = (v, threshold, max) => {
322
324
  };
323
325
 
324
326
  // src/utils/dom.ts
325
- var getDeepElement = (x, y) => {
326
- let el = document.elementFromPoint(x, y);
327
- while (el && el.shadowRoot) {
328
- const nested = el.shadowRoot.elementFromPoint(x, y);
329
- if (!nested || nested === el) break;
330
- el = nested;
331
- }
332
- return el;
327
+ var getDeepElement = (x, y, ignoreClass = "omnipad-target-zone") => {
328
+ const elements = document.elementsFromPoint(x, y);
329
+ let target = elements.find((el) => !el.classList.contains(ignoreClass));
330
+ if (!target) return null;
331
+ while (target && target.shadowRoot) {
332
+ const nestedElements = target.shadowRoot.elementsFromPoint(x, y);
333
+ const nestedTarget = nestedElements.find((el) => !el.classList.contains(ignoreClass));
334
+ if (!nestedTarget || nestedTarget === target) break;
335
+ target = nestedTarget;
336
+ }
337
+ return target;
333
338
  };
334
339
  var getDeepActiveElement = () => {
335
340
  let el = document.activeElement;
@@ -373,7 +378,7 @@ var dispatchPointerEventAtPos = (type, x, y, opts = {}) => {
373
378
  target.dispatchEvent(
374
379
  new PointerEvent(type, {
375
380
  isPrimary: true,
376
- pointerId: 1,
381
+ pointerId: 9999,
377
382
  pointerType: "mouse",
378
383
  // Emulate mouse behavior for Flash MouseOver/Down logic
379
384
  ...commonProps
@@ -441,97 +446,6 @@ var resolveLayoutStyle = (layout) => {
441
446
  return style;
442
447
  };
443
448
 
444
- // src/utils/emitter.ts
445
- var SimpleEmitter = class {
446
- constructor() {
447
- __publicField(this, "listeners", /* @__PURE__ */ new Set());
448
- }
449
- /**
450
- * Registers a callback function to be executed whenever data is emitted.
451
- *
452
- * @param fn - The callback function.
453
- * @returns A function that, when called, unsubscribes the listener.
454
- */
455
- subscribe(fn) {
456
- this.listeners.add(fn);
457
- return () => this.listeners.delete(fn);
458
- }
459
- /**
460
- * Broadcasts the provided data to all registered listeners.
461
- * Each listener is executed within a try-catch block to ensure that
462
- * an error in one subscriber doesn't prevent others from receiving the signal.
463
- *
464
- * @param data - The payload to be sent to all subscribers.
465
- */
466
- emit(data) {
467
- this.listeners.forEach((fn) => {
468
- try {
469
- fn(data);
470
- } catch (error) {
471
- console.error("[OmniPad-Core] Emitter callback error:", error);
472
- }
473
- });
474
- }
475
- /**
476
- * Removes all listeners and clears the subscription set.
477
- * Essential for preventing memory leaks when an Entity is destroyed.
478
- */
479
- clear() {
480
- this.listeners.clear();
481
- }
482
- };
483
-
484
- // src/entities/BaseEntity.ts
485
- var BaseEntity = class {
486
- constructor(uid, type, initialConfig, initialState) {
487
- __publicField(this, "uid");
488
- __publicField(this, "type");
489
- __publicField(this, "config");
490
- __publicField(this, "state");
491
- __publicField(this, "rect", null);
492
- // 内部状态发射器,负责处理状态订阅逻辑 / Internal emitter for state subscription logic
493
- __publicField(this, "stateEmitter", new SimpleEmitter());
494
- this.uid = uid;
495
- this.type = type;
496
- this.config = initialConfig;
497
- this.state = initialState;
498
- }
499
- // --- IObservable Implementation ---
500
- subscribe(cb) {
501
- cb(this.state);
502
- return this.stateEmitter.subscribe(cb);
503
- }
504
- // --- State Management ---
505
- /**
506
- * Updates the internal state and notifies all subscribers.
507
- *
508
- * @param partialState - Partial object containing updated state values.
509
- */
510
- setState(partialState) {
511
- this.state = { ...this.state, ...partialState };
512
- this.stateEmitter.emit(this.state);
513
- }
514
- // --- Lifecycle ---
515
- destroy() {
516
- this.reset();
517
- this.stateEmitter.clear();
518
- Registry.getInstance().unregister(this.uid);
519
- }
520
- updateRect(rect) {
521
- this.rect = rect;
522
- }
523
- updateConfig(newConfig) {
524
- this.config = { ...this.config, ...newConfig };
525
- this.stateEmitter.emit(this.state);
526
- }
527
- getState() {
528
- return this.state;
529
- }
530
- getConfig() {
531
- return this.config;
532
- }
533
- };
534
-
535
449
  // src/registry/index.ts
536
450
  var import_meta = {};
537
451
  var GLOBAL_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("omnipad.registry.instance");
@@ -587,8 +501,9 @@ var Registry = class _Registry {
587
501
  }
588
502
  const parentMap = /* @__PURE__ */ new Map();
589
503
  all.forEach((entity) => {
590
- if (entity instanceof BaseEntity) {
591
- const config = entity.getConfig();
504
+ const e = entity;
505
+ if (typeof e.getConfig === "function") {
506
+ const config = e.getConfig();
592
507
  if (config.parentId) {
593
508
  if (!parentMap.has(config.parentId)) {
594
509
  parentMap.set(config.parentId, []);
@@ -756,6 +671,64 @@ function exportProfile(meta, rootUid) {
756
671
  };
757
672
  }
758
673
 
674
+ // src/utils/emitter.ts
675
+ var SimpleEmitter = class {
676
+ constructor() {
677
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
678
+ }
679
+ /**
680
+ * Registers a callback function to be executed whenever data is emitted.
681
+ *
682
+ * @param fn - The callback function.
683
+ * @returns A function that, when called, unsubscribes the listener.
684
+ */
685
+ subscribe(fn) {
686
+ this.listeners.add(fn);
687
+ return () => this.listeners.delete(fn);
688
+ }
689
+ /**
690
+ * Broadcasts the provided data to all registered listeners.
691
+ * Each listener is executed within a try-catch block to ensure that
692
+ * an error in one subscriber doesn't prevent others from receiving the signal.
693
+ *
694
+ * @param data - The payload to be sent to all subscribers.
695
+ */
696
+ emit(data) {
697
+ this.listeners.forEach((fn) => {
698
+ try {
699
+ fn(data);
700
+ } catch (error) {
701
+ console.error("[OmniPad-Core] Emitter callback error:", error);
702
+ }
703
+ });
704
+ }
705
+ /**
706
+ * Removes all listeners and clears the subscription set.
707
+ * Essential for preventing memory leaks when an Entity is destroyed.
708
+ */
709
+ clear() {
710
+ this.listeners.clear();
711
+ }
712
+ };
713
+
714
+ // src/utils/performance.ts
715
+ function createRafThrottler(callback) {
716
+ let ticking = false;
717
+ let latestPayload = null;
718
+ return function(payload) {
719
+ latestPayload = payload;
720
+ if (!ticking) {
721
+ ticking = true;
722
+ window.requestAnimationFrame(() => {
723
+ if (latestPayload !== null) {
724
+ callback(latestPayload);
725
+ }
726
+ ticking = false;
727
+ });
728
+ }
729
+ };
730
+ }
731
+
759
732
  // src/imputManager/index.ts
760
733
  var import_meta2 = {};
761
734
  var INPUT_MANAGER_KEY = /* @__PURE__ */ Symbol.for("omnipad.input_manager.instance");
@@ -763,6 +736,8 @@ var InputManager = class _InputManager {
763
736
  constructor() {
764
737
  /** Internal flag to prevent multiple event registrations */
765
738
  __publicField(this, "_isListening", false);
739
+ /** A throttled version of the reset logic */
740
+ __publicField(this, "throttledReset");
766
741
  /**
767
742
  * Manually triggers a system-wide input reset via Registry.
768
743
  */
@@ -772,6 +747,20 @@ var InputManager = class _InputManager {
772
747
  }
773
748
  Registry.getInstance().resetAll();
774
749
  });
750
+ __publicField(this, "handleResizeReset", () => {
751
+ this.throttledReset(null);
752
+ });
753
+ __publicField(this, "handleBlurReset", () => {
754
+ this.handleGlobalReset();
755
+ });
756
+ __publicField(this, "handleVisibilityChangeReset", () => {
757
+ if (document.visibilityState === "hidden") {
758
+ this.handleGlobalReset();
759
+ }
760
+ });
761
+ this.throttledReset = createRafThrottler(() => {
762
+ this.handleGlobalReset();
763
+ });
775
764
  }
776
765
  /**
777
766
  * Retrieves the global instance of the InputManager.
@@ -790,8 +779,9 @@ var InputManager = class _InputManager {
790
779
  */
791
780
  init() {
792
781
  if (this._isListening) return;
793
- window.addEventListener("resize", this.handleGlobalReset);
794
- window.addEventListener("blur", this.handleGlobalReset);
782
+ window.addEventListener("resize", this.handleResizeReset);
783
+ window.addEventListener("blur", this.handleBlurReset);
784
+ document.addEventListener("visibilitychange", this.handleVisibilityChangeReset);
795
785
  this._isListening = true;
796
786
  if (import_meta2.env?.DEV) {
797
787
  console.log("[OmniPad-Core] Global InputManager monitoring started.");
@@ -825,12 +815,70 @@ var InputManager = class _InputManager {
825
815
  * Detaches all global listeners.
826
816
  */
827
817
  destroy() {
828
- window.removeEventListener("resize", this.handleGlobalReset);
829
- window.removeEventListener("blur", this.handleGlobalReset);
818
+ window.removeEventListener("resize", this.handleResizeReset);
819
+ window.removeEventListener("blur", this.handleBlurReset);
820
+ window.removeEventListener("visibilitychange", this.handleVisibilityChangeReset);
830
821
  this._isListening = false;
831
822
  }
832
823
  };
833
824
 
825
+ // src/entities/BaseEntity.ts
826
+ var BaseEntity = class {
827
+ constructor(uid, type, initialConfig, initialState) {
828
+ __publicField(this, "uid");
829
+ __publicField(this, "type");
830
+ __publicField(this, "config");
831
+ __publicField(this, "state");
832
+ __publicField(this, "rectProvider", null);
833
+ // 内部状态发射器,负责处理状态订阅逻辑 / Internal emitter for state subscription logic
834
+ __publicField(this, "stateEmitter", new SimpleEmitter());
835
+ this.uid = uid;
836
+ this.type = type;
837
+ this.config = initialConfig;
838
+ this.state = initialState;
839
+ }
840
+ // --- IObservable Implementation ---
841
+ subscribe(cb) {
842
+ cb(this.state);
843
+ return this.stateEmitter.subscribe(cb);
844
+ }
845
+ // --- State Management ---
846
+ /**
847
+ * Updates the internal state and notifies all subscribers.
848
+ *
849
+ * @param partialState - Partial object containing updated state values.
850
+ */
851
+ setState(partialState) {
852
+ this.state = { ...this.state, ...partialState };
853
+ this.stateEmitter.emit(this.state);
854
+ }
855
+ // --- Lifecycle ---
856
+ destroy() {
857
+ this.reset();
858
+ this.stateEmitter.clear();
859
+ Registry.getInstance().unregister(this.uid);
860
+ }
861
+ bindRectProvider(provider) {
862
+ this.rectProvider = provider;
863
+ }
864
+ /**
865
+ * Called when triggering interactions, this subclass fetches the latest boundaries in real time.
866
+ */
867
+ getRect() {
868
+ return this.rectProvider ? this.rectProvider() : null;
869
+ }
870
+ updateConfig(newConfig) {
871
+ this.config = { ...this.config, ...newConfig };
872
+ this.stateEmitter.emit(this.state);
873
+ }
874
+ getState() {
875
+ return this.state;
876
+ }
877
+ getConfig() {
878
+ return this.config;
879
+ }
880
+ };
881
+
834
882
  // src/entities/InputZoneCore.ts
835
883
  var INITIAL_STATE = {
836
884
  isDynamicActive: false,
@@ -839,12 +887,13 @@ var INITIAL_STATE = {
839
887
  };
840
888
  var InputZoneCore = class extends BaseEntity {
841
889
  constructor(uid, config) {
842
- super(uid, TYPES.INPUT_ZONE, config, INITIAL_STATE);
890
+ super(uid, CMP_TYPES.INPUT_ZONE, config, INITIAL_STATE);
843
891
  }
844
892
  onPointerDown(e) {
845
893
  if (this.state.isDynamicActive) return;
846
894
  if (e.target !== e.currentTarget) return;
847
895
  if (e.cancelable) e.preventDefault();
896
+ e.stopPropagation();
848
897
  const pos = this.calculateRelativePosition(e.clientX, e.clientY);
849
898
  this.setState({
850
899
  isDynamicActive: true,
@@ -856,6 +905,7 @@ var InputZoneCore = class extends BaseEntity {
856
905
  if (!this.state.isDynamicActive || e.pointerId !== this.state.dynamicPointerId) return;
857
906
  }
858
907
  onPointerUp(e) {
908
+ if (e.cancelable) e.preventDefault();
859
909
  this.handleRelease(e);
860
910
  }
861
911
  onPointerCancel(e) {
@@ -881,10 +931,11 @@ var InputZoneCore = class extends BaseEntity {
881
931
  * Converts viewport pixels to percentage coordinates relative to the zone.
882
932
  */
883
933
  calculateRelativePosition(clientX, clientY) {
884
- if (!this.rect) return { x: 0, y: 0 };
934
+ const rect = this.getRect();
935
+ if (!rect) return { x: 0, y: 0 };
885
936
  return {
886
- x: pxToPercent(clientX - this.rect.left, this.rect.width),
887
- y: pxToPercent(clientY - this.rect.top, this.rect.height)
937
+ x: pxToPercent(clientX - rect.left, rect.width),
938
+ y: pxToPercent(clientY - rect.top, rect.height)
888
939
  };
889
940
  }
890
941
  /**
@@ -917,11 +968,12 @@ var KeyboardButtonCore = class extends BaseEntity {
917
968
  * @param config - The flat configuration object for the button.
918
969
  */
919
970
  constructor(uid, config) {
920
- super(uid, TYPES.KEYBOARD_BUTTON, config, INITIAL_STATE2);
971
+ super(uid, CMP_TYPES.KEYBOARD_BUTTON, config, INITIAL_STATE2);
921
972
  }
922
973
  // --- IPointerHandler Implementation ---
923
974
  onPointerDown(e) {
924
975
  if (e.cancelable) e.preventDefault();
976
+ e.stopPropagation();
925
977
  e.target.setPointerCapture(e.pointerId);
926
978
  this.setState({
927
979
  isActive: true,
@@ -976,7 +1028,7 @@ var KeyboardButtonCore = class extends BaseEntity {
976
1028
  target.handleSignal(signal);
977
1029
  } else {
978
1030
  if (import_meta3.env?.DEV) {
979
- console.warn(`[OmniPad-Core] Button ${this.uid} target not found: ${targetId}`);
1031
+ console.warn(`[OmniPad-Core] KeyboardButton ${this.uid} target not found: ${targetId}`);
980
1032
  }
981
1033
  }
982
1034
  }
@@ -989,20 +1041,109 @@ var KeyboardButtonCore = class extends BaseEntity {
989
1041
  }
990
1042
  };
991
1043
 
992
- // src/entities/RootLayerCore.ts
1044
+ // src/entities/MouseButtonCore.ts
1045
+ var import_meta4 = {};
993
1046
  var INITIAL_STATE3 = {
1047
+ isActive: false,
1048
+ isPressed: false,
1049
+ pointerId: null,
1050
+ value: 0
1051
+ };
1052
+ var MouseButtonCore = class extends BaseEntity {
1053
+ constructor(uid, config) {
1054
+ super(uid, CMP_TYPES.MOUSE_BUTTON, config, INITIAL_STATE3);
1055
+ }
1056
+ // --- IPointerHandler Implementation ---
1057
+ onPointerDown(e) {
1058
+ if (e.cancelable) e.preventDefault();
1059
+ e.stopPropagation();
1060
+ e.target.setPointerCapture(e.pointerId);
1061
+ this.setState({
1062
+ isActive: true,
1063
+ isPressed: true,
1064
+ pointerId: e.pointerId
1065
+ });
1066
+ this.sendInputSignal(ACTION_TYPES.MOUSEDOWN);
1067
+ }
1068
+ onPointerUp(e) {
1069
+ if (e.cancelable) e.preventDefault();
1070
+ this.handleRelease(e, true);
1071
+ }
1072
+ onPointerCancel(e) {
1073
+ this.handleRelease(e, false);
1074
+ }
1075
+ onPointerMove(e) {
1076
+ if (e.cancelable) e.preventDefault();
1077
+ }
1078
+ // --- Internal Logic ---
1079
+ /**
1080
+ * Handles the release of the button.
1081
+ *
1082
+ * @param e - The pointer event.
1083
+ * @param isNormalRelease - If true, a 'click' event will also be dispatched.
1084
+ */
1085
+ handleRelease(e, isNormalRelease) {
1086
+ const isCancelEvent = e.type === "pointercancel" || e.type === "lostpointercapture";
1087
+ if (!isCancelEvent && this.state.pointerId !== e.pointerId) return;
1088
+ if (e.target.hasPointerCapture(e.pointerId)) {
1089
+ e.target.releasePointerCapture(e.pointerId);
1090
+ }
1091
+ this.setState(INITIAL_STATE3);
1092
+ this.sendInputSignal(ACTION_TYPES.MOUSEUP);
1093
+ if (isNormalRelease) {
1094
+ this.sendInputSignal(ACTION_TYPES.CLICK);
1095
+ }
1096
+ }
1097
+ /**
1098
+ * Dispatches input signals to the registered target stage.
1099
+ *
1100
+ * @param type - The action type (mousedown, mouseup, or click).
1101
+ */
1102
+ sendInputSignal(type) {
1103
+ const targetId = this.config.targetStageId;
1104
+ if (!targetId) return;
1105
+ const target = Registry.getInstance().getEntity(targetId);
1106
+ if (target && typeof target.handleSignal === "function") {
1107
+ const signal = {
1108
+ targetStageId: targetId,
1109
+ type,
1110
+ payload: {
1111
+ // 传递配置中的鼠标按键索引 (0:左键, 1:中键, 2:右键)
1112
+ button: this.config.button ?? 0,
1113
+ // 如果配置了固定坐标,一并传递给 TargetZone
1114
+ point: this.config.fixedPoint
1115
+ }
1116
+ };
1117
+ target.handleSignal(signal);
1118
+ } else {
1119
+ if (import_meta4.env?.DEV) {
1120
+ console.warn(`[OmniPad-Core] MouseButton ${this.uid} target not found: ${targetId}`);
1121
+ }
1122
+ }
1123
+ }
1124
+ // --- IResettable Implementation ---
1125
+ reset() {
1126
+ if (this.state.isPressed) {
1127
+ this.sendInputSignal(ACTION_TYPES.MOUSEUP);
1128
+ }
1129
+ this.setState(INITIAL_STATE3);
1130
+ }
1131
+ };
1132
+
1133
+ // src/entities/RootLayerCore.ts
1134
+ var INITIAL_STATE4 = {
994
1135
  isHighlighted: false
995
1136
  };
996
1137
  var RootLayerCore = class extends BaseEntity {
997
1138
  constructor(uid, config) {
998
- super(uid, TYPES.ROOT_LAYER, config, INITIAL_STATE3);
1139
+ super(uid, CMP_TYPES.ROOT_LAYER, config, INITIAL_STATE4);
999
1140
  }
1000
1141
  reset() {
1001
1142
  }
1002
1143
  };
1003
1144
 
1004
1145
  // src/entities/TargetZoneCore.ts
1005
- var INITIAL_STATE4 = {
1146
+ var INITIAL_STATE5 = {
1006
1147
  position: { x: 50, y: 50 },
1007
1148
  isVisible: false,
1008
1149
  isPointerDown: false,
@@ -1010,9 +1151,48 @@ var INITIAL_STATE4 = {
1010
1151
  };
1011
1152
  var TargetZoneCore = class extends BaseEntity {
1012
1153
  constructor(uid, config) {
1013
- super(uid, TYPES.TARGET_ZONE, config, INITIAL_STATE4);
1154
+ super(uid, CMP_TYPES.TARGET_ZONE, config, INITIAL_STATE5);
1014
1155
  __publicField(this, "hideTimer", null);
1015
1156
  __publicField(this, "focusFeedbackTimer", null);
1157
+ __publicField(this, "throttledPointerMove");
1158
+ this.throttledPointerMove = createRafThrottler((e) => {
1159
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEMOVE);
1160
+ });
1161
+ }
1162
+ // --- IPointerHandler Implementation ---
1163
+ onPointerDown(e) {
1164
+ if (e.cancelable) e.preventDefault();
1165
+ e.stopPropagation();
1166
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEDOWN);
1167
+ }
1168
+ onPointerMove(e) {
1169
+ if (e.cancelable) e.preventDefault();
1170
+ e.stopPropagation();
1171
+ this.throttledPointerMove(e);
1172
+ }
1173
+ onPointerUp(e) {
1174
+ if (e.cancelable) e.preventDefault();
1175
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEUP);
1176
+ this.processPhysicalEvent(e, ACTION_TYPES.CLICK);
1177
+ }
1178
+ onPointerCancel(e) {
1179
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEUP);
1180
+ }
1181
+ /**
1182
+ * Convert physical DOM events into internal signals
1183
+ */
1184
+ processPhysicalEvent(e, type) {
1185
+ const rect = this.getRect();
1186
+ if (!rect) return;
1187
+ const point = {
1188
+ x: pxToPercent(e.clientX - rect.left, rect.width),
1189
+ y: pxToPercent(e.clientY - rect.top, rect.height)
1190
+ };
1191
+ this.handleSignal({
1192
+ targetStageId: this.uid,
1193
+ type,
1194
+ payload: { point, button: e.button }
1195
+ });
1016
1196
  }
1017
1197
  // --- ISignalReceiver Implementation ---
1018
1198
  handleSignal(signal) {
@@ -1026,13 +1206,18 @@ var TargetZoneCore = class extends BaseEntity {
1026
1206
  case ACTION_TYPES.MOUSEMOVE:
1027
1207
  if (payload.point) {
1028
1208
  this.updateCursorPosition(payload.point);
1029
- if (this.config.cursorEnabled) this.showCursor();
1030
- this.executeMouseAction(ACTION_TYPES.POINTERMOVE, payload);
1209
+ } else if (payload.delta) {
1210
+ this.updateCursorPositionByDelta(payload.delta);
1031
1211
  }
1212
+ if (this.config.cursorEnabled) this.showCursor();
1213
+ this.executeMouseAction(ACTION_TYPES.POINTERMOVE, payload);
1032
1214
  break;
1033
1215
  case ACTION_TYPES.MOUSEDOWN:
1034
1216
  case ACTION_TYPES.MOUSEUP:
1035
1217
  case ACTION_TYPES.CLICK:
1218
+ if (payload.point) {
1219
+ this.updateCursorPosition(payload.point);
1220
+ }
1036
1221
  if (this.config.cursorEnabled) this.showCursor();
1037
1222
  this.executeMouseAction(
1038
1223
  type.startsWith(ACTION_TYPES.MOUSE) ? type.replace(ACTION_TYPES.MOUSE, ACTION_TYPES.POINTER) : type,
@@ -1049,12 +1234,13 @@ var TargetZoneCore = class extends BaseEntity {
1049
1234
  * @param payload - Data containing point coordinates or button info.
1050
1235
  */
1051
1236
  executeMouseAction(pointerType, payload) {
1052
- if (!this.rect) return;
1237
+ const rect = this.getRect();
1238
+ if (!rect) return;
1053
1239
  if (pointerType === ACTION_TYPES.POINTERDOWN) this.setState({ isPointerDown: true });
1054
1240
  if (pointerType === ACTION_TYPES.POINTERUP) this.setState({ isPointerDown: false });
1055
1241
  const target = payload.point || this.state.position;
1056
- const px = this.rect.left + percentToPx(target.x, this.rect.width);
1057
- const py = this.rect.top + percentToPx(target.y, this.rect.height);
1242
+ const px = rect.left + percentToPx(target.x, rect.width);
1243
+ const py = rect.top + percentToPx(target.y, rect.height);
1058
1244
  dispatchPointerEventAtPos(pointerType, px, py, {
1059
1245
  button: payload.button ?? 0,
1060
1246
  buttons: this.state.isPointerDown ? 1 : 0
@@ -1065,9 +1251,10 @@ var TargetZoneCore = class extends BaseEntity {
1065
1251
  * Checks if the target element under the virtual cursor has focus, and reclaims it if lost.
1066
1252
  */
1067
1253
  ensureFocus() {
1068
- if (!this.rect) return;
1069
- const px = this.rect.left + percentToPx(this.state.position.x, this.rect.width);
1070
- const py = this.rect.top + percentToPx(this.state.position.y, this.rect.height);
1254
+ const rect = this.getRect();
1255
+ if (!rect) return;
1256
+ const px = rect.left + percentToPx(this.state.position.x, rect.width);
1257
+ const py = rect.top + percentToPx(this.state.position.y, rect.height);
1071
1258
  const target = getDeepElement(px, py);
1072
1259
  if (!target) return;
1073
1260
  if (getDeepActiveElement() !== target) {
@@ -1088,8 +1275,23 @@ var TargetZoneCore = class extends BaseEntity {
1088
1275
  * Updates the internal virtual cursor coordinates.
1089
1276
  */
1090
1277
  updateCursorPosition(point) {
1278
+ if (isVec2Equal(point, this.state.position)) return;
1091
1279
  this.setState({ position: { ...point } });
1092
1280
  }
1281
+ /**
1282
+ * Updates the internal virtual cursor coordinates by delta.
1283
+ */
1284
+ updateCursorPositionByDelta(delta) {
1285
+ if (isVec2Equal(delta, { x: 0, y: 0 })) return;
1286
+ const rect = this.getRect();
1287
+ if (!rect) return;
1288
+ const dxPercent = pxToPercent(delta.x, rect.width);
1289
+ const dyPercent = pxToPercent(delta.y, rect.height);
1290
+ this.updateCursorPosition({
1291
+ x: clamp(this.state.position.x + dxPercent, 0, 100),
1292
+ y: clamp(this.state.position.y + dyPercent, 0, 100)
1293
+ });
1294
+ }
1093
1295
  /**
1094
1296
  * Makes the virtual cursor visible and sets a timeout for auto-hiding.
1095
1297
  */
@@ -1118,27 +1320,165 @@ var TargetZoneCore = class extends BaseEntity {
1118
1320
  }
1119
1321
  };
1120
1322
 
1323
+ // src/entities/TrackpadCore.ts
1324
+ var GESTURE = {
1325
+ TAP_TIME: 200,
1326
+ // 200ms 以内抬起视为轻点 / Release within 200ms counts as a tap
1327
+ TAP_DISTANCE: 10,
1328
+ // 位移小于 10px 视为点击判定 / Movement within 10px counts as a tap
1329
+ DOUBLE_TAP_GAP: 300
1330
+ // 两次点击间隔小于 300ms 视为双击触发 / Interval within 300ms counts as double-tap
1331
+ };
1332
+ var INITIAL_STATE6 = {
1333
+ isActive: false,
1334
+ isPressed: false,
1335
+ pointerId: null,
1336
+ value: 0
1337
+ };
1338
+ var TrackpadCore = class extends BaseEntity {
1339
+ /**
1340
+ * Creates an instance of TrackpadCore.
1341
+ *
1342
+ * @param uid - Unique entity ID.
1343
+ * @param config - Configuration for the trackpad.
1344
+ */
1345
+ constructor(uid, config) {
1346
+ super(uid, CMP_TYPES.TRACKPAD, config, INITIAL_STATE6);
1347
+ __publicField(this, "lastPointerPos", { x: 0, y: 0 });
1348
+ __publicField(this, "startTime", 0);
1349
+ __publicField(this, "startPos", { x: 0, y: 0 });
1350
+ // 连击状态追踪 / State tracking for consecutive taps
1351
+ __publicField(this, "lastClickTime", 0);
1352
+ __publicField(this, "isDragMode", false);
1353
+ __publicField(this, "throttledPointerMove");
1354
+ this.throttledPointerMove = createRafThrottler((e) => {
1355
+ this.processPointerMove(e);
1356
+ });
1357
+ }
1358
+ // --- IPointerHandler Implementation ---
1359
+ onPointerDown(e) {
1360
+ if (e.cancelable) e.preventDefault();
1361
+ e.stopPropagation();
1362
+ e.target.setPointerCapture(e.pointerId);
1363
+ const now = Date.now();
1364
+ this.startTime = now;
1365
+ this.startPos = { x: e.clientX, y: e.clientY };
1366
+ this.lastPointerPos = { x: e.clientX, y: e.clientY };
1367
+ if (now - this.lastClickTime < GESTURE.DOUBLE_TAP_GAP) {
1368
+ this.isDragMode = true;
1369
+ this.sendSignal(ACTION_TYPES.MOUSEDOWN);
1370
+ this.setState({ isPressed: true });
1371
+ }
1372
+ this.setState({ isActive: true, pointerId: e.pointerId });
1373
+ }
1374
+ onPointerMove(e) {
1375
+ if (e.cancelable) e.preventDefault();
1376
+ e.stopPropagation();
1377
+ if (this.state.pointerId !== e.pointerId) return;
1378
+ this.throttledPointerMove(e);
1379
+ }
1380
+ /**
1381
+ * Internal logic for processing pointer movement.
1382
+ * Calculates displacement and emits relative move signals.
1383
+ *
1384
+ * @param e - The pointer event.
1385
+ */
1386
+ processPointerMove(e) {
1387
+ const dx = e.clientX - this.lastPointerPos.x;
1388
+ const dy = e.clientY - this.lastPointerPos.y;
1389
+ const rect = this.getRect();
1390
+ if (!rect) return;
1391
+ const deltaX = dx / rect.width * 100 * this.config.sensitivity;
1392
+ const deltaY = dy / rect.height * 100 * this.config.sensitivity;
1393
+ if (Math.abs(dx) > 0 || Math.abs(dy) > 0) {
1394
+ this.sendSignal(ACTION_TYPES.MOUSEMOVE, { delta: { x: deltaX, y: deltaY } });
1395
+ }
1396
+ this.lastPointerPos = { x: e.clientX, y: e.clientY };
1397
+ }
1398
+ onPointerUp(e) {
1399
+ if (this.state.pointerId !== e.pointerId) return;
1400
+ if (e.cancelable) e.preventDefault();
1401
+ const duration = Date.now() - this.startTime;
1402
+ const dist = Math.hypot(e.clientX - this.startPos.x, e.clientY - this.startPos.y);
1403
+ if (this.isDragMode) {
1404
+ this.sendSignal(ACTION_TYPES.MOUSEUP);
1405
+ this.isDragMode = false;
1406
+ } else if (duration < GESTURE.TAP_TIME && dist < GESTURE.TAP_DISTANCE) {
1407
+ this.sendSignal(ACTION_TYPES.CLICK);
1408
+ this.lastClickTime = Date.now();
1409
+ }
1410
+ this.handleRelease(e);
1411
+ }
1412
+ onPointerCancel(e) {
1413
+ this.handleRelease(e);
1414
+ }
1415
+ // --- IResettable Implementation ---
1416
+ reset() {
1417
+ if (this.isDragMode) this.sendSignal(ACTION_TYPES.MOUSEUP);
1418
+ this.isDragMode = false;
1419
+ this.setState(INITIAL_STATE6);
1420
+ }
1421
+ // --- Internal Helpers ---
1422
+ /**
1423
+ * Clean up pointer capture and reset interaction state.
1424
+ */
1425
+ handleRelease(e) {
1426
+ if (e.target.hasPointerCapture(e.pointerId)) {
1427
+ try {
1428
+ e.target.releasePointerCapture(e.pointerId);
1429
+ } catch (err) {
1430
+ }
1431
+ }
1432
+ this.setState(INITIAL_STATE6);
1433
+ }
1434
+ /**
1435
+ * Helper to send signals to the target stage via Registry.
1436
+ *
1437
+ * @param type - Signal action type.
1438
+ * @param extraPayload - Additional data like delta or point.
1439
+ */
1440
+ sendSignal(type, extraPayload = {}) {
1441
+ const targetId = this.config.targetStageId;
1442
+ if (!targetId) return;
1443
+ const target = Registry.getInstance().getEntity(targetId);
1444
+ if (target && typeof target.handleSignal === "function") {
1445
+ target.handleSignal({
1446
+ targetStageId: targetId,
1447
+ type,
1448
+ payload: {
1449
+ button: 0,
1450
+ // 触摸板操作默认模拟左键 / Trackpad defaults to left button
1451
+ ...extraPayload
1452
+ }
1453
+ });
1454
+ }
1455
+ }
1456
+ };
1457
+
1121
1458
  // src/index.ts
1122
1459
  var OmniPad = {
1460
+ ActionTypes: ACTION_TYPES,
1123
1461
  Context: CONTEXT,
1124
1462
  Keys: KEYS,
1125
- Types: TYPES
1463
+ Types: CMP_TYPES
1126
1464
  };
1127
1465
  // Annotate the CommonJS export names for ESM import in node:
1128
1466
  0 && (module.exports = {
1129
1467
  ACTION_TYPES,
1130
1468
  BaseEntity,
1469
+ CMP_TYPES,
1131
1470
  CONTEXT,
1132
1471
  InputManager,
1133
1472
  InputZoneCore,
1134
1473
  KEYS,
1135
1474
  KeyboardButtonCore,
1475
+ MouseButtonCore,
1136
1476
  OmniPad,
1137
1477
  Registry,
1138
1478
  RootLayerCore,
1139
1479
  SimpleEmitter,
1140
- TYPES,
1141
1480
  TargetZoneCore,
1481
+ TrackpadCore,
1142
1482
  addVec,
1143
1483
  applyAxialDeadzone,
1144
1484
  applyRadialDeadzone,