@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.mjs CHANGED
@@ -117,7 +117,7 @@ var STANDARD_KEYS = {
117
117
  var KEYS = STANDARD_KEYS;
118
118
 
119
119
  // src/types/index.ts
120
- var TYPES = {
120
+ var CMP_TYPES = {
121
121
  // --- Zones ---
122
122
  /** Area responsible for capturing touches and spawning dynamic widgets */
123
123
  INPUT_ZONE: "input-zone",
@@ -252,14 +252,17 @@ var applyAxialDeadzone = (v, threshold, max) => {
252
252
  };
253
253
 
254
254
  // src/utils/dom.ts
255
- var getDeepElement = (x, y) => {
256
- let el = document.elementFromPoint(x, y);
257
- while (el && el.shadowRoot) {
258
- const nested = el.shadowRoot.elementFromPoint(x, y);
259
- if (!nested || nested === el) break;
260
- el = nested;
261
- }
262
- return el;
255
+ var getDeepElement = (x, y, ignoreClass = "omnipad-target-zone") => {
256
+ const elements = document.elementsFromPoint(x, y);
257
+ let target = elements.find((el) => !el.classList.contains(ignoreClass));
258
+ if (!target) return null;
259
+ while (target && target.shadowRoot) {
260
+ const nestedElements = target.shadowRoot.elementsFromPoint(x, y);
261
+ const nestedTarget = nestedElements.find((el) => !el.classList.contains(ignoreClass));
262
+ if (!nestedTarget || nestedTarget === target) break;
263
+ target = nestedTarget;
264
+ }
265
+ return target;
263
266
  };
264
267
  var getDeepActiveElement = () => {
265
268
  let el = document.activeElement;
@@ -303,7 +306,7 @@ var dispatchPointerEventAtPos = (type, x, y, opts = {}) => {
303
306
  target.dispatchEvent(
304
307
  new PointerEvent(type, {
305
308
  isPrimary: true,
306
- pointerId: 1,
309
+ pointerId: 9999,
307
310
  pointerType: "mouse",
308
311
  // Emulate mouse behavior for Flash MouseOver/Down logic
309
312
  ...commonProps
@@ -371,97 +374,6 @@ var resolveLayoutStyle = (layout) => {
371
374
  return style;
372
375
  };
373
376
 
374
- // src/utils/emitter.ts
375
- var SimpleEmitter = class {
376
- constructor() {
377
- __publicField(this, "listeners", /* @__PURE__ */ new Set());
378
- }
379
- /**
380
- * Registers a callback function to be executed whenever data is emitted.
381
- *
382
- * @param fn - The callback function.
383
- * @returns A function that, when called, unsubscribes the listener.
384
- */
385
- subscribe(fn) {
386
- this.listeners.add(fn);
387
- return () => this.listeners.delete(fn);
388
- }
389
- /**
390
- * Broadcasts the provided data to all registered listeners.
391
- * Each listener is executed within a try-catch block to ensure that
392
- * an error in one subscriber doesn't prevent others from receiving the signal.
393
- *
394
- * @param data - The payload to be sent to all subscribers.
395
- */
396
- emit(data) {
397
- this.listeners.forEach((fn) => {
398
- try {
399
- fn(data);
400
- } catch (error) {
401
- console.error("[OmniPad-Core] Emitter callback error:", error);
402
- }
403
- });
404
- }
405
- /**
406
- * Removes all listeners and clears the subscription set.
407
- * Essential for preventing memory leaks when an Entity is destroyed.
408
- */
409
- clear() {
410
- this.listeners.clear();
411
- }
412
- };
413
-
414
- // src/entities/BaseEntity.ts
415
- var BaseEntity = class {
416
- constructor(uid, type, initialConfig, initialState) {
417
- __publicField(this, "uid");
418
- __publicField(this, "type");
419
- __publicField(this, "config");
420
- __publicField(this, "state");
421
- __publicField(this, "rect", null);
422
- // 内部状态发射器,负责处理状态订阅逻辑 / Internal emitter for state subscription logic
423
- __publicField(this, "stateEmitter", new SimpleEmitter());
424
- this.uid = uid;
425
- this.type = type;
426
- this.config = initialConfig;
427
- this.state = initialState;
428
- }
429
- // --- IObservable Implementation ---
430
- subscribe(cb) {
431
- cb(this.state);
432
- return this.stateEmitter.subscribe(cb);
433
- }
434
- // --- State Management ---
435
- /**
436
- * Updates the internal state and notifies all subscribers.
437
- *
438
- * @param partialState - Partial object containing updated state values.
439
- */
440
- setState(partialState) {
441
- this.state = { ...this.state, ...partialState };
442
- this.stateEmitter.emit(this.state);
443
- }
444
- // --- Lifecycle ---
445
- destroy() {
446
- this.reset();
447
- this.stateEmitter.clear();
448
- Registry.getInstance().unregister(this.uid);
449
- }
450
- updateRect(rect) {
451
- this.rect = rect;
452
- }
453
- updateConfig(newConfig) {
454
- this.config = { ...this.config, ...newConfig };
455
- this.stateEmitter.emit(this.state);
456
- }
457
- getState() {
458
- return this.state;
459
- }
460
- getConfig() {
461
- return this.config;
462
- }
463
- };
464
-
465
377
  // src/registry/index.ts
466
378
  var GLOBAL_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("omnipad.registry.instance");
467
379
  var Registry = class _Registry {
@@ -516,8 +428,9 @@ var Registry = class _Registry {
516
428
  }
517
429
  const parentMap = /* @__PURE__ */ new Map();
518
430
  all.forEach((entity) => {
519
- if (entity instanceof BaseEntity) {
520
- const config = entity.getConfig();
431
+ const e = entity;
432
+ if (typeof e.getConfig === "function") {
433
+ const config = e.getConfig();
521
434
  if (config.parentId) {
522
435
  if (!parentMap.has(config.parentId)) {
523
436
  parentMap.set(config.parentId, []);
@@ -685,12 +598,72 @@ function exportProfile(meta, rootUid) {
685
598
  };
686
599
  }
687
600
 
601
+ // src/utils/emitter.ts
602
+ var SimpleEmitter = class {
603
+ constructor() {
604
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
605
+ }
606
+ /**
607
+ * Registers a callback function to be executed whenever data is emitted.
608
+ *
609
+ * @param fn - The callback function.
610
+ * @returns A function that, when called, unsubscribes the listener.
611
+ */
612
+ subscribe(fn) {
613
+ this.listeners.add(fn);
614
+ return () => this.listeners.delete(fn);
615
+ }
616
+ /**
617
+ * Broadcasts the provided data to all registered listeners.
618
+ * Each listener is executed within a try-catch block to ensure that
619
+ * an error in one subscriber doesn't prevent others from receiving the signal.
620
+ *
621
+ * @param data - The payload to be sent to all subscribers.
622
+ */
623
+ emit(data) {
624
+ this.listeners.forEach((fn) => {
625
+ try {
626
+ fn(data);
627
+ } catch (error) {
628
+ console.error("[OmniPad-Core] Emitter callback error:", error);
629
+ }
630
+ });
631
+ }
632
+ /**
633
+ * Removes all listeners and clears the subscription set.
634
+ * Essential for preventing memory leaks when an Entity is destroyed.
635
+ */
636
+ clear() {
637
+ this.listeners.clear();
638
+ }
639
+ };
640
+
641
+ // src/utils/performance.ts
642
+ function createRafThrottler(callback) {
643
+ let ticking = false;
644
+ let latestPayload = null;
645
+ return function(payload) {
646
+ latestPayload = payload;
647
+ if (!ticking) {
648
+ ticking = true;
649
+ window.requestAnimationFrame(() => {
650
+ if (latestPayload !== null) {
651
+ callback(latestPayload);
652
+ }
653
+ ticking = false;
654
+ });
655
+ }
656
+ };
657
+ }
658
+
688
659
  // src/imputManager/index.ts
689
660
  var INPUT_MANAGER_KEY = /* @__PURE__ */ Symbol.for("omnipad.input_manager.instance");
690
661
  var InputManager = class _InputManager {
691
662
  constructor() {
692
663
  /** Internal flag to prevent multiple event registrations */
693
664
  __publicField(this, "_isListening", false);
665
+ /** A throttled version of the reset logic */
666
+ __publicField(this, "throttledReset");
694
667
  /**
695
668
  * Manually triggers a system-wide input reset via Registry.
696
669
  */
@@ -700,6 +673,20 @@ var InputManager = class _InputManager {
700
673
  }
701
674
  Registry.getInstance().resetAll();
702
675
  });
676
+ __publicField(this, "handleResizeReset", () => {
677
+ this.throttledReset(null);
678
+ });
679
+ __publicField(this, "handleBlurReset", () => {
680
+ this.handleGlobalReset();
681
+ });
682
+ __publicField(this, "handleVisibilityChangeReset", () => {
683
+ if (document.visibilityState === "hidden") {
684
+ this.handleGlobalReset();
685
+ }
686
+ });
687
+ this.throttledReset = createRafThrottler(() => {
688
+ this.handleGlobalReset();
689
+ });
703
690
  }
704
691
  /**
705
692
  * Retrieves the global instance of the InputManager.
@@ -718,8 +705,9 @@ var InputManager = class _InputManager {
718
705
  */
719
706
  init() {
720
707
  if (this._isListening) return;
721
- window.addEventListener("resize", this.handleGlobalReset);
722
- window.addEventListener("blur", this.handleGlobalReset);
708
+ window.addEventListener("resize", this.handleResizeReset);
709
+ window.addEventListener("blur", this.handleBlurReset);
710
+ document.addEventListener("visibilitychange", this.handleVisibilityChangeReset);
723
711
  this._isListening = true;
724
712
  if (import.meta.env?.DEV) {
725
713
  console.log("[OmniPad-Core] Global InputManager monitoring started.");
@@ -753,12 +741,70 @@ var InputManager = class _InputManager {
753
741
  * Detaches all global listeners.
754
742
  */
755
743
  destroy() {
756
- window.removeEventListener("resize", this.handleGlobalReset);
757
- window.removeEventListener("blur", this.handleGlobalReset);
744
+ window.removeEventListener("resize", this.handleResizeReset);
745
+ window.removeEventListener("blur", this.handleBlurReset);
746
+ window.removeEventListener("visibilitychange", this.handleVisibilityChangeReset);
758
747
  this._isListening = false;
759
748
  }
760
749
  };
761
750
 
751
+ // src/entities/BaseEntity.ts
752
+ var BaseEntity = class {
753
+ constructor(uid, type, initialConfig, initialState) {
754
+ __publicField(this, "uid");
755
+ __publicField(this, "type");
756
+ __publicField(this, "config");
757
+ __publicField(this, "state");
758
+ __publicField(this, "rectProvider", null);
759
+ // 内部状态发射器,负责处理状态订阅逻辑 / Internal emitter for state subscription logic
760
+ __publicField(this, "stateEmitter", new SimpleEmitter());
761
+ this.uid = uid;
762
+ this.type = type;
763
+ this.config = initialConfig;
764
+ this.state = initialState;
765
+ }
766
+ // --- IObservable Implementation ---
767
+ subscribe(cb) {
768
+ cb(this.state);
769
+ return this.stateEmitter.subscribe(cb);
770
+ }
771
+ // --- State Management ---
772
+ /**
773
+ * Updates the internal state and notifies all subscribers.
774
+ *
775
+ * @param partialState - Partial object containing updated state values.
776
+ */
777
+ setState(partialState) {
778
+ this.state = { ...this.state, ...partialState };
779
+ this.stateEmitter.emit(this.state);
780
+ }
781
+ // --- Lifecycle ---
782
+ destroy() {
783
+ this.reset();
784
+ this.stateEmitter.clear();
785
+ Registry.getInstance().unregister(this.uid);
786
+ }
787
+ bindRectProvider(provider) {
788
+ this.rectProvider = provider;
789
+ }
790
+ /**
791
+ * Called when triggering interactions, this subclass fetches the latest boundaries in real time.
792
+ */
793
+ getRect() {
794
+ return this.rectProvider ? this.rectProvider() : null;
795
+ }
796
+ updateConfig(newConfig) {
797
+ this.config = { ...this.config, ...newConfig };
798
+ this.stateEmitter.emit(this.state);
799
+ }
800
+ getState() {
801
+ return this.state;
802
+ }
803
+ getConfig() {
804
+ return this.config;
805
+ }
806
+ };
807
+
762
808
  // src/entities/InputZoneCore.ts
763
809
  var INITIAL_STATE = {
764
810
  isDynamicActive: false,
@@ -767,12 +813,13 @@ var INITIAL_STATE = {
767
813
  };
768
814
  var InputZoneCore = class extends BaseEntity {
769
815
  constructor(uid, config) {
770
- super(uid, TYPES.INPUT_ZONE, config, INITIAL_STATE);
816
+ super(uid, CMP_TYPES.INPUT_ZONE, config, INITIAL_STATE);
771
817
  }
772
818
  onPointerDown(e) {
773
819
  if (this.state.isDynamicActive) return;
774
820
  if (e.target !== e.currentTarget) return;
775
821
  if (e.cancelable) e.preventDefault();
822
+ e.stopPropagation();
776
823
  const pos = this.calculateRelativePosition(e.clientX, e.clientY);
777
824
  this.setState({
778
825
  isDynamicActive: true,
@@ -784,6 +831,7 @@ var InputZoneCore = class extends BaseEntity {
784
831
  if (!this.state.isDynamicActive || e.pointerId !== this.state.dynamicPointerId) return;
785
832
  }
786
833
  onPointerUp(e) {
834
+ if (e.cancelable) e.preventDefault();
787
835
  this.handleRelease(e);
788
836
  }
789
837
  onPointerCancel(e) {
@@ -809,10 +857,11 @@ var InputZoneCore = class extends BaseEntity {
809
857
  * Converts viewport pixels to percentage coordinates relative to the zone.
810
858
  */
811
859
  calculateRelativePosition(clientX, clientY) {
812
- if (!this.rect) return { x: 0, y: 0 };
860
+ const rect = this.getRect();
861
+ if (!rect) return { x: 0, y: 0 };
813
862
  return {
814
- x: pxToPercent(clientX - this.rect.left, this.rect.width),
815
- y: pxToPercent(clientY - this.rect.top, this.rect.height)
863
+ x: pxToPercent(clientX - rect.left, rect.width),
864
+ y: pxToPercent(clientY - rect.top, rect.height)
816
865
  };
817
866
  }
818
867
  /**
@@ -844,11 +893,12 @@ var KeyboardButtonCore = class extends BaseEntity {
844
893
  * @param config - The flat configuration object for the button.
845
894
  */
846
895
  constructor(uid, config) {
847
- super(uid, TYPES.KEYBOARD_BUTTON, config, INITIAL_STATE2);
896
+ super(uid, CMP_TYPES.KEYBOARD_BUTTON, config, INITIAL_STATE2);
848
897
  }
849
898
  // --- IPointerHandler Implementation ---
850
899
  onPointerDown(e) {
851
900
  if (e.cancelable) e.preventDefault();
901
+ e.stopPropagation();
852
902
  e.target.setPointerCapture(e.pointerId);
853
903
  this.setState({
854
904
  isActive: true,
@@ -903,7 +953,7 @@ var KeyboardButtonCore = class extends BaseEntity {
903
953
  target.handleSignal(signal);
904
954
  } else {
905
955
  if (import.meta.env?.DEV) {
906
- console.warn(`[OmniPad-Core] Button ${this.uid} target not found: ${targetId}`);
956
+ console.warn(`[OmniPad-Core] KeyboardButton ${this.uid} target not found: ${targetId}`);
907
957
  }
908
958
  }
909
959
  }
@@ -916,20 +966,108 @@ var KeyboardButtonCore = class extends BaseEntity {
916
966
  }
917
967
  };
918
968
 
919
- // src/entities/RootLayerCore.ts
969
+ // src/entities/MouseButtonCore.ts
920
970
  var INITIAL_STATE3 = {
971
+ isActive: false,
972
+ isPressed: false,
973
+ pointerId: null,
974
+ value: 0
975
+ };
976
+ var MouseButtonCore = class extends BaseEntity {
977
+ constructor(uid, config) {
978
+ super(uid, CMP_TYPES.MOUSE_BUTTON, config, INITIAL_STATE3);
979
+ }
980
+ // --- IPointerHandler Implementation ---
981
+ onPointerDown(e) {
982
+ if (e.cancelable) e.preventDefault();
983
+ e.stopPropagation();
984
+ e.target.setPointerCapture(e.pointerId);
985
+ this.setState({
986
+ isActive: true,
987
+ isPressed: true,
988
+ pointerId: e.pointerId
989
+ });
990
+ this.sendInputSignal(ACTION_TYPES.MOUSEDOWN);
991
+ }
992
+ onPointerUp(e) {
993
+ if (e.cancelable) e.preventDefault();
994
+ this.handleRelease(e, true);
995
+ }
996
+ onPointerCancel(e) {
997
+ this.handleRelease(e, false);
998
+ }
999
+ onPointerMove(e) {
1000
+ if (e.cancelable) e.preventDefault();
1001
+ }
1002
+ // --- Internal Logic ---
1003
+ /**
1004
+ * Handles the release of the button.
1005
+ *
1006
+ * @param e - The pointer event.
1007
+ * @param isNormalRelease - If true, a 'click' event will also be dispatched.
1008
+ */
1009
+ handleRelease(e, isNormalRelease) {
1010
+ const isCancelEvent = e.type === "pointercancel" || e.type === "lostpointercapture";
1011
+ if (!isCancelEvent && this.state.pointerId !== e.pointerId) return;
1012
+ if (e.target.hasPointerCapture(e.pointerId)) {
1013
+ e.target.releasePointerCapture(e.pointerId);
1014
+ }
1015
+ this.setState(INITIAL_STATE3);
1016
+ this.sendInputSignal(ACTION_TYPES.MOUSEUP);
1017
+ if (isNormalRelease) {
1018
+ this.sendInputSignal(ACTION_TYPES.CLICK);
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Dispatches input signals to the registered target stage.
1023
+ *
1024
+ * @param type - The action type (mousedown, mouseup, or click).
1025
+ */
1026
+ sendInputSignal(type) {
1027
+ const targetId = this.config.targetStageId;
1028
+ if (!targetId) return;
1029
+ const target = Registry.getInstance().getEntity(targetId);
1030
+ if (target && typeof target.handleSignal === "function") {
1031
+ const signal = {
1032
+ targetStageId: targetId,
1033
+ type,
1034
+ payload: {
1035
+ // 传递配置中的鼠标按键索引 (0:左键, 1:中键, 2:右键)
1036
+ button: this.config.button ?? 0,
1037
+ // 如果配置了固定坐标,一并传递给 TargetZone
1038
+ point: this.config.fixedPoint
1039
+ }
1040
+ };
1041
+ target.handleSignal(signal);
1042
+ } else {
1043
+ if (import.meta.env?.DEV) {
1044
+ console.warn(`[OmniPad-Core] MouseButton ${this.uid} target not found: ${targetId}`);
1045
+ }
1046
+ }
1047
+ }
1048
+ // --- IResettable Implementation ---
1049
+ reset() {
1050
+ if (this.state.isPressed) {
1051
+ this.sendInputSignal(ACTION_TYPES.MOUSEUP);
1052
+ }
1053
+ this.setState(INITIAL_STATE3);
1054
+ }
1055
+ };
1056
+
1057
+ // src/entities/RootLayerCore.ts
1058
+ var INITIAL_STATE4 = {
921
1059
  isHighlighted: false
922
1060
  };
923
1061
  var RootLayerCore = class extends BaseEntity {
924
1062
  constructor(uid, config) {
925
- super(uid, TYPES.ROOT_LAYER, config, INITIAL_STATE3);
1063
+ super(uid, CMP_TYPES.ROOT_LAYER, config, INITIAL_STATE4);
926
1064
  }
927
1065
  reset() {
928
1066
  }
929
1067
  };
930
1068
 
931
1069
  // src/entities/TargetZoneCore.ts
932
- var INITIAL_STATE4 = {
1070
+ var INITIAL_STATE5 = {
933
1071
  position: { x: 50, y: 50 },
934
1072
  isVisible: false,
935
1073
  isPointerDown: false,
@@ -937,9 +1075,48 @@ var INITIAL_STATE4 = {
937
1075
  };
938
1076
  var TargetZoneCore = class extends BaseEntity {
939
1077
  constructor(uid, config) {
940
- super(uid, TYPES.TARGET_ZONE, config, INITIAL_STATE4);
1078
+ super(uid, CMP_TYPES.TARGET_ZONE, config, INITIAL_STATE5);
941
1079
  __publicField(this, "hideTimer", null);
942
1080
  __publicField(this, "focusFeedbackTimer", null);
1081
+ __publicField(this, "throttledPointerMove");
1082
+ this.throttledPointerMove = createRafThrottler((e) => {
1083
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEMOVE);
1084
+ });
1085
+ }
1086
+ // --- IPointerHandler Implementation ---
1087
+ onPointerDown(e) {
1088
+ if (e.cancelable) e.preventDefault();
1089
+ e.stopPropagation();
1090
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEDOWN);
1091
+ }
1092
+ onPointerMove(e) {
1093
+ if (e.cancelable) e.preventDefault();
1094
+ e.stopPropagation();
1095
+ this.throttledPointerMove(e);
1096
+ }
1097
+ onPointerUp(e) {
1098
+ if (e.cancelable) e.preventDefault();
1099
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEUP);
1100
+ this.processPhysicalEvent(e, ACTION_TYPES.CLICK);
1101
+ }
1102
+ onPointerCancel(e) {
1103
+ this.processPhysicalEvent(e, ACTION_TYPES.MOUSEUP);
1104
+ }
1105
+ /**
1106
+ * Convert physical DOM events into internal signals
1107
+ */
1108
+ processPhysicalEvent(e, type) {
1109
+ const rect = this.getRect();
1110
+ if (!rect) return;
1111
+ const point = {
1112
+ x: pxToPercent(e.clientX - rect.left, rect.width),
1113
+ y: pxToPercent(e.clientY - rect.top, rect.height)
1114
+ };
1115
+ this.handleSignal({
1116
+ targetStageId: this.uid,
1117
+ type,
1118
+ payload: { point, button: e.button }
1119
+ });
943
1120
  }
944
1121
  // --- ISignalReceiver Implementation ---
945
1122
  handleSignal(signal) {
@@ -953,13 +1130,18 @@ var TargetZoneCore = class extends BaseEntity {
953
1130
  case ACTION_TYPES.MOUSEMOVE:
954
1131
  if (payload.point) {
955
1132
  this.updateCursorPosition(payload.point);
956
- if (this.config.cursorEnabled) this.showCursor();
957
- this.executeMouseAction(ACTION_TYPES.POINTERMOVE, payload);
1133
+ } else if (payload.delta) {
1134
+ this.updateCursorPositionByDelta(payload.delta);
958
1135
  }
1136
+ if (this.config.cursorEnabled) this.showCursor();
1137
+ this.executeMouseAction(ACTION_TYPES.POINTERMOVE, payload);
959
1138
  break;
960
1139
  case ACTION_TYPES.MOUSEDOWN:
961
1140
  case ACTION_TYPES.MOUSEUP:
962
1141
  case ACTION_TYPES.CLICK:
1142
+ if (payload.point) {
1143
+ this.updateCursorPosition(payload.point);
1144
+ }
963
1145
  if (this.config.cursorEnabled) this.showCursor();
964
1146
  this.executeMouseAction(
965
1147
  type.startsWith(ACTION_TYPES.MOUSE) ? type.replace(ACTION_TYPES.MOUSE, ACTION_TYPES.POINTER) : type,
@@ -976,12 +1158,13 @@ var TargetZoneCore = class extends BaseEntity {
976
1158
  * @param payload - Data containing point coordinates or button info.
977
1159
  */
978
1160
  executeMouseAction(pointerType, payload) {
979
- if (!this.rect) return;
1161
+ const rect = this.getRect();
1162
+ if (!rect) return;
980
1163
  if (pointerType === ACTION_TYPES.POINTERDOWN) this.setState({ isPointerDown: true });
981
1164
  if (pointerType === ACTION_TYPES.POINTERUP) this.setState({ isPointerDown: false });
982
1165
  const target = payload.point || this.state.position;
983
- const px = this.rect.left + percentToPx(target.x, this.rect.width);
984
- const py = this.rect.top + percentToPx(target.y, this.rect.height);
1166
+ const px = rect.left + percentToPx(target.x, rect.width);
1167
+ const py = rect.top + percentToPx(target.y, rect.height);
985
1168
  dispatchPointerEventAtPos(pointerType, px, py, {
986
1169
  button: payload.button ?? 0,
987
1170
  buttons: this.state.isPointerDown ? 1 : 0
@@ -992,9 +1175,10 @@ var TargetZoneCore = class extends BaseEntity {
992
1175
  * Checks if the target element under the virtual cursor has focus, and reclaims it if lost.
993
1176
  */
994
1177
  ensureFocus() {
995
- if (!this.rect) return;
996
- const px = this.rect.left + percentToPx(this.state.position.x, this.rect.width);
997
- const py = this.rect.top + percentToPx(this.state.position.y, this.rect.height);
1178
+ const rect = this.getRect();
1179
+ if (!rect) return;
1180
+ const px = rect.left + percentToPx(this.state.position.x, rect.width);
1181
+ const py = rect.top + percentToPx(this.state.position.y, rect.height);
998
1182
  const target = getDeepElement(px, py);
999
1183
  if (!target) return;
1000
1184
  if (getDeepActiveElement() !== target) {
@@ -1015,8 +1199,23 @@ var TargetZoneCore = class extends BaseEntity {
1015
1199
  * Updates the internal virtual cursor coordinates.
1016
1200
  */
1017
1201
  updateCursorPosition(point) {
1202
+ if (isVec2Equal(point, this.state.position)) return;
1018
1203
  this.setState({ position: { ...point } });
1019
1204
  }
1205
+ /**
1206
+ * Updates the internal virtual cursor coordinates by delta.
1207
+ */
1208
+ updateCursorPositionByDelta(delta) {
1209
+ if (isVec2Equal(delta, { x: 0, y: 0 })) return;
1210
+ const rect = this.getRect();
1211
+ if (!rect) return;
1212
+ const dxPercent = pxToPercent(delta.x, rect.width);
1213
+ const dyPercent = pxToPercent(delta.y, rect.height);
1214
+ this.updateCursorPosition({
1215
+ x: clamp(this.state.position.x + dxPercent, 0, 100),
1216
+ y: clamp(this.state.position.y + dyPercent, 0, 100)
1217
+ });
1218
+ }
1020
1219
  /**
1021
1220
  * Makes the virtual cursor visible and sets a timeout for auto-hiding.
1022
1221
  */
@@ -1045,26 +1244,164 @@ var TargetZoneCore = class extends BaseEntity {
1045
1244
  }
1046
1245
  };
1047
1246
 
1247
+ // src/entities/TrackpadCore.ts
1248
+ var GESTURE = {
1249
+ TAP_TIME: 200,
1250
+ // 200ms 以内抬起视为轻点 / Release within 200ms counts as a tap
1251
+ TAP_DISTANCE: 10,
1252
+ // 位移小于 10px 视为点击判定 / Movement within 10px counts as a tap
1253
+ DOUBLE_TAP_GAP: 300
1254
+ // 两次点击间隔小于 300ms 视为双击触发 / Interval within 300ms counts as double-tap
1255
+ };
1256
+ var INITIAL_STATE6 = {
1257
+ isActive: false,
1258
+ isPressed: false,
1259
+ pointerId: null,
1260
+ value: 0
1261
+ };
1262
+ var TrackpadCore = class extends BaseEntity {
1263
+ /**
1264
+ * Creates an instance of TrackpadCore.
1265
+ *
1266
+ * @param uid - Unique entity ID.
1267
+ * @param config - Configuration for the trackpad.
1268
+ */
1269
+ constructor(uid, config) {
1270
+ super(uid, CMP_TYPES.TRACKPAD, config, INITIAL_STATE6);
1271
+ __publicField(this, "lastPointerPos", { x: 0, y: 0 });
1272
+ __publicField(this, "startTime", 0);
1273
+ __publicField(this, "startPos", { x: 0, y: 0 });
1274
+ // 连击状态追踪 / State tracking for consecutive taps
1275
+ __publicField(this, "lastClickTime", 0);
1276
+ __publicField(this, "isDragMode", false);
1277
+ __publicField(this, "throttledPointerMove");
1278
+ this.throttledPointerMove = createRafThrottler((e) => {
1279
+ this.processPointerMove(e);
1280
+ });
1281
+ }
1282
+ // --- IPointerHandler Implementation ---
1283
+ onPointerDown(e) {
1284
+ if (e.cancelable) e.preventDefault();
1285
+ e.stopPropagation();
1286
+ e.target.setPointerCapture(e.pointerId);
1287
+ const now = Date.now();
1288
+ this.startTime = now;
1289
+ this.startPos = { x: e.clientX, y: e.clientY };
1290
+ this.lastPointerPos = { x: e.clientX, y: e.clientY };
1291
+ if (now - this.lastClickTime < GESTURE.DOUBLE_TAP_GAP) {
1292
+ this.isDragMode = true;
1293
+ this.sendSignal(ACTION_TYPES.MOUSEDOWN);
1294
+ this.setState({ isPressed: true });
1295
+ }
1296
+ this.setState({ isActive: true, pointerId: e.pointerId });
1297
+ }
1298
+ onPointerMove(e) {
1299
+ if (e.cancelable) e.preventDefault();
1300
+ e.stopPropagation();
1301
+ if (this.state.pointerId !== e.pointerId) return;
1302
+ this.throttledPointerMove(e);
1303
+ }
1304
+ /**
1305
+ * Internal logic for processing pointer movement.
1306
+ * Calculates displacement and emits relative move signals.
1307
+ *
1308
+ * @param e - The pointer event.
1309
+ */
1310
+ processPointerMove(e) {
1311
+ const dx = e.clientX - this.lastPointerPos.x;
1312
+ const dy = e.clientY - this.lastPointerPos.y;
1313
+ const rect = this.getRect();
1314
+ if (!rect) return;
1315
+ const deltaX = dx / rect.width * 100 * this.config.sensitivity;
1316
+ const deltaY = dy / rect.height * 100 * this.config.sensitivity;
1317
+ if (Math.abs(dx) > 0 || Math.abs(dy) > 0) {
1318
+ this.sendSignal(ACTION_TYPES.MOUSEMOVE, { delta: { x: deltaX, y: deltaY } });
1319
+ }
1320
+ this.lastPointerPos = { x: e.clientX, y: e.clientY };
1321
+ }
1322
+ onPointerUp(e) {
1323
+ if (this.state.pointerId !== e.pointerId) return;
1324
+ if (e.cancelable) e.preventDefault();
1325
+ const duration = Date.now() - this.startTime;
1326
+ const dist = Math.hypot(e.clientX - this.startPos.x, e.clientY - this.startPos.y);
1327
+ if (this.isDragMode) {
1328
+ this.sendSignal(ACTION_TYPES.MOUSEUP);
1329
+ this.isDragMode = false;
1330
+ } else if (duration < GESTURE.TAP_TIME && dist < GESTURE.TAP_DISTANCE) {
1331
+ this.sendSignal(ACTION_TYPES.CLICK);
1332
+ this.lastClickTime = Date.now();
1333
+ }
1334
+ this.handleRelease(e);
1335
+ }
1336
+ onPointerCancel(e) {
1337
+ this.handleRelease(e);
1338
+ }
1339
+ // --- IResettable Implementation ---
1340
+ reset() {
1341
+ if (this.isDragMode) this.sendSignal(ACTION_TYPES.MOUSEUP);
1342
+ this.isDragMode = false;
1343
+ this.setState(INITIAL_STATE6);
1344
+ }
1345
+ // --- Internal Helpers ---
1346
+ /**
1347
+ * Clean up pointer capture and reset interaction state.
1348
+ */
1349
+ handleRelease(e) {
1350
+ if (e.target.hasPointerCapture(e.pointerId)) {
1351
+ try {
1352
+ e.target.releasePointerCapture(e.pointerId);
1353
+ } catch (err) {
1354
+ }
1355
+ }
1356
+ this.setState(INITIAL_STATE6);
1357
+ }
1358
+ /**
1359
+ * Helper to send signals to the target stage via Registry.
1360
+ *
1361
+ * @param type - Signal action type.
1362
+ * @param extraPayload - Additional data like delta or point.
1363
+ */
1364
+ sendSignal(type, extraPayload = {}) {
1365
+ const targetId = this.config.targetStageId;
1366
+ if (!targetId) return;
1367
+ const target = Registry.getInstance().getEntity(targetId);
1368
+ if (target && typeof target.handleSignal === "function") {
1369
+ target.handleSignal({
1370
+ targetStageId: targetId,
1371
+ type,
1372
+ payload: {
1373
+ button: 0,
1374
+ // 触摸板操作默认模拟左键 / Trackpad defaults to left button
1375
+ ...extraPayload
1376
+ }
1377
+ });
1378
+ }
1379
+ }
1380
+ };
1381
+
1048
1382
  // src/index.ts
1049
1383
  var OmniPad = {
1384
+ ActionTypes: ACTION_TYPES,
1050
1385
  Context: CONTEXT,
1051
1386
  Keys: KEYS,
1052
- Types: TYPES
1387
+ Types: CMP_TYPES
1053
1388
  };
1054
1389
  export {
1055
1390
  ACTION_TYPES,
1056
1391
  BaseEntity,
1392
+ CMP_TYPES,
1057
1393
  CONTEXT,
1058
1394
  InputManager,
1059
1395
  InputZoneCore,
1060
1396
  KEYS,
1061
1397
  KeyboardButtonCore,
1398
+ MouseButtonCore,
1062
1399
  OmniPad,
1063
1400
  Registry,
1064
1401
  RootLayerCore,
1065
1402
  SimpleEmitter,
1066
- TYPES,
1067
1403
  TargetZoneCore,
1404
+ TrackpadCore,
1068
1405
  addVec,
1069
1406
  applyAxialDeadzone,
1070
1407
  applyRadialDeadzone,