@baron1996/klinecharts-adapter 0.9.4 → 0.9.5

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/adapter.js CHANGED
@@ -12,8 +12,10 @@ import { toOverlayStyles } from './conversion/overlays.js';
12
12
  import { drawingToSceneOverlay, sceneOverlayToDrawing, } from './drawing/overlay-conversion.js';
13
13
  import { MainSeriesPresentationError, presentationToSceneCandle, STANDARD_CLOSE_LINE_PRESENTATION, } from './main-series-presentation.js';
14
14
  import { createDragCandidate, } from './interaction/dragging.js';
15
- import { hitTestOverlayGeometries, } from './interaction/hit-testing.js';
15
+ import { DEFAULT_OVERLAY_MOUSE_HIT_TOLERANCE, DEFAULT_OVERLAY_TOUCH_HIT_TOLERANCE, hitTestOverlayGeometries, } from './interaction/hit-testing.js';
16
+ import { projectOverlayGeometry } from './interaction/overlay-geometry.js';
16
17
  import { shouldIgnoreStaleOverlayDeselection } from './interaction/selection-arbitration.js';
18
+ import { isTouchPrecisionTap, resolveTouchPrecisionCursor, TouchPrecisionDrawingGuide, } from './interaction/touch-precision-drawing.js';
17
19
  function promoteSceneToM2(scene, scale) {
18
20
  const candidate = structuredClone(scene);
19
21
  candidate.runtime.runtimeVersion = '0.2.0';
@@ -41,12 +43,19 @@ export class KLineChartsSceneAdapter {
41
43
  #engine;
42
44
  /** 引擎点击仲裁显式复位;每次绘制开始前调用,保证新绘制首击独立成单击。 */
43
45
  #resetClickArbitration;
46
+ /** 向引擎派发鼠标移动语义,供触摸虚拟光标复用原生绘制预览。 */
47
+ #dispatchEngineMouseMove;
48
+ /** 向引擎派发鼠标单击语义,供触摸轻点复用原生绘制落点。 */
49
+ #dispatchEngineMouseClick;
44
50
  /** 引擎十字线动作监听器集合。 */
45
51
  #crosshairListeners = new Set();
46
52
  /** 引擎十字线动作退订函数。 */
47
53
  #unsubscribeCrosshair = null;
48
54
  /** 引擎十字线动作的绑定处理函数。 */
49
55
  #handleCrosshairChange = (payload) => {
56
+ if (this.#chartInteractionSnapshot !== undefined) {
57
+ return;
58
+ }
50
59
  const event = (typeof payload === 'object' && payload !== null
51
60
  ? payload
52
61
  : {});
@@ -102,12 +111,26 @@ export class KLineChartsSceneAdapter {
102
111
  #listeners = new Set();
103
112
  /** 当前选择状态,null 表示明确未选择。 */
104
113
  #selectedOverlayId = null;
114
+ /** 独占选择前的图表交互状态,用于无损恢复宿主原配置。 */
115
+ #chartInteractionSnapshot;
116
+ /** 点击空白退出编辑时被完整消费的 Pointer ID。 */
117
+ #deselectingPointerId;
105
118
  /** 当前受控拖动事务;progress 永不写入 #scene。 */
106
119
  #pointerInteraction;
107
120
  /** 当前交互式量度的首锚点;只用于补偿引擎丢弃快速第二击,不进入 Scene。 */
108
121
  #interactivePriceMeasurement;
109
122
  /** 当前引擎进行中的绘制 Overlay ID;非 null 时 pointerdown 只路由给新绘制,不做命中测试。 */
110
123
  #drawingInProgressId = null;
124
+ /** 宿主显式开启的 Drawing 输入策略;未配置时保持引擎原生行为。 */
125
+ #drawingInteraction;
126
+ /** 触摸精确绘制的通用提示与虚拟光标层。 */
127
+ #touchPrecisionGuide;
128
+ /** 当前允许切换到触摸精确交互的线段绘制。 */
129
+ #touchPrecisionDrawing;
130
+ /** 当前单指定位手势;拖动只更新光标,轻点才确认。 */
131
+ #touchPrecisionPointer;
132
+ /** 阻断触摸结束后浏览器补发的兼容鼠标事件。 */
133
+ #suppressCompatibilityMouseUntil = 0;
111
134
  /** 确定性 opaque 交互 ID 序号。 */
112
135
  #interactionSequence = 0;
113
136
  /** 显式 Workspace 模式;Legacy 与 Workspace 状态严格隔离。 */
@@ -135,14 +158,19 @@ export class KLineChartsSceneAdapter {
135
158
  horizontal: false,
136
159
  vertical: false,
137
160
  };
138
- constructor(container, scene, handle, idMap, originalBackground) {
161
+ constructor(container, scene, handle, idMap, originalBackground, drawingInteraction = {}) {
139
162
  this.#container = container;
140
163
  this.#scene = scene;
141
164
  this.#chart = handle.chart;
142
165
  this.#engine = handle.module;
143
166
  this.#resetClickArbitration = handle.resetClickArbitration;
167
+ this.#dispatchEngineMouseMove = handle.dispatchMouseMove;
168
+ this.#dispatchEngineMouseClick = handle.dispatchMouseClick;
144
169
  this.#idMap = idMap;
145
170
  this.#originalBackground = originalBackground;
171
+ this.#drawingInteraction = structuredClone(drawingInteraction);
172
+ this.#hitTolerance('mouse');
173
+ this.#hitTolerance('touch');
146
174
  this.#installInteractionListeners();
147
175
  this.#chart.subscribeAction('onCrosshairChange', this.#handleCrosshairChange);
148
176
  this.#unsubscribeCrosshair = () => {
@@ -196,7 +224,7 @@ export class KLineChartsSceneAdapter {
196
224
  registerProjectOverlays(handle.module.registerOverlay);
197
225
  const idMap = createEngineIdMap(scene, handle.chart);
198
226
  applyPanes(scene, handle.chart, idMap);
199
- adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground);
227
+ adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground, options?.drawingInteraction);
200
228
  applyViewport(handle.chart, scene.viewport);
201
229
  container.style.backgroundColor = scene.chart.layout.backgroundColor;
202
230
  adapter.#workspaceMode = true;
@@ -359,6 +387,12 @@ export class KLineChartsSceneAdapter {
359
387
  if (previousId === id) {
360
388
  return;
361
389
  }
390
+ if (previousId === null && id !== null) {
391
+ this.#enterExclusiveSelection();
392
+ }
393
+ else if (previousId !== null && id === null) {
394
+ this.#leaveExclusiveSelection();
395
+ }
362
396
  this.#selectedOverlayId = id;
363
397
  if (this.#workspaceMode) {
364
398
  this.#emitPort({
@@ -372,7 +406,63 @@ export class KLineChartsSceneAdapter {
372
406
  this.#emit({ type: 'overlay-selected', id });
373
407
  }
374
408
  }
409
+ #enterExclusiveSelection() {
410
+ if (this.#drawingInteraction.exclusiveSelection !== true ||
411
+ this.#chartInteractionSnapshot !== undefined) {
412
+ return;
413
+ }
414
+ this.#chartInteractionSnapshot = {
415
+ scrollEnabled: this.#chart.isScrollEnabled(),
416
+ zoomEnabled: this.#chart.isZoomEnabled(),
417
+ crosshairVisible: this.#chart.getStyles().crosshair.show,
418
+ };
419
+ this.#chart.setScrollEnabled(false);
420
+ this.#chart.setZoomEnabled(false);
421
+ this.#chart.setStyles({ crosshair: { show: false } });
422
+ }
423
+ #leaveExclusiveSelection() {
424
+ const snapshot = this.#chartInteractionSnapshot;
425
+ if (snapshot === undefined) {
426
+ return;
427
+ }
428
+ this.#chartInteractionSnapshot = undefined;
429
+ this.#chart.setScrollEnabled(snapshot.scrollEnabled);
430
+ this.#chart.setZoomEnabled(snapshot.zoomEnabled);
431
+ this.#chart.setStyles({ crosshair: { show: snapshot.crosshairVisible } });
432
+ }
433
+ #hitTolerance(pointerType) {
434
+ const touch = pointerType === 'touch';
435
+ const defaults = touch
436
+ ? DEFAULT_OVERLAY_TOUCH_HIT_TOLERANCE
437
+ : DEFAULT_OVERLAY_MOUSE_HIT_TOLERANCE;
438
+ const configured = touch
439
+ ? this.#drawingInteraction.hitTolerance?.touch
440
+ : this.#drawingInteraction.hitTolerance?.mouse;
441
+ const body = configured?.body ?? defaults.body;
442
+ const anchor = configured?.anchor ?? defaults.anchor;
443
+ if (!Number.isFinite(body) || body < 0 || !Number.isFinite(anchor) || anchor < 0) {
444
+ throw new SceneError('RUNTIME_INIT_FAILED', '/drawingInteraction/hitTolerance', 'Drawing hit tolerance must contain finite non-negative CSS pixel values.');
445
+ }
446
+ return { body, anchor };
447
+ }
375
448
  #dimensionsForHit(overlay, hit) {
449
+ const constrainedType = overlay.type === 'horizontalStraightLine'
450
+ || overlay.type === 'priceLine'
451
+ || overlay.type === 'simpleTag'
452
+ || overlay.type === 'horizontalRayLine'
453
+ || overlay.type === 'horizontalSegment'
454
+ ? 'vertical'
455
+ : overlay.type === 'verticalStraightLine'
456
+ || overlay.type === 'verticalRayLine'
457
+ || overlay.type === 'verticalSegment'
458
+ ? 'horizontal'
459
+ : null;
460
+ if (constrainedType === 'vertical') {
461
+ return { horizontal: false, vertical: true };
462
+ }
463
+ if (constrainedType === 'horizontal') {
464
+ return { horizontal: true, vertical: false };
465
+ }
376
466
  if (hit.target !== 'anchor') {
377
467
  return { horizontal: true, vertical: true };
378
468
  }
@@ -430,6 +520,7 @@ export class KLineChartsSceneAdapter {
430
520
  preventDefault?.();
431
521
  },
432
522
  onDrawEnd: ({ overlay }) => {
523
+ this.#finishTouchPrecisionDrawing(overlay.id);
433
524
  if (drawing &&
434
525
  this.#interactivePriceMeasurement?.source.id === source.id) {
435
526
  this.#interactivePriceMeasurement = undefined;
@@ -463,6 +554,7 @@ export class KLineChartsSceneAdapter {
463
554
  }
464
555
  },
465
556
  onRemoved: ({ overlay }) => {
557
+ this.#finishTouchPrecisionDrawing(overlay.id);
466
558
  if (this.#interactivePriceMeasurement?.source.id === overlay.id) {
467
559
  this.#interactivePriceMeasurement = undefined;
468
560
  }
@@ -496,32 +588,191 @@ export class KLineChartsSceneAdapter {
496
588
  throw error;
497
589
  }
498
590
  }
591
+ #prepareTouchPrecisionDrawing(id, paneId) {
592
+ if (this.#drawingInteraction.touch !== 'precision-cursor') {
593
+ return;
594
+ }
595
+ this.#finishTouchPrecisionDrawing();
596
+ this.#touchPrecisionDrawing = {
597
+ id,
598
+ paneId,
599
+ phase: 'move-start',
600
+ };
601
+ const view = this.#container.ownerDocument.defaultView;
602
+ if ((view?.navigator.maxTouchPoints ?? 0) > 0 ||
603
+ view?.matchMedia('(any-pointer: coarse)').matches === true) {
604
+ const guide = this.#ensureTouchPrecisionGuide();
605
+ guide.setPhase('move-start');
606
+ guide.show();
607
+ }
608
+ }
609
+ #ensureTouchPrecisionGuide() {
610
+ this.#touchPrecisionGuide ??= new TouchPrecisionDrawingGuide(this.#container, () => this.#cancelTouchPrecisionDrawing(true));
611
+ return this.#touchPrecisionGuide;
612
+ }
613
+ #finishTouchPrecisionDrawing(id) {
614
+ const drawing = this.#touchPrecisionDrawing;
615
+ if (drawing === undefined || (id !== undefined && drawing.id !== id)) {
616
+ return;
617
+ }
618
+ this.#stopTouchPrecisionPointerCapture();
619
+ this.#touchPrecisionPointer = undefined;
620
+ this.#touchPrecisionDrawing = undefined;
621
+ this.#touchPrecisionGuide?.hide();
622
+ }
623
+ #cancelTouchPrecisionDrawing(emitRemoval) {
624
+ const drawing = this.#touchPrecisionDrawing;
625
+ if (drawing === undefined) {
626
+ return;
627
+ }
628
+ const id = drawing.id;
629
+ this.#finishTouchPrecisionDrawing(id);
630
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
631
+ this.#chart.removeOverlay({ id });
632
+ if (this.#drawingInProgressId === id) {
633
+ this.#drawingInProgressId = null;
634
+ }
635
+ if (this.#workspaceMode) {
636
+ const removed = this.#workspaceSources.delete(id);
637
+ if (emitRemoval && removed) {
638
+ this.#emitPort({ type: 'removed', id });
639
+ }
640
+ }
641
+ }
642
+ #stopTouchPrecisionPointerCapture() {
643
+ const pointer = this.#touchPrecisionPointer;
644
+ if (pointer === undefined) {
645
+ return;
646
+ }
647
+ try {
648
+ if (this.#container.hasPointerCapture(pointer.pointerId)) {
649
+ this.#container.releasePointerCapture(pointer.pointerId);
650
+ }
651
+ }
652
+ catch {
653
+ // Synthetic browser tests and interrupted system gestures may not own capture.
654
+ }
655
+ }
656
+ #touchPrecisionMainElement(paneId) {
657
+ const filter = this.#primaryAxisFilter(paneId);
658
+ const main = this.#chart.getDom(filter.paneId, 'main');
659
+ if (!(main instanceof HTMLElement)) {
660
+ throw new SceneError('INVALID_REFERENCE', '/drawings/target', 'Touch Drawing target Pane has no interactive main element.');
661
+ }
662
+ return main;
663
+ }
664
+ #touchPrecisionBounds(paneId) {
665
+ const containerRect = this.#container.getBoundingClientRect();
666
+ const mainRect = this.#touchPrecisionMainElement(paneId).getBoundingClientRect();
667
+ return {
668
+ left: Math.max(0, mainRect.left - containerRect.left),
669
+ top: Math.max(0, mainRect.top - containerRect.top),
670
+ right: Math.min(containerRect.width, mainRect.right - containerRect.left),
671
+ bottom: Math.min(containerRect.height, mainRect.bottom - containerRect.top),
672
+ };
673
+ }
674
+ #dispatchTouchPrecisionMouseMove(point) {
675
+ const rect = this.#container.getBoundingClientRect();
676
+ this.#dispatchEngineMouseMove({
677
+ x: point.x,
678
+ y: point.y,
679
+ pageX: rect.left + point.x + window.scrollX,
680
+ pageY: rect.top + point.y + window.scrollY,
681
+ });
682
+ }
683
+ #dispatchTouchPrecisionClick(point) {
684
+ const rect = this.#container.getBoundingClientRect();
685
+ this.#resetClickArbitration();
686
+ this.#dispatchEngineMouseClick({
687
+ x: point.x,
688
+ y: point.y,
689
+ pageX: rect.left + point.x + window.scrollX,
690
+ pageY: rect.top + point.y + window.scrollY,
691
+ });
692
+ }
693
+ #updateTouchPrecisionCursor(drawing, pointer) {
694
+ const cursor = resolveTouchPrecisionCursor(pointer, this.#touchPrecisionBounds(drawing.paneId));
695
+ this.#touchPrecisionGuide?.updateCursor(cursor);
696
+ this.#dispatchTouchPrecisionMouseMove(cursor);
697
+ return cursor;
698
+ }
699
+ #isTouchPrecisionCancelTarget(target) {
700
+ return this.#touchPrecisionGuide?.ownsCancelTarget(target) ?? false;
701
+ }
499
702
  #installInteractionListeners() {
500
703
  this.#container.addEventListener('mousedown', this.#handleRightMouseDown, true);
704
+ this.#container.addEventListener('click', this.#handleCompatibilityClick, true);
501
705
  this.#container.addEventListener('contextmenu', this.#handleContextMenu, true);
502
706
  this.#container.addEventListener('pointerdown', this.#handlePointerDown, true);
503
707
  this.#container.addEventListener('pointermove', this.#handlePointerMove, true);
504
708
  this.#container.addEventListener('pointerup', this.#handlePointerUp, true);
505
709
  this.#container.addEventListener('pointercancel', this.#handlePointerCancel, true);
710
+ this.#container.addEventListener('touchstart', this.#handleCompatibilityTouch, {
711
+ capture: true,
712
+ passive: false,
713
+ });
714
+ this.#container.addEventListener('touchmove', this.#handleCompatibilityTouch, {
715
+ capture: true,
716
+ passive: false,
717
+ });
718
+ this.#container.addEventListener('touchend', this.#handleCompatibilityTouch, {
719
+ capture: true,
720
+ passive: false,
721
+ });
722
+ this.#container.addEventListener('touchcancel', this.#handleCompatibilityTouch, {
723
+ capture: true,
724
+ passive: false,
725
+ });
506
726
  window.addEventListener('keydown', this.#handleKeyDown);
507
727
  window.addEventListener('blur', this.#handleWindowBlur);
508
728
  }
509
729
  #removeInteractionListeners() {
510
730
  this.#container.removeEventListener('mousedown', this.#handleRightMouseDown, true);
731
+ this.#container.removeEventListener('click', this.#handleCompatibilityClick, true);
511
732
  this.#container.removeEventListener('contextmenu', this.#handleContextMenu, true);
512
733
  this.#container.removeEventListener('pointerdown', this.#handlePointerDown, true);
513
734
  this.#container.removeEventListener('pointermove', this.#handlePointerMove, true);
514
735
  this.#container.removeEventListener('pointerup', this.#handlePointerUp, true);
515
736
  this.#container.removeEventListener('pointercancel', this.#handlePointerCancel, true);
737
+ this.#container.removeEventListener('touchstart', this.#handleCompatibilityTouch, true);
738
+ this.#container.removeEventListener('touchmove', this.#handleCompatibilityTouch, true);
739
+ this.#container.removeEventListener('touchend', this.#handleCompatibilityTouch, true);
740
+ this.#container.removeEventListener('touchcancel', this.#handleCompatibilityTouch, true);
516
741
  window.removeEventListener('keydown', this.#handleKeyDown);
517
742
  window.removeEventListener('blur', this.#handleWindowBlur);
518
743
  }
519
744
  #handleRightMouseDown = (event) => {
745
+ if (event.button === 0 &&
746
+ event.isTrusted &&
747
+ performance.now() < this.#suppressCompatibilityMouseUntil &&
748
+ !this.#isTouchPrecisionCancelTarget(event.target)) {
749
+ event.preventDefault();
750
+ event.stopImmediatePropagation();
751
+ return;
752
+ }
520
753
  if (event.button === 2) {
521
754
  // 阻止事件到达 KLineCharts;不 preventDefault,保留浏览器原生右键菜单。
522
755
  event.stopPropagation();
523
756
  }
524
757
  };
758
+ #handleCompatibilityClick = (event) => {
759
+ if (event.isTrusted &&
760
+ performance.now() < this.#suppressCompatibilityMouseUntil &&
761
+ !this.#isTouchPrecisionCancelTarget(event.target)) {
762
+ event.preventDefault();
763
+ event.stopImmediatePropagation();
764
+ }
765
+ };
766
+ #handleCompatibilityTouch = (event) => {
767
+ if ((this.#touchPrecisionDrawing !== undefined ||
768
+ performance.now() < this.#suppressCompatibilityMouseUntil) &&
769
+ !this.#isTouchPrecisionCancelTarget(event.target)) {
770
+ if (event.cancelable) {
771
+ event.preventDefault();
772
+ }
773
+ event.stopImmediatePropagation();
774
+ }
775
+ };
525
776
  #handleContextMenu = (event) => {
526
777
  // KLineCharts 会 preventDefault。捕获阶段截断传播,但 Baron 本身不拦截菜单。
527
778
  event.stopPropagation();
@@ -598,60 +849,40 @@ export class KLineChartsSceneAdapter {
598
849
  #overlayGeometries() {
599
850
  const geometries = [];
600
851
  const active = this.#activeOverlays();
852
+ const containerRect = this.#container.getBoundingClientRect();
853
+ const canvas = this.#container.ownerDocument.createElement('canvas');
854
+ const textContext = canvas.getContext('2d');
601
855
  for (let sceneIndex = 0; sceneIndex < active.length; sceneIndex++) {
602
856
  const overlay = active[sceneIndex];
603
857
  if (overlay === undefined || !overlay.visible) {
604
858
  continue;
605
859
  }
606
- if (overlay.type === 'horizontalStraightLine') {
607
- const anchor = overlay.anchor;
608
- if (anchor === undefined || !('value' in anchor)) {
609
- continue;
610
- }
611
- const paneFilter = this.#primaryAxisFilter(overlay.paneId);
612
- const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
613
- const containerRect = this.#container.getBoundingClientRect();
614
- const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
615
- const projected = this.#toPixel({ timestamp: this.#scene.data[0].timestamp, value: anchor.value }, overlay.paneId);
616
- const start = { x: mainRect.left - containerRect.left, y: projected.y };
617
- const end = { x: mainRect.right - containerRect.left, y: projected.y };
618
- geometries.push({
619
- overlayId: overlay.id,
620
- sceneIndex,
621
- zLevel: overlay.zLevel,
622
- locked: overlay.locked,
623
- anchors: [{ x: (start.x + end.x) / 2, y: projected.y }],
624
- bodySegments: [[start, end]],
625
- });
626
- continue;
627
- }
628
- if (overlay.type === 'priceMeasurement' && overlay.start !== undefined && overlay.end !== undefined) {
629
- const start = this.#toPixel(overlay.start, overlay.paneId);
630
- const end = this.#toPixel(overlay.end, overlay.paneId);
631
- geometries.push({
632
- overlayId: overlay.id,
633
- sceneIndex,
634
- zLevel: overlay.zLevel,
635
- locked: overlay.locked,
636
- anchors: [start, end],
637
- bodySegments: [[start, end]],
638
- });
639
- continue;
640
- }
641
- if (overlay.points !== undefined && overlay.points.length >= 2) {
642
- const anchors = overlay.points.map((point) => this.#toPixel(point, overlay.paneId));
643
- const bodySegments = [];
644
- for (let pointIndex = 1; pointIndex < anchors.length; pointIndex++) {
645
- bodySegments.push([anchors[pointIndex - 1], anchors[pointIndex]]);
646
- }
647
- geometries.push({
648
- overlayId: overlay.id,
649
- sceneIndex,
650
- zLevel: overlay.zLevel,
651
- locked: overlay.locked,
652
- anchors,
653
- bodySegments,
654
- });
860
+ const paneFilter = this.#primaryAxisFilter(overlay.paneId);
861
+ const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
862
+ const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
863
+ const geometry = projectOverlayGeometry(overlay, sceneIndex, {
864
+ bounds: {
865
+ left: mainRect.left - containerRect.left,
866
+ top: mainRect.top - containerRect.top,
867
+ right: mainRect.right - containerRect.left,
868
+ bottom: mainRect.bottom - containerRect.top,
869
+ },
870
+ referenceTimestamp: this.#scene.data[0].timestamp,
871
+ referenceValue: this.#scene.data[0].close,
872
+ project: (point) => this.#toPixel(point, overlay.paneId),
873
+ measureText: (text, source) => {
874
+ const textStyle = source.styles.text;
875
+ if (textContext !== null) {
876
+ textContext.font = `${textStyle.weight} ${textStyle.size}px ${textStyle.family}`;
877
+ }
878
+ return {
879
+ width: Math.max(textStyle.size, textContext?.measureText(text).width ?? [...text].length * textStyle.size),
880
+ height: textStyle.size * 1.4,
881
+ };
882
+ },
883
+ });
884
+ if (geometry !== null) {
885
+ geometries.push(geometry);
655
886
  }
656
887
  }
657
888
  return geometries;
@@ -695,13 +926,102 @@ export class KLineChartsSceneAdapter {
695
926
  this.#emit({ type: 'scene-error', issues: structuredClone(error.issues) });
696
927
  }
697
928
  }
929
+ #routeTouchPrecisionPointerDown(event) {
930
+ const drawing = this.#touchPrecisionDrawing;
931
+ if (drawing === undefined || this.#isTouchPrecisionCancelTarget(event.target)) {
932
+ return false;
933
+ }
934
+ if (event.pointerType !== 'touch') {
935
+ this.#finishTouchPrecisionDrawing(drawing.id);
936
+ return false;
937
+ }
938
+ event.preventDefault();
939
+ event.stopImmediatePropagation();
940
+ if (this.#touchPrecisionPointer !== undefined) {
941
+ return true;
942
+ }
943
+ const guide = this.#ensureTouchPrecisionGuide();
944
+ guide.setPhase(drawing.phase);
945
+ guide.show();
946
+ const point = this.#pointerCoordinate(event);
947
+ this.#touchPrecisionPointer = {
948
+ pointerId: event.pointerId,
949
+ origin: point,
950
+ current: point,
951
+ };
952
+ try {
953
+ this.#container.setPointerCapture(event.pointerId);
954
+ }
955
+ catch {
956
+ // Synthetic PointerEvents do not own browser pointer capture.
957
+ }
958
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
959
+ this.#updateTouchPrecisionCursor(drawing, point);
960
+ return true;
961
+ }
962
+ #routeTouchPrecisionPointerMove(event) {
963
+ const drawing = this.#touchPrecisionDrawing;
964
+ const pointer = this.#touchPrecisionPointer;
965
+ if (drawing === undefined ||
966
+ pointer === undefined ||
967
+ pointer.pointerId !== event.pointerId) {
968
+ return false;
969
+ }
970
+ event.preventDefault();
971
+ event.stopImmediatePropagation();
972
+ const point = this.#pointerCoordinate(event);
973
+ pointer.current = point;
974
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
975
+ this.#updateTouchPrecisionCursor(drawing, point);
976
+ return true;
977
+ }
978
+ #routeTouchPrecisionPointerUp(event) {
979
+ const drawing = this.#touchPrecisionDrawing;
980
+ const pointer = this.#touchPrecisionPointer;
981
+ if (drawing === undefined ||
982
+ pointer === undefined ||
983
+ pointer.pointerId !== event.pointerId) {
984
+ return false;
985
+ }
986
+ event.preventDefault();
987
+ event.stopImmediatePropagation();
988
+ const point = this.#pointerCoordinate(event);
989
+ pointer.current = point;
990
+ const cursor = this.#updateTouchPrecisionCursor(drawing, point);
991
+ const tap = isTouchPrecisionTap(pointer.origin, pointer.current);
992
+ this.#stopTouchPrecisionPointerCapture();
993
+ this.#touchPrecisionPointer = undefined;
994
+ this.#suppressCompatibilityMouseUntil = performance.now() + 800;
995
+ if (drawing.phase === 'move-start') {
996
+ drawing.phase = 'confirm-start';
997
+ this.#touchPrecisionGuide?.setPhase('confirm-start');
998
+ return true;
999
+ }
1000
+ if (!tap) {
1001
+ return true;
1002
+ }
1003
+ const phase = drawing.phase;
1004
+ this.#dispatchTouchPrecisionClick(cursor);
1005
+ const active = this.#touchPrecisionDrawing;
1006
+ if (phase === 'confirm-start' &&
1007
+ active !== undefined &&
1008
+ active.id === drawing.id) {
1009
+ active.phase = 'confirm-end';
1010
+ this.#touchPrecisionGuide?.setPhase('confirm-end');
1011
+ }
1012
+ return true;
1013
+ }
698
1014
  #handlePointerDown = (event) => {
699
1015
  if (this.#disposed ||
700
1016
  event.button !== 0 ||
701
1017
  this.#pointerInteraction !== undefined ||
1018
+ this.#deselectingPointerId !== undefined ||
702
1019
  !this.#mutationsEnabled) {
703
1020
  return;
704
1021
  }
1022
+ if (this.#routeTouchPrecisionPointerDown(event)) {
1023
+ return;
1024
+ }
705
1025
  const coordinate = this.#pointerCoordinate(event);
706
1026
  const measurement = this.#interactivePriceMeasurement;
707
1027
  if (measurement !== undefined) {
@@ -732,9 +1052,22 @@ export class KLineChartsSceneAdapter {
732
1052
  if (this.#drawingInProgressId !== null) {
733
1053
  return;
734
1054
  }
735
- const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
1055
+ const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries(), this.#hitTolerance(event.pointerType));
736
1056
  if (hit === null) {
737
1057
  const selected = this.#activeOverlays().find((overlay) => overlay.id === this.#selectedOverlayId);
1058
+ if (selected !== undefined &&
1059
+ this.#drawingInteraction.exclusiveSelection === true) {
1060
+ event.preventDefault();
1061
+ event.stopImmediatePropagation();
1062
+ this.#deselectingPointerId = event.pointerId;
1063
+ try {
1064
+ this.#container.setPointerCapture(event.pointerId);
1065
+ }
1066
+ catch {
1067
+ // Synthetic PointerEvents do not own browser pointer capture.
1068
+ }
1069
+ return;
1070
+ }
738
1071
  if (selected !== undefined && isControlledInteractionOverlay(selected)) {
739
1072
  this.#selectOverlay(null);
740
1073
  }
@@ -745,7 +1078,8 @@ export class KLineChartsSceneAdapter {
745
1078
  return;
746
1079
  }
747
1080
  this.#selectOverlay(before.id);
748
- if (!isControlledInteractionOverlay(before)) {
1081
+ if (!isControlledInteractionOverlay(before) &&
1082
+ this.#drawingInteraction.exclusiveSelection !== true) {
749
1083
  return;
750
1084
  }
751
1085
  event.preventDefault();
@@ -753,7 +1087,12 @@ export class KLineChartsSceneAdapter {
753
1087
  if (hit.locked) {
754
1088
  return;
755
1089
  }
756
- this.#container.setPointerCapture(event.pointerId);
1090
+ try {
1091
+ this.#container.setPointerCapture(event.pointerId);
1092
+ }
1093
+ catch {
1094
+ // Synthetic PointerEvents do not own browser pointer capture.
1095
+ }
757
1096
  this.#interactionDimensions = this.#dimensionsForHit(before, hit);
758
1097
  this.#pointerInteraction = {
759
1098
  pointerId: event.pointerId,
@@ -766,6 +1105,14 @@ export class KLineChartsSceneAdapter {
766
1105
  };
767
1106
  };
768
1107
  #handlePointerMove = (event) => {
1108
+ if (this.#routeTouchPrecisionPointerMove(event)) {
1109
+ return;
1110
+ }
1111
+ if (this.#deselectingPointerId === event.pointerId) {
1112
+ event.preventDefault();
1113
+ event.stopImmediatePropagation();
1114
+ return;
1115
+ }
769
1116
  const interaction = this.#pointerInteraction;
770
1117
  if (interaction === undefined || interaction.pointerId !== event.pointerId) {
771
1118
  return;
@@ -797,6 +1144,7 @@ export class KLineChartsSceneAdapter {
797
1144
  throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to preview Overlay ${normalized.id}.`);
798
1145
  }
799
1146
  if (this.#workspaceMode) {
1147
+ interaction.candidate = candidate;
800
1148
  this.#setActiveOverlays(overlays);
801
1149
  return;
802
1150
  }
@@ -816,6 +1164,19 @@ export class KLineChartsSceneAdapter {
816
1164
  }
817
1165
  };
818
1166
  #handlePointerUp = (event) => {
1167
+ if (this.#routeTouchPrecisionPointerUp(event)) {
1168
+ return;
1169
+ }
1170
+ if (this.#deselectingPointerId === event.pointerId) {
1171
+ event.preventDefault();
1172
+ event.stopImmediatePropagation();
1173
+ if (this.#container.hasPointerCapture(event.pointerId)) {
1174
+ this.#container.releasePointerCapture(event.pointerId);
1175
+ }
1176
+ this.#deselectingPointerId = undefined;
1177
+ this.#selectOverlay(null);
1178
+ return;
1179
+ }
819
1180
  const interaction = this.#pointerInteraction;
820
1181
  if (interaction === undefined || interaction.pointerId !== event.pointerId) {
821
1182
  return;
@@ -823,6 +1184,7 @@ export class KLineChartsSceneAdapter {
823
1184
  event.preventDefault();
824
1185
  event.stopImmediatePropagation();
825
1186
  this.#pointerInteraction = undefined;
1187
+ const interactionDimensions = this.#interactionDimensions;
826
1188
  this.#interactionDimensions = { horizontal: false, vertical: false };
827
1189
  this.#stopPointerCapture(interaction);
828
1190
  if (!interaction.started) {
@@ -841,7 +1203,7 @@ export class KLineChartsSceneAdapter {
841
1203
  type: 'updated',
842
1204
  id: overlay.id,
843
1205
  drawing: snapshotOfDrawing(sceneOverlayToDrawing(overlay, drawing)),
844
- editDimensions: structuredClone(this.#interactionDimensions),
1206
+ editDimensions: structuredClone(interactionDimensions),
845
1207
  });
846
1208
  }
847
1209
  return;
@@ -856,21 +1218,56 @@ export class KLineChartsSceneAdapter {
856
1218
  this.#emit({ type: 'overlay-updated', overlay: committed });
857
1219
  };
858
1220
  #handlePointerCancel = (event) => {
1221
+ if (this.#touchPrecisionPointer?.pointerId === event.pointerId) {
1222
+ event.preventDefault();
1223
+ event.stopImmediatePropagation();
1224
+ this.#cancelTouchPrecisionDrawing(true);
1225
+ return;
1226
+ }
859
1227
  if (this.#pointerInteraction?.pointerId === event.pointerId) {
860
1228
  event.preventDefault();
861
1229
  event.stopImmediatePropagation();
862
1230
  this.#cancelInteraction('pointer-cancel');
1231
+ return;
1232
+ }
1233
+ if (this.#deselectingPointerId === event.pointerId) {
1234
+ event.preventDefault();
1235
+ event.stopImmediatePropagation();
1236
+ if (this.#container.hasPointerCapture(event.pointerId)) {
1237
+ this.#container.releasePointerCapture(event.pointerId);
1238
+ }
1239
+ this.#deselectingPointerId = undefined;
1240
+ this.#selectOverlay(null);
863
1241
  }
864
1242
  };
865
1243
  #handleKeyDown = (event) => {
1244
+ if (event.key === 'Escape' && this.#touchPrecisionDrawing !== undefined) {
1245
+ this.#cancelTouchPrecisionDrawing(true);
1246
+ return;
1247
+ }
866
1248
  if (event.key === 'Escape' && this.#pointerInteraction !== undefined) {
867
1249
  this.#cancelInteraction('escape');
868
1250
  }
1251
+ if (event.key === 'Escape' &&
1252
+ this.#drawingInteraction.exclusiveSelection === true &&
1253
+ this.#selectedOverlayId !== null) {
1254
+ this.#selectOverlay(null);
1255
+ }
869
1256
  };
870
1257
  #handleWindowBlur = () => {
1258
+ if (this.#touchPrecisionDrawing !== undefined) {
1259
+ this.#cancelTouchPrecisionDrawing(true);
1260
+ }
871
1261
  if (this.#pointerInteraction !== undefined) {
872
1262
  this.#cancelInteraction('window-blur');
873
1263
  }
1264
+ if (this.#deselectingPointerId !== undefined) {
1265
+ if (this.#container.hasPointerCapture(this.#deselectingPointerId)) {
1266
+ this.#container.releasePointerCapture(this.#deselectingPointerId);
1267
+ }
1268
+ this.#deselectingPointerId = undefined;
1269
+ this.#selectOverlay(null);
1270
+ }
874
1271
  };
875
1272
  subscribe(listener) {
876
1273
  this.#assertActive();
@@ -1080,6 +1477,9 @@ export class KLineChartsSceneAdapter {
1080
1477
  throw new SceneError('RUNTIME_INIT_FAILED', '/drawings', `KLineCharts failed to start Drawing ${request.id}.`);
1081
1478
  }
1082
1479
  this.#drawingInProgressId = request.id;
1480
+ if (request.type === 'segment') {
1481
+ this.#prepareTouchPrecisionDrawing(request.id, overlay.paneId);
1482
+ }
1083
1483
  return request.id;
1084
1484
  }
1085
1485
  listDrawings() {
@@ -1542,6 +1942,11 @@ export class KLineChartsSceneAdapter {
1542
1942
  return;
1543
1943
  }
1544
1944
  this.#cancelInteraction('destroy');
1945
+ this.#deselectingPointerId = undefined;
1946
+ this.#leaveExclusiveSelection();
1947
+ this.#cancelTouchPrecisionDrawing(false);
1948
+ this.#touchPrecisionGuide?.destroy();
1949
+ this.#touchPrecisionGuide = undefined;
1545
1950
  this.#interactivePriceMeasurement = undefined;
1546
1951
  this.#drawingInProgressId = null;
1547
1952
  this.#disposed = true;