@baron1996/klinecharts-adapter 0.1.1 → 0.2.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.
Files changed (35) hide show
  1. package/dist/adapter.d.ts +40 -1
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +488 -21
  4. package/dist/conversion/overlays.d.ts +3 -0
  5. package/dist/conversion/overlays.d.ts.map +1 -1
  6. package/dist/conversion/overlays.js +2 -0
  7. package/dist/conversion/panes.d.ts +3 -1
  8. package/dist/conversion/panes.d.ts.map +1 -1
  9. package/dist/conversion/panes.js +7 -1
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js +3 -3
  12. package/dist/extensions/price-measurement.d.ts +13 -0
  13. package/dist/extensions/price-measurement.d.ts.map +1 -0
  14. package/dist/extensions/price-measurement.js +64 -0
  15. package/dist/extensions/register.d.ts.map +1 -1
  16. package/dist/extensions/register.js +2 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/interaction/dragging.d.ts +12 -0
  21. package/dist/interaction/dragging.d.ts.map +1 -0
  22. package/dist/interaction/dragging.js +71 -0
  23. package/dist/interaction/hit-testing.d.ts +26 -0
  24. package/dist/interaction/hit-testing.d.ts.map +1 -0
  25. package/dist/interaction/hit-testing.js +86 -0
  26. package/dist/interaction/selection-arbitration.d.ts +6 -0
  27. package/dist/interaction/selection-arbitration.d.ts.map +1 -0
  28. package/dist/interaction/selection-arbitration.js +10 -0
  29. package/dist/registry/overlays.d.ts +2 -2
  30. package/dist/registry/overlays.d.ts.map +1 -1
  31. package/dist/registry/overlays.js +1 -0
  32. package/dist/version.d.ts +3 -2
  33. package/dist/version.d.ts.map +1 -1
  34. package/dist/version.js +3 -2
  35. package/package.json +2 -2
package/dist/adapter.js CHANGED
@@ -2,9 +2,29 @@ import { parseChartScene, SceneError, } from '@baron1996/kline-scene-schema';
2
2
  import { createEngine } from './engine.js';
3
3
  import { registerProjectOverlays } from './extensions/register.js';
4
4
  import { createEngineIdMap, } from './conversion/id-map.js';
5
- import { applyPanes } from './conversion/panes.js';
5
+ import { applyPanes, overrideSceneYAxis } from './conversion/panes.js';
6
6
  import { createSceneOverlays, fromEngineOverlay, toEngineOverlay, toEngineOverlayDrawing, } from './conversion/overlays.js';
7
+ import { normalizePriceValue } from './conversion/price.js';
7
8
  import { applyViewport } from './conversion/viewport.js';
9
+ import { createDragCandidate, } from './interaction/dragging.js';
10
+ import { hitTestOverlayGeometries, } from './interaction/hit-testing.js';
11
+ import { shouldIgnoreStaleOverlayDeselection } from './interaction/selection-arbitration.js';
12
+ function promoteSceneToM2(scene, scale) {
13
+ const candidate = structuredClone(scene);
14
+ candidate.runtime.runtimeVersion = '0.2.0';
15
+ for (const pane of candidate.panes) {
16
+ for (const axis of pane.yAxes) {
17
+ axis.scale =
18
+ pane.kind === 'candle' && axis.role === 'primary' && scale !== undefined
19
+ ? scale
20
+ : axis.scale ?? 'linear';
21
+ }
22
+ }
23
+ return candidate;
24
+ }
25
+ function isControlledInteractionOverlay(overlay) {
26
+ return overlay.type === 'horizontalStraightLine' || overlay.type === 'priceMeasurement';
27
+ }
8
28
  /**
9
29
  * ChartScene 与 KLineCharts 之间的唯一边界。
10
30
  * 引擎对象和内部 ID 永不从该类的公共接口泄露。
@@ -16,7 +36,7 @@ export class KLineChartsSceneAdapter {
16
36
  #engine;
17
37
  /** 场景 ID 与引擎内部 ID 的双向映射。 */
18
38
  #idMap;
19
- /** 当前可导出的规范化场景。 */
39
+ /** 当前最后一次成功提交、可导出的规范化场景。 */
20
40
  #scene;
21
41
  /** 当前引擎容器。 */
22
42
  #container;
@@ -26,6 +46,14 @@ export class KLineChartsSceneAdapter {
26
46
  #disposed = false;
27
47
  /** 仅传递纯场景数据的事件订阅者。 */
28
48
  #listeners = new Set();
49
+ /** 当前选择状态,null 表示明确未选择。 */
50
+ #selectedOverlayId = null;
51
+ /** 当前受控拖动事务;progress 永不写入 #scene。 */
52
+ #pointerInteraction;
53
+ /** 当前交互式量度的首锚点;只用于补偿引擎丢弃快速第二击,不进入 Scene。 */
54
+ #interactivePriceMeasurement;
55
+ /** 确定性 opaque 交互 ID 序号。 */
56
+ #interactionSequence = 0;
29
57
  constructor(container, scene, handle, idMap, originalBackground) {
30
58
  this.#container = container;
31
59
  this.#scene = scene;
@@ -33,6 +61,7 @@ export class KLineChartsSceneAdapter {
33
61
  this.#engine = handle.module;
34
62
  this.#idMap = idMap;
35
63
  this.#originalBackground = originalBackground;
64
+ this.#installInteractionListeners();
36
65
  }
37
66
  static async create(container, value) {
38
67
  const scene = parseChartScene(value);
@@ -75,6 +104,17 @@ export class KLineChartsSceneAdapter {
75
104
  listener(structuredClone(event));
76
105
  }
77
106
  }
107
+ #selectOverlay(id) {
108
+ const previousId = this.#selectedOverlayId;
109
+ if (previousId === id) {
110
+ return;
111
+ }
112
+ this.#selectedOverlayId = id;
113
+ this.#emit({ type: 'overlay-selection-changed', previousId, id });
114
+ if (id !== null) {
115
+ this.#emit({ type: 'overlay-selected', id });
116
+ }
117
+ }
78
118
  #commitEngineOverlay(engineOverlay, source, kind) {
79
119
  const existingIndex = this.#scene.overlays.findIndex((overlay) => overlay.id === source.id);
80
120
  const path = existingIndex < 0 ? `/overlays/${this.#scene.overlays.length}` : `/overlays/${existingIndex}`;
@@ -99,20 +139,47 @@ export class KLineChartsSceneAdapter {
99
139
  #overlayCallbacks(source, drawing = false) {
100
140
  return {
101
141
  onDrawEnd: ({ overlay }) => {
142
+ if (drawing &&
143
+ this.#interactivePriceMeasurement?.source.id === source.id) {
144
+ this.#interactivePriceMeasurement = undefined;
145
+ }
102
146
  this.#safelyCommitEngineOverlay(overlay, source, drawing ? 'created' : 'updated');
103
147
  },
148
+ onPressedMoveStart: ({ overlay }) => {
149
+ this.#selectOverlay(overlay.id);
150
+ },
104
151
  onPressedMoveEnd: ({ overlay }) => {
105
- this.#safelyCommitEngineOverlay(overlay, source, 'updated');
152
+ if (!isControlledInteractionOverlay(source)) {
153
+ this.#safelyCommitEngineOverlay(overlay, source, 'updated');
154
+ }
106
155
  },
107
156
  onSelected: ({ overlay }) => {
108
- this.#emit({ type: 'overlay-selected', id: overlay.id });
157
+ this.#selectOverlay(overlay.id);
158
+ },
159
+ onDeselected: (event) => {
160
+ const eventX = event.x;
161
+ const eventY = event.y;
162
+ const coordinate = typeof eventX === 'number' && Number.isFinite(eventX) &&
163
+ typeof eventY === 'number' && Number.isFinite(eventY)
164
+ ? { x: eventX, y: eventY }
165
+ : undefined;
166
+ if (this.#selectedOverlayId === event.overlay.id &&
167
+ !shouldIgnoreStaleOverlayDeselection(this.#selectedOverlayId, event.overlay.id, coordinate, this.#overlayGeometries())) {
168
+ this.#selectOverlay(null);
169
+ }
109
170
  },
110
171
  onRemoved: ({ overlay }) => {
172
+ if (this.#interactivePriceMeasurement?.source.id === overlay.id) {
173
+ this.#interactivePriceMeasurement = undefined;
174
+ }
111
175
  if (this.#scene.overlays.some((candidate) => candidate.id === overlay.id)) {
112
176
  this.#scene = parseChartScene({
113
177
  ...structuredClone(this.#scene),
114
178
  overlays: this.#scene.overlays.filter((candidate) => candidate.id !== overlay.id),
115
179
  });
180
+ if (this.#selectedOverlayId === overlay.id) {
181
+ this.#selectOverlay(null);
182
+ }
116
183
  this.#emit({ type: 'overlay-removed', id: overlay.id });
117
184
  }
118
185
  },
@@ -130,6 +197,334 @@ export class KLineChartsSceneAdapter {
130
197
  throw error;
131
198
  }
132
199
  }
200
+ #installInteractionListeners() {
201
+ this.#container.addEventListener('pointerdown', this.#handlePointerDown, true);
202
+ this.#container.addEventListener('pointermove', this.#handlePointerMove, true);
203
+ this.#container.addEventListener('pointerup', this.#handlePointerUp, true);
204
+ this.#container.addEventListener('pointercancel', this.#handlePointerCancel, true);
205
+ window.addEventListener('keydown', this.#handleKeyDown);
206
+ window.addEventListener('blur', this.#handleWindowBlur);
207
+ }
208
+ #removeInteractionListeners() {
209
+ this.#container.removeEventListener('pointerdown', this.#handlePointerDown, true);
210
+ this.#container.removeEventListener('pointermove', this.#handlePointerMove, true);
211
+ this.#container.removeEventListener('pointerup', this.#handlePointerUp, true);
212
+ this.#container.removeEventListener('pointercancel', this.#handlePointerCancel, true);
213
+ window.removeEventListener('keydown', this.#handleKeyDown);
214
+ window.removeEventListener('blur', this.#handleWindowBlur);
215
+ }
216
+ #pointerCoordinate(event) {
217
+ const rect = this.#container.getBoundingClientRect();
218
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
219
+ }
220
+ #primaryAxisFilter(paneId) {
221
+ const pane = this.#scene.panes.find((candidate) => candidate.id === paneId);
222
+ const axis = pane?.yAxes.find((candidate) => candidate.role === 'primary');
223
+ const enginePaneId = this.#idMap.paneToEngine.get(paneId);
224
+ const engineAxisId = axis === undefined ? undefined : this.#idMap.yAxisToEngine.get(axis.id);
225
+ if (enginePaneId === undefined || engineAxisId === undefined) {
226
+ throw new SceneError('INVALID_REFERENCE', '/panes', 'Overlay Pane or primary Y-axis is unmapped.');
227
+ }
228
+ return { paneId: enginePaneId, yAxisId: engineAxisId, absolute: true };
229
+ }
230
+ #toPixel(point, paneId) {
231
+ const converted = this.#chart.convertToPixel(point, this.#primaryAxisFilter(paneId));
232
+ if (!Number.isFinite(converted.x) || !Number.isFinite(converted.y)) {
233
+ throw new SceneError('EXPORT_INVALID', '/overlays', 'KLineCharts returned a non-finite pixel coordinate.');
234
+ }
235
+ return { x: converted.x, y: converted.y };
236
+ }
237
+ #fromPixel(point, paneId) {
238
+ const converted = this.#chart.convertFromPixel([point], this.#primaryAxisFilter(paneId));
239
+ const value = converted[0];
240
+ if (!Number.isFinite(value?.dataIndex) || !Number.isFinite(value?.value)) {
241
+ throw new SceneError('INVALID_REFERENCE', '/overlays', 'Pointer does not map to finite chart data.');
242
+ }
243
+ return { dataIndex: value.dataIndex, value: value.value };
244
+ }
245
+ #measurementAnchor(point, paneId, path) {
246
+ const converted = this.#chart.convertFromPixel([point], this.#primaryAxisFilter(paneId));
247
+ const value = converted[0];
248
+ if (!Number.isSafeInteger(value?.timestamp) ||
249
+ !Number.isFinite(value?.value) ||
250
+ !this.#scene.data.some((bar) => bar.timestamp === value?.timestamp)) {
251
+ throw new SceneError('INVALID_REFERENCE', path, 'Pointer does not map to a finite price and an embedded market-data timestamp.');
252
+ }
253
+ return {
254
+ timestamp: value.timestamp,
255
+ value: normalizePriceValue(value.value, this.#scene.symbol.pricePrecision, `${path}/value`),
256
+ };
257
+ }
258
+ #completeInteractivePriceMeasurement(drawing, end) {
259
+ const { text: _text, ...source } = drawing.source;
260
+ const candidate = parseChartScene({
261
+ ...structuredClone(this.#scene),
262
+ overlays: [
263
+ ...structuredClone(this.#scene.overlays),
264
+ {
265
+ ...structuredClone(source),
266
+ start: structuredClone(drawing.start),
267
+ end: structuredClone(end),
268
+ },
269
+ ],
270
+ });
271
+ const index = candidate.overlays.length - 1;
272
+ const overlay = candidate.overlays[index];
273
+ this.#interactivePriceMeasurement = undefined;
274
+ if (!this.#chart.removeOverlay({ id: overlay.id })) {
275
+ throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to replace in-progress Overlay ${overlay.id}.`);
276
+ }
277
+ const result = this.#chart.createOverlay(toEngineOverlay(overlay, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(overlay)));
278
+ if (result !== overlay.id) {
279
+ throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to commit interactive Overlay ${overlay.id}.`);
280
+ }
281
+ this.#scene = candidate;
282
+ this.#emit({ type: 'overlay-created', overlay });
283
+ }
284
+ #overlayGeometries() {
285
+ const geometries = [];
286
+ for (let sceneIndex = 0; sceneIndex < this.#scene.overlays.length; sceneIndex++) {
287
+ const overlay = this.#scene.overlays[sceneIndex];
288
+ if (overlay === undefined || !overlay.visible) {
289
+ continue;
290
+ }
291
+ if (overlay.type === 'horizontalStraightLine') {
292
+ const anchor = overlay.anchor;
293
+ if (anchor === undefined || !('value' in anchor)) {
294
+ continue;
295
+ }
296
+ const paneFilter = this.#primaryAxisFilter(overlay.paneId);
297
+ const paneMain = this.#chart.getDom(paneFilter.paneId, 'main');
298
+ const containerRect = this.#container.getBoundingClientRect();
299
+ const mainRect = paneMain?.getBoundingClientRect() ?? containerRect;
300
+ const projected = this.#toPixel({ timestamp: this.#scene.data[0].timestamp, value: anchor.value }, overlay.paneId);
301
+ const start = { x: mainRect.left - containerRect.left, y: projected.y };
302
+ const end = { x: mainRect.right - containerRect.left, y: projected.y };
303
+ geometries.push({
304
+ overlayId: overlay.id,
305
+ sceneIndex,
306
+ zLevel: overlay.zLevel,
307
+ locked: overlay.locked,
308
+ anchors: [{ x: (start.x + end.x) / 2, y: projected.y }],
309
+ bodySegments: [[start, end]],
310
+ });
311
+ continue;
312
+ }
313
+ if (overlay.type === 'priceMeasurement' && overlay.start !== undefined && overlay.end !== undefined) {
314
+ const start = this.#toPixel(overlay.start, overlay.paneId);
315
+ const end = this.#toPixel(overlay.end, overlay.paneId);
316
+ geometries.push({
317
+ overlayId: overlay.id,
318
+ sceneIndex,
319
+ zLevel: overlay.zLevel,
320
+ locked: overlay.locked,
321
+ anchors: [start, end],
322
+ bodySegments: [[start, end]],
323
+ });
324
+ continue;
325
+ }
326
+ if (overlay.points !== undefined && overlay.points.length >= 2) {
327
+ const anchors = overlay.points.map((point) => this.#toPixel(point, overlay.paneId));
328
+ const bodySegments = [];
329
+ for (let pointIndex = 1; pointIndex < anchors.length; pointIndex++) {
330
+ bodySegments.push([anchors[pointIndex - 1], anchors[pointIndex]]);
331
+ }
332
+ geometries.push({
333
+ overlayId: overlay.id,
334
+ sceneIndex,
335
+ zLevel: overlay.zLevel,
336
+ locked: overlay.locked,
337
+ anchors,
338
+ bodySegments,
339
+ });
340
+ }
341
+ }
342
+ return geometries;
343
+ }
344
+ #interactionIdentity(interaction) {
345
+ return {
346
+ interactionId: interaction.interactionId,
347
+ overlayId: interaction.hit.overlayId,
348
+ target: interaction.hit.target,
349
+ anchorIndex: interaction.hit.anchorIndex,
350
+ before: structuredClone(interaction.before),
351
+ };
352
+ }
353
+ #stopPointerCapture(interaction) {
354
+ if (this.#container.hasPointerCapture(interaction.pointerId)) {
355
+ this.#container.releasePointerCapture(interaction.pointerId);
356
+ }
357
+ }
358
+ #cancelInteraction(reason, error) {
359
+ const interaction = this.#pointerInteraction;
360
+ if (interaction === undefined) {
361
+ return;
362
+ }
363
+ this.#pointerInteraction = undefined;
364
+ this.#stopPointerCapture(interaction);
365
+ if (!interaction.started) {
366
+ return;
367
+ }
368
+ const index = this.#scene.overlays.findIndex((overlay) => overlay.id === interaction.before.id);
369
+ if (index < 0 ||
370
+ !this.#chart.overrideOverlay(toEngineOverlay(interaction.before, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(interaction.before)))) {
371
+ throw new SceneError('RUNTIME_INIT_FAILED', '/overlays', `KLineCharts failed to restore Overlay ${interaction.before.id}.`);
372
+ }
373
+ this.#emit({
374
+ type: 'overlay-drag-cancelled',
375
+ ...this.#interactionIdentity(interaction),
376
+ reason,
377
+ });
378
+ if (reason === 'validation-error' && error !== undefined) {
379
+ this.#emit({ type: 'scene-error', issues: structuredClone(error.issues) });
380
+ }
381
+ }
382
+ #handlePointerDown = (event) => {
383
+ if (this.#disposed || event.button !== 0 || this.#pointerInteraction !== undefined) {
384
+ return;
385
+ }
386
+ const coordinate = this.#pointerCoordinate(event);
387
+ const measurement = this.#interactivePriceMeasurement;
388
+ if (measurement !== undefined) {
389
+ try {
390
+ const point = this.#measurementAnchor(coordinate, measurement.source.paneId, measurement.start === undefined ? '/overlays/start' : '/overlays/end');
391
+ if (measurement.start === undefined) {
392
+ measurement.start = point;
393
+ return;
394
+ }
395
+ event.preventDefault();
396
+ event.stopImmediatePropagation();
397
+ this.#completeInteractivePriceMeasurement(measurement, point);
398
+ return;
399
+ }
400
+ catch (error) {
401
+ if (error instanceof SceneError) {
402
+ event.preventDefault();
403
+ event.stopImmediatePropagation();
404
+ this.#emit({ type: 'scene-error', issues: structuredClone(error.issues) });
405
+ return;
406
+ }
407
+ throw error;
408
+ }
409
+ }
410
+ const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
411
+ if (hit === null) {
412
+ const selected = this.#scene.overlays.find((overlay) => overlay.id === this.#selectedOverlayId);
413
+ if (selected !== undefined && isControlledInteractionOverlay(selected)) {
414
+ this.#selectOverlay(null);
415
+ }
416
+ return;
417
+ }
418
+ const before = this.#scene.overlays.find((overlay) => overlay.id === hit.overlayId);
419
+ if (before === undefined) {
420
+ return;
421
+ }
422
+ this.#selectOverlay(before.id);
423
+ if (!isControlledInteractionOverlay(before)) {
424
+ return;
425
+ }
426
+ event.preventDefault();
427
+ event.stopImmediatePropagation();
428
+ if (hit.locked) {
429
+ return;
430
+ }
431
+ this.#container.setPointerCapture(event.pointerId);
432
+ this.#pointerInteraction = {
433
+ pointerId: event.pointerId,
434
+ originClient: coordinate,
435
+ originData: this.#fromPixel(coordinate, before.paneId),
436
+ hit,
437
+ before: structuredClone(before),
438
+ interactionId: `interaction-${this.#interactionSequence++}`,
439
+ started: false,
440
+ };
441
+ };
442
+ #handlePointerMove = (event) => {
443
+ const interaction = this.#pointerInteraction;
444
+ if (interaction === undefined || interaction.pointerId !== event.pointerId) {
445
+ return;
446
+ }
447
+ event.preventDefault();
448
+ event.stopImmediatePropagation();
449
+ const coordinate = this.#pointerCoordinate(event);
450
+ if (!interaction.started &&
451
+ Math.hypot(coordinate.x - interaction.originClient.x, coordinate.y - interaction.originClient.y) < 0.5) {
452
+ return;
453
+ }
454
+ if (!interaction.started) {
455
+ interaction.started = true;
456
+ this.#emit({
457
+ type: 'overlay-drag-started',
458
+ ...this.#interactionIdentity(interaction),
459
+ });
460
+ }
461
+ try {
462
+ const candidate = createDragCandidate(interaction.before, interaction.hit, interaction.originData, this.#fromPixel(coordinate, interaction.before.paneId), this.#scene.data.map((bar) => bar.timestamp), this.#scene.symbol.pricePrecision);
463
+ const index = this.#scene.overlays.findIndex((overlay) => overlay.id === candidate.id);
464
+ const overlays = structuredClone(this.#scene.overlays);
465
+ overlays[index] = candidate;
466
+ const parsed = parseChartScene({ ...structuredClone(this.#scene), overlays });
467
+ const normalized = parsed.overlays[index];
468
+ if (!this.#chart.overrideOverlay(toEngineOverlay(normalized, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(normalized)))) {
469
+ throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to preview Overlay ${normalized.id}.`);
470
+ }
471
+ interaction.candidate = normalized;
472
+ this.#emit({
473
+ type: 'overlay-dragging',
474
+ ...this.#interactionIdentity(interaction),
475
+ candidate: normalized,
476
+ });
477
+ }
478
+ catch (error) {
479
+ if (error instanceof SceneError) {
480
+ this.#cancelInteraction('validation-error', error);
481
+ return;
482
+ }
483
+ throw error;
484
+ }
485
+ };
486
+ #handlePointerUp = (event) => {
487
+ const interaction = this.#pointerInteraction;
488
+ if (interaction === undefined || interaction.pointerId !== event.pointerId) {
489
+ return;
490
+ }
491
+ event.preventDefault();
492
+ event.stopImmediatePropagation();
493
+ this.#pointerInteraction = undefined;
494
+ this.#stopPointerCapture(interaction);
495
+ if (!interaction.started) {
496
+ return;
497
+ }
498
+ const overlay = interaction.candidate ?? interaction.before;
499
+ const index = this.#scene.overlays.findIndex((candidate) => candidate.id === overlay.id);
500
+ const overlays = structuredClone(this.#scene.overlays);
501
+ overlays[index] = overlay;
502
+ this.#scene = parseChartScene({ ...structuredClone(this.#scene), overlays });
503
+ const committed = this.#scene.overlays[index];
504
+ this.#emit({
505
+ type: 'overlay-drag-committed',
506
+ ...this.#interactionIdentity(interaction),
507
+ overlay: committed,
508
+ });
509
+ this.#emit({ type: 'overlay-updated', overlay: committed });
510
+ };
511
+ #handlePointerCancel = (event) => {
512
+ if (this.#pointerInteraction?.pointerId === event.pointerId) {
513
+ event.preventDefault();
514
+ event.stopImmediatePropagation();
515
+ this.#cancelInteraction('pointer-cancel');
516
+ }
517
+ };
518
+ #handleKeyDown = (event) => {
519
+ if (event.key === 'Escape' && this.#pointerInteraction !== undefined) {
520
+ this.#cancelInteraction('escape');
521
+ }
522
+ };
523
+ #handleWindowBlur = () => {
524
+ if (this.#pointerInteraction !== undefined) {
525
+ this.#cancelInteraction('window-blur');
526
+ }
527
+ };
133
528
  subscribe(listener) {
134
529
  this.#assertActive();
135
530
  this.#listeners.add(listener);
@@ -139,24 +534,60 @@ export class KLineChartsSceneAdapter {
139
534
  }
140
535
  exportScene() {
141
536
  this.#assertActive();
142
- const engines = new Map(this.#engineOverlays().map((overlay) => [overlay.id, overlay]));
143
- const overlays = this.#scene.overlays.map((source, index) => {
144
- const engine = engines.get(source.id);
145
- if (engine === undefined) {
146
- throw new SceneError('EXPORT_INVALID', `/overlays/${index}`, `KLineCharts lost Overlay ${source.id}.`);
537
+ for (let index = 0; index < this.#scene.overlays.length; index++) {
538
+ const overlay = this.#scene.overlays[index];
539
+ if (!this.#engineOverlays().some((engine) => engine.id === overlay.id)) {
540
+ throw new SceneError('EXPORT_INVALID', `/overlays/${index}`, `KLineCharts lost Overlay ${overlay.id}.`);
147
541
  }
148
- return fromEngineOverlay(engine, source, this.#idMap, `/overlays/${index}`, this.#scene.symbol.pricePrecision);
149
- });
150
- return parseChartScene({
151
- ...structuredClone(this.#scene),
152
- overlays,
153
- });
542
+ }
543
+ return parseChartScene(structuredClone(this.#scene));
544
+ }
545
+ async setPriceScale(scale) {
546
+ this.#assertActive();
547
+ const candidate = parseChartScene(promoteSceneToM2(this.#scene, scale));
548
+ const paneIndex = candidate.panes.findIndex((pane) => pane.kind === 'candle');
549
+ const pane = candidate.panes[paneIndex];
550
+ const axisIndex = pane.yAxes.findIndex((axis) => axis.role === 'primary');
551
+ const axis = pane.yAxes[axisIndex];
552
+ const previousPane = this.#scene.panes[paneIndex];
553
+ const previousAxis = previousPane.yAxes[axisIndex];
554
+ const path = `/panes/${paneIndex}/yAxes/${axisIndex}`;
555
+ try {
556
+ overrideSceneYAxis(this.#chart, this.#idMap, axis, pane.id, path);
557
+ // KLineCharts batches Y-axis recreation in a microtask; await that formal
558
+ // layout boundary before making the upgraded Scene externally visible.
559
+ await Promise.resolve();
560
+ const reference = candidate.data[0];
561
+ this.#toPixel({ timestamp: reference.timestamp, value: reference.close }, pane.id);
562
+ }
563
+ catch (error) {
564
+ overrideSceneYAxis(this.#chart, this.#idMap, previousAxis, previousPane.id, path);
565
+ await Promise.resolve();
566
+ if (error instanceof SceneError) {
567
+ throw error;
568
+ }
569
+ throw new SceneError('RUNTIME_INIT_FAILED', `${path}/scale`, 'KLineCharts failed to apply the requested price scale atomically.');
570
+ }
571
+ this.#scene = candidate;
572
+ return structuredClone(candidate);
573
+ }
574
+ projectPoint(point, paneId) {
575
+ this.#assertActive();
576
+ const targetPane = paneId ?? this.#scene.panes.find((pane) => pane.kind === 'candle').id;
577
+ return this.#toPixel(point, targetPane);
578
+ }
579
+ hitTestOverlay(point) {
580
+ this.#assertActive();
581
+ return hitTestOverlayGeometries(point, this.#overlayGeometries());
154
582
  }
155
583
  addOverlay(value) {
156
584
  this.#assertActive();
585
+ const baseScene = value.type === 'priceMeasurement'
586
+ ? promoteSceneToM2(this.#scene)
587
+ : structuredClone(this.#scene);
157
588
  const candidate = parseChartScene({
158
- ...structuredClone(this.#scene),
159
- overlays: [...this.#scene.overlays, structuredClone(value)],
589
+ ...baseScene,
590
+ overlays: [...baseScene.overlays, structuredClone(value)],
160
591
  });
161
592
  const index = candidate.overlays.length - 1;
162
593
  const overlay = candidate.overlays[index];
@@ -168,12 +599,13 @@ export class KLineChartsSceneAdapter {
168
599
  this.#emit({ type: 'overlay-created', overlay });
169
600
  return structuredClone(overlay);
170
601
  }
171
- updateOverlay(value) {
602
+ #updateOverlay(value, styleChange) {
172
603
  this.#assertActive();
173
604
  const index = this.#scene.overlays.findIndex((overlay) => overlay.id === value.id);
174
605
  if (index < 0) {
175
606
  throw new SceneError('INVALID_REFERENCE', '/overlays', `Overlay ${value.id} does not exist.`);
176
607
  }
608
+ const before = structuredClone(this.#scene.overlays[index]);
177
609
  const overlays = structuredClone(this.#scene.overlays);
178
610
  overlays[index] = structuredClone(value);
179
611
  const candidate = parseChartScene({
@@ -181,13 +613,26 @@ export class KLineChartsSceneAdapter {
181
613
  overlays,
182
614
  });
183
615
  const overlay = candidate.overlays[index];
184
- if (!this.#chart.overrideOverlay(toEngineOverlay(overlay, this.#idMap, `/overlays/${index}`))) {
616
+ if (!this.#chart.overrideOverlay(toEngineOverlay(overlay, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(overlay)))) {
185
617
  throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to update Overlay ${overlay.id}.`);
186
618
  }
187
619
  this.#scene = candidate;
620
+ if (styleChange) {
621
+ this.#emit({ type: 'overlay-style-changed', before, overlay });
622
+ }
188
623
  this.#emit({ type: 'overlay-updated', overlay });
189
624
  return structuredClone(overlay);
190
625
  }
626
+ updateOverlay(value) {
627
+ return this.#updateOverlay(value, false);
628
+ }
629
+ updateOverlayStyles(id, styles) {
630
+ const overlay = this.getOverlay(id);
631
+ if (overlay === undefined) {
632
+ throw new SceneError('INVALID_REFERENCE', '/overlays', `Overlay ${id} does not exist.`);
633
+ }
634
+ return this.#updateOverlay({ ...overlay, styles: structuredClone(styles) }, true);
635
+ }
191
636
  removeOverlay(id) {
192
637
  this.#assertActive();
193
638
  const index = this.#scene.overlays.findIndex((overlay) => overlay.id === id);
@@ -202,6 +647,9 @@ export class KLineChartsSceneAdapter {
202
647
  ...structuredClone(this.#scene),
203
648
  overlays: this.#scene.overlays.filter((overlay) => overlay.id !== id),
204
649
  });
650
+ if (this.#selectedOverlayId === id) {
651
+ this.#selectOverlay(null);
652
+ }
205
653
  this.#emit({ type: 'overlay-removed', id });
206
654
  }
207
655
  return true;
@@ -212,17 +660,32 @@ export class KLineChartsSceneAdapter {
212
660
  this.#engineOverlays().some((overlay) => overlay.id === request.id)) {
213
661
  throw new SceneError('DUPLICATE_ID', '/overlays/id', `Overlay ${request.id} already exists.`);
214
662
  }
663
+ const candidate = request.type === 'priceMeasurement'
664
+ ? parseChartScene(promoteSceneToM2(this.#scene))
665
+ : this.#scene;
215
666
  const result = this.#chart.createOverlay(toEngineOverlayDrawing(structuredClone(request), this.#idMap, this.#overlayCallbacks(structuredClone(request), true)));
216
667
  if (result !== request.id) {
217
668
  throw new SceneError('RUNTIME_INIT_FAILED', '/overlays', `KLineCharts failed to start drawing Overlay ${request.id}.`);
218
669
  }
670
+ this.#scene = candidate;
671
+ if (request.type === 'priceMeasurement') {
672
+ this.#interactivePriceMeasurement = {
673
+ source: {
674
+ ...structuredClone(request),
675
+ type: 'priceMeasurement',
676
+ },
677
+ };
678
+ }
219
679
  return request.id;
220
680
  }
221
681
  getOverlay(id) {
222
- return this.exportScene().overlays.find((overlay) => overlay.id === id);
682
+ this.#assertActive();
683
+ const overlay = this.#scene.overlays.find((candidate) => candidate.id === id);
684
+ return overlay === undefined ? undefined : structuredClone(overlay);
223
685
  }
224
686
  listOverlays() {
225
- return this.exportScene().overlays;
687
+ this.#assertActive();
688
+ return structuredClone(this.#scene.overlays);
226
689
  }
227
690
  inspect() {
228
691
  this.#assertActive();
@@ -241,6 +704,7 @@ export class KLineChartsSceneAdapter {
241
704
  });
242
705
  return {
243
706
  engineVersion: this.#engine.version(),
707
+ runtimeVersion: this.#scene.runtime.runtimeVersion,
244
708
  dataCount: this.#chart.getDataList().length,
245
709
  paneIds: this.#scene.panes.map((pane) => pane.id),
246
710
  indicators,
@@ -253,7 +717,10 @@ export class KLineChartsSceneAdapter {
253
717
  if (this.#disposed) {
254
718
  return;
255
719
  }
720
+ this.#cancelInteraction('destroy');
721
+ this.#interactivePriceMeasurement = undefined;
256
722
  this.#disposed = true;
723
+ this.#removeInteractionListeners();
257
724
  this.#listeners.clear();
258
725
  this.#engine.dispose(this.#container);
259
726
  this.#container.replaceChildren();
@@ -18,9 +18,12 @@ export interface OverlayDrawingSource extends OverlaySourceSnapshot {
18
18
  }
19
19
  export interface EngineOverlayCallbacks {
20
20
  readonly onDrawEnd?: NonNullable<OverlayCreate['onDrawEnd']>;
21
+ readonly onPressedMoveStart?: NonNullable<OverlayCreate['onPressedMoveStart']>;
22
+ readonly onPressedMoving?: NonNullable<OverlayCreate['onPressedMoving']>;
21
23
  readonly onPressedMoveEnd?: NonNullable<OverlayCreate['onPressedMoveEnd']>;
22
24
  readonly onRemoved?: NonNullable<OverlayCreate['onRemoved']>;
23
25
  readonly onSelected?: NonNullable<OverlayCreate['onSelected']>;
26
+ readonly onDeselected?: NonNullable<OverlayCreate['onDeselected']>;
24
27
  }
25
28
  export declare function toEngineOverlay(overlay: SceneOverlay, idMap: EngineIdMap, path: string, callbacks?: EngineOverlayCallbacks): OverlayCreate;
26
29
  /** 创建尚无几何点的交互式 Overlay,不把临时状态写入 Scene。 */
@@ -1 +1 @@
1
- {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../src/conversion/overlays.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,YAAY,EACZ,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAEX,OAAO,EACP,aAAa,EAGb,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC;CAC/D;AAmGD,wBAAgB,eAAe,CAC9B,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,sBAA2B,GACpC,aAAa,CAwBf;AAED,0CAA0C;AAC1C,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,sBAAsB,GAC/B,aAAa,CAsBf;AAiED,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GACpB,YAAY,CAiHd;AAED,qBAAqB;AACrB,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE;IAAE,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CAAE,EACpF,KAAK,EAAE,WAAW,EAClB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,sBAAsB,GAC3D,IAAI,CAiBN"}
1
+ {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../src/conversion/overlays.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,YAAY,EACZ,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAEX,OAAO,EACP,aAAa,EAGb,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,oBAAqB,SAAQ,qBAAqB;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,kBAAkB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAC/E,QAAQ,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,UAAU,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC;CACnE;AAoGD,wBAAgB,eAAe,CAC9B,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,sBAA2B,GACpC,aAAa,CAwBf;AAED,0CAA0C;AAC1C,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,sBAAsB,GAC/B,aAAa,CAsBf;AAiED,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GACpB,YAAY,CAkHd;AAED,qBAAqB;AACrB,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE;IAAE,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CAAE,EACpF,KAAK,EAAE,WAAW,EAClB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,sBAAsB,GAC3D,IAAI,CAiBN"}