@baron1996/klinecharts-adapter 0.9.3 → 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 = () => {
@@ -188,11 +216,15 @@ export class KLineChartsSceneAdapter {
188
216
  let handle;
189
217
  let adapter;
190
218
  try {
191
- handle = await createEngine(container, scene);
219
+ handle = await createEngine(container, scene, {
220
+ ...(options?.displayTimezone === undefined
221
+ ? {}
222
+ : { displayTimezone: options.displayTimezone }),
223
+ });
192
224
  registerProjectOverlays(handle.module.registerOverlay);
193
225
  const idMap = createEngineIdMap(scene, handle.chart);
194
226
  applyPanes(scene, handle.chart, idMap);
195
- adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground);
227
+ adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground, options?.drawingInteraction);
196
228
  applyViewport(handle.chart, scene.viewport);
197
229
  container.style.backgroundColor = scene.chart.layout.backgroundColor;
198
230
  adapter.#workspaceMode = true;
@@ -355,6 +387,12 @@ export class KLineChartsSceneAdapter {
355
387
  if (previousId === id) {
356
388
  return;
357
389
  }
390
+ if (previousId === null && id !== null) {
391
+ this.#enterExclusiveSelection();
392
+ }
393
+ else if (previousId !== null && id === null) {
394
+ this.#leaveExclusiveSelection();
395
+ }
358
396
  this.#selectedOverlayId = id;
359
397
  if (this.#workspaceMode) {
360
398
  this.#emitPort({
@@ -368,7 +406,63 @@ export class KLineChartsSceneAdapter {
368
406
  this.#emit({ type: 'overlay-selected', id });
369
407
  }
370
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
+ }
371
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
+ }
372
466
  if (hit.target !== 'anchor') {
373
467
  return { horizontal: true, vertical: true };
374
468
  }
@@ -426,6 +520,7 @@ export class KLineChartsSceneAdapter {
426
520
  preventDefault?.();
427
521
  },
428
522
  onDrawEnd: ({ overlay }) => {
523
+ this.#finishTouchPrecisionDrawing(overlay.id);
429
524
  if (drawing &&
430
525
  this.#interactivePriceMeasurement?.source.id === source.id) {
431
526
  this.#interactivePriceMeasurement = undefined;
@@ -459,6 +554,7 @@ export class KLineChartsSceneAdapter {
459
554
  }
460
555
  },
461
556
  onRemoved: ({ overlay }) => {
557
+ this.#finishTouchPrecisionDrawing(overlay.id);
462
558
  if (this.#interactivePriceMeasurement?.source.id === overlay.id) {
463
559
  this.#interactivePriceMeasurement = undefined;
464
560
  }
@@ -492,32 +588,191 @@ export class KLineChartsSceneAdapter {
492
588
  throw error;
493
589
  }
494
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
+ }
495
702
  #installInteractionListeners() {
496
703
  this.#container.addEventListener('mousedown', this.#handleRightMouseDown, true);
704
+ this.#container.addEventListener('click', this.#handleCompatibilityClick, true);
497
705
  this.#container.addEventListener('contextmenu', this.#handleContextMenu, true);
498
706
  this.#container.addEventListener('pointerdown', this.#handlePointerDown, true);
499
707
  this.#container.addEventListener('pointermove', this.#handlePointerMove, true);
500
708
  this.#container.addEventListener('pointerup', this.#handlePointerUp, true);
501
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
+ });
502
726
  window.addEventListener('keydown', this.#handleKeyDown);
503
727
  window.addEventListener('blur', this.#handleWindowBlur);
504
728
  }
505
729
  #removeInteractionListeners() {
506
730
  this.#container.removeEventListener('mousedown', this.#handleRightMouseDown, true);
731
+ this.#container.removeEventListener('click', this.#handleCompatibilityClick, true);
507
732
  this.#container.removeEventListener('contextmenu', this.#handleContextMenu, true);
508
733
  this.#container.removeEventListener('pointerdown', this.#handlePointerDown, true);
509
734
  this.#container.removeEventListener('pointermove', this.#handlePointerMove, true);
510
735
  this.#container.removeEventListener('pointerup', this.#handlePointerUp, true);
511
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);
512
741
  window.removeEventListener('keydown', this.#handleKeyDown);
513
742
  window.removeEventListener('blur', this.#handleWindowBlur);
514
743
  }
515
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
+ }
516
753
  if (event.button === 2) {
517
754
  // 阻止事件到达 KLineCharts;不 preventDefault,保留浏览器原生右键菜单。
518
755
  event.stopPropagation();
519
756
  }
520
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
+ };
521
776
  #handleContextMenu = (event) => {
522
777
  // KLineCharts 会 preventDefault。捕获阶段截断传播,但 Baron 本身不拦截菜单。
523
778
  event.stopPropagation();
@@ -594,60 +849,40 @@ export class KLineChartsSceneAdapter {
594
849
  #overlayGeometries() {
595
850
  const geometries = [];
596
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');
597
855
  for (let sceneIndex = 0; sceneIndex < active.length; sceneIndex++) {
598
856
  const overlay = active[sceneIndex];
599
857
  if (overlay === undefined || !overlay.visible) {
600
858
  continue;
601
859
  }
602
- if (overlay.type === 'horizontalStraightLine') {
603
- const anchor = overlay.anchor;
604
- if (anchor === undefined || !('value' in anchor)) {
605
- continue;
606
- }
607
- const paneFilter = this.#primaryAxisFilter(overlay.paneId);
608
- const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
609
- const containerRect = this.#container.getBoundingClientRect();
610
- const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
611
- const projected = this.#toPixel({ timestamp: this.#scene.data[0].timestamp, value: anchor.value }, overlay.paneId);
612
- const start = { x: mainRect.left - containerRect.left, y: projected.y };
613
- const end = { x: mainRect.right - containerRect.left, y: projected.y };
614
- geometries.push({
615
- overlayId: overlay.id,
616
- sceneIndex,
617
- zLevel: overlay.zLevel,
618
- locked: overlay.locked,
619
- anchors: [{ x: (start.x + end.x) / 2, y: projected.y }],
620
- bodySegments: [[start, end]],
621
- });
622
- continue;
623
- }
624
- if (overlay.type === 'priceMeasurement' && overlay.start !== undefined && overlay.end !== undefined) {
625
- const start = this.#toPixel(overlay.start, overlay.paneId);
626
- const end = this.#toPixel(overlay.end, overlay.paneId);
627
- geometries.push({
628
- overlayId: overlay.id,
629
- sceneIndex,
630
- zLevel: overlay.zLevel,
631
- locked: overlay.locked,
632
- anchors: [start, end],
633
- bodySegments: [[start, end]],
634
- });
635
- continue;
636
- }
637
- if (overlay.points !== undefined && overlay.points.length >= 2) {
638
- const anchors = overlay.points.map((point) => this.#toPixel(point, overlay.paneId));
639
- const bodySegments = [];
640
- for (let pointIndex = 1; pointIndex < anchors.length; pointIndex++) {
641
- bodySegments.push([anchors[pointIndex - 1], anchors[pointIndex]]);
642
- }
643
- geometries.push({
644
- overlayId: overlay.id,
645
- sceneIndex,
646
- zLevel: overlay.zLevel,
647
- locked: overlay.locked,
648
- anchors,
649
- bodySegments,
650
- });
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);
651
886
  }
652
887
  }
653
888
  return geometries;
@@ -691,13 +926,102 @@ export class KLineChartsSceneAdapter {
691
926
  this.#emit({ type: 'scene-error', issues: structuredClone(error.issues) });
692
927
  }
693
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
+ }
694
1014
  #handlePointerDown = (event) => {
695
1015
  if (this.#disposed ||
696
1016
  event.button !== 0 ||
697
1017
  this.#pointerInteraction !== undefined ||
1018
+ this.#deselectingPointerId !== undefined ||
698
1019
  !this.#mutationsEnabled) {
699
1020
  return;
700
1021
  }
1022
+ if (this.#routeTouchPrecisionPointerDown(event)) {
1023
+ return;
1024
+ }
701
1025
  const coordinate = this.#pointerCoordinate(event);
702
1026
  const measurement = this.#interactivePriceMeasurement;
703
1027
  if (measurement !== undefined) {
@@ -728,9 +1052,22 @@ export class KLineChartsSceneAdapter {
728
1052
  if (this.#drawingInProgressId !== null) {
729
1053
  return;
730
1054
  }
731
- const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
1055
+ const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries(), this.#hitTolerance(event.pointerType));
732
1056
  if (hit === null) {
733
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
+ }
734
1071
  if (selected !== undefined && isControlledInteractionOverlay(selected)) {
735
1072
  this.#selectOverlay(null);
736
1073
  }
@@ -741,7 +1078,8 @@ export class KLineChartsSceneAdapter {
741
1078
  return;
742
1079
  }
743
1080
  this.#selectOverlay(before.id);
744
- if (!isControlledInteractionOverlay(before)) {
1081
+ if (!isControlledInteractionOverlay(before) &&
1082
+ this.#drawingInteraction.exclusiveSelection !== true) {
745
1083
  return;
746
1084
  }
747
1085
  event.preventDefault();
@@ -749,7 +1087,12 @@ export class KLineChartsSceneAdapter {
749
1087
  if (hit.locked) {
750
1088
  return;
751
1089
  }
752
- 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
+ }
753
1096
  this.#interactionDimensions = this.#dimensionsForHit(before, hit);
754
1097
  this.#pointerInteraction = {
755
1098
  pointerId: event.pointerId,
@@ -762,6 +1105,14 @@ export class KLineChartsSceneAdapter {
762
1105
  };
763
1106
  };
764
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
+ }
765
1116
  const interaction = this.#pointerInteraction;
766
1117
  if (interaction === undefined || interaction.pointerId !== event.pointerId) {
767
1118
  return;
@@ -793,6 +1144,7 @@ export class KLineChartsSceneAdapter {
793
1144
  throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to preview Overlay ${normalized.id}.`);
794
1145
  }
795
1146
  if (this.#workspaceMode) {
1147
+ interaction.candidate = candidate;
796
1148
  this.#setActiveOverlays(overlays);
797
1149
  return;
798
1150
  }
@@ -812,6 +1164,19 @@ export class KLineChartsSceneAdapter {
812
1164
  }
813
1165
  };
814
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
+ }
815
1180
  const interaction = this.#pointerInteraction;
816
1181
  if (interaction === undefined || interaction.pointerId !== event.pointerId) {
817
1182
  return;
@@ -819,6 +1184,7 @@ export class KLineChartsSceneAdapter {
819
1184
  event.preventDefault();
820
1185
  event.stopImmediatePropagation();
821
1186
  this.#pointerInteraction = undefined;
1187
+ const interactionDimensions = this.#interactionDimensions;
822
1188
  this.#interactionDimensions = { horizontal: false, vertical: false };
823
1189
  this.#stopPointerCapture(interaction);
824
1190
  if (!interaction.started) {
@@ -837,7 +1203,7 @@ export class KLineChartsSceneAdapter {
837
1203
  type: 'updated',
838
1204
  id: overlay.id,
839
1205
  drawing: snapshotOfDrawing(sceneOverlayToDrawing(overlay, drawing)),
840
- editDimensions: structuredClone(this.#interactionDimensions),
1206
+ editDimensions: structuredClone(interactionDimensions),
841
1207
  });
842
1208
  }
843
1209
  return;
@@ -852,21 +1218,56 @@ export class KLineChartsSceneAdapter {
852
1218
  this.#emit({ type: 'overlay-updated', overlay: committed });
853
1219
  };
854
1220
  #handlePointerCancel = (event) => {
1221
+ if (this.#touchPrecisionPointer?.pointerId === event.pointerId) {
1222
+ event.preventDefault();
1223
+ event.stopImmediatePropagation();
1224
+ this.#cancelTouchPrecisionDrawing(true);
1225
+ return;
1226
+ }
855
1227
  if (this.#pointerInteraction?.pointerId === event.pointerId) {
856
1228
  event.preventDefault();
857
1229
  event.stopImmediatePropagation();
858
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);
859
1241
  }
860
1242
  };
861
1243
  #handleKeyDown = (event) => {
1244
+ if (event.key === 'Escape' && this.#touchPrecisionDrawing !== undefined) {
1245
+ this.#cancelTouchPrecisionDrawing(true);
1246
+ return;
1247
+ }
862
1248
  if (event.key === 'Escape' && this.#pointerInteraction !== undefined) {
863
1249
  this.#cancelInteraction('escape');
864
1250
  }
1251
+ if (event.key === 'Escape' &&
1252
+ this.#drawingInteraction.exclusiveSelection === true &&
1253
+ this.#selectedOverlayId !== null) {
1254
+ this.#selectOverlay(null);
1255
+ }
865
1256
  };
866
1257
  #handleWindowBlur = () => {
1258
+ if (this.#touchPrecisionDrawing !== undefined) {
1259
+ this.#cancelTouchPrecisionDrawing(true);
1260
+ }
867
1261
  if (this.#pointerInteraction !== undefined) {
868
1262
  this.#cancelInteraction('window-blur');
869
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
+ }
870
1271
  };
871
1272
  subscribe(listener) {
872
1273
  this.#assertActive();
@@ -1076,6 +1477,9 @@ export class KLineChartsSceneAdapter {
1076
1477
  throw new SceneError('RUNTIME_INIT_FAILED', '/drawings', `KLineCharts failed to start Drawing ${request.id}.`);
1077
1478
  }
1078
1479
  this.#drawingInProgressId = request.id;
1480
+ if (request.type === 'segment') {
1481
+ this.#prepareTouchPrecisionDrawing(request.id, overlay.paneId);
1482
+ }
1079
1483
  return request.id;
1080
1484
  }
1081
1485
  listDrawings() {
@@ -1538,6 +1942,11 @@ export class KLineChartsSceneAdapter {
1538
1942
  return;
1539
1943
  }
1540
1944
  this.#cancelInteraction('destroy');
1945
+ this.#deselectingPointerId = undefined;
1946
+ this.#leaveExclusiveSelection();
1947
+ this.#cancelTouchPrecisionDrawing(false);
1948
+ this.#touchPrecisionGuide?.destroy();
1949
+ this.#touchPrecisionGuide = undefined;
1541
1950
  this.#interactivePriceMeasurement = undefined;
1542
1951
  this.#drawingInProgressId = null;
1543
1952
  this.#disposed = true;