@baron1996/klinecharts-adapter 0.3.0 → 0.4.1

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 (46) hide show
  1. package/dist/adapter.d.ts +27 -2
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adapter.js +645 -26
  4. package/dist/conversion/chart-options.d.ts.map +1 -1
  5. package/dist/conversion/chart-options.js +6 -0
  6. package/dist/conversion/overlays.d.ts +2 -1
  7. package/dist/conversion/overlays.d.ts.map +1 -1
  8. package/dist/conversion/overlays.js +1 -1
  9. package/dist/conversion/price.d.ts.map +1 -1
  10. package/dist/conversion/price.js +10 -39
  11. package/dist/drawing/engine-port.d.ts +71 -0
  12. package/dist/drawing/engine-port.d.ts.map +1 -0
  13. package/dist/drawing/engine-port.js +1 -0
  14. package/dist/drawing/interaction-normalization.d.ts +18 -0
  15. package/dist/drawing/interaction-normalization.d.ts.map +1 -0
  16. package/dist/drawing/interaction-normalization.js +61 -0
  17. package/dist/drawing/kline-projection-policy.d.ts +8 -0
  18. package/dist/drawing/kline-projection-policy.d.ts.map +1 -0
  19. package/dist/drawing/kline-projection-policy.js +69 -0
  20. package/dist/drawing/overlay-conversion.d.ts +9 -0
  21. package/dist/drawing/overlay-conversion.d.ts.map +1 -0
  22. package/dist/drawing/overlay-conversion.js +245 -0
  23. package/dist/drawing/projection-policy.d.ts +34 -0
  24. package/dist/drawing/projection-policy.d.ts.map +1 -0
  25. package/dist/drawing/projection-policy.js +10 -0
  26. package/dist/drawing/time-series-projection-policy.d.ts +6 -0
  27. package/dist/drawing/time-series-projection-policy.d.ts.map +1 -0
  28. package/dist/drawing/time-series-projection-policy.js +28 -0
  29. package/dist/engine.d.ts +2 -0
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +14 -1
  32. package/dist/extensions/price-measurement.d.ts +1 -1
  33. package/dist/extensions/price-measurement.d.ts.map +1 -1
  34. package/dist/extensions/price-measurement.js +8 -2
  35. package/dist/index.d.ts +7 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +7 -0
  38. package/dist/main-series-presentation.d.ts +35 -0
  39. package/dist/main-series-presentation.d.ts.map +1 -0
  40. package/dist/main-series-presentation.js +52 -0
  41. package/dist/time-series/adapter.d.ts +25 -2
  42. package/dist/time-series/adapter.d.ts.map +1 -1
  43. package/dist/time-series/adapter.js +889 -1
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/package.json +2 -2
package/dist/adapter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { parseChartScene, SceneError, } from '@baron1996/kline-scene-schema';
1
+ import { parseDrawableWorkspaceDocument, 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';
@@ -6,6 +6,10 @@ import { applyPanes, overrideSceneYAxis } from './conversion/panes.js';
6
6
  import { createSceneOverlays, fromEngineOverlay, toEngineOverlay, toEngineOverlayDrawing, } from './conversion/overlays.js';
7
7
  import { normalizePriceValue } from './conversion/price.js';
8
8
  import { applyViewport } from './conversion/viewport.js';
9
+ import { createStaticDataLoader } from './static-data-loader.js';
10
+ import { toOverlayStyles } from './conversion/overlays.js';
11
+ import { drawingToSceneOverlay, sceneOverlayToDrawing, } from './drawing/overlay-conversion.js';
12
+ import { MainSeriesPresentationError, presentationToSceneCandle, STANDARD_CLOSE_LINE_PRESENTATION, } from './main-series-presentation.js';
9
13
  import { createDragCandidate, } from './interaction/dragging.js';
10
14
  import { hitTestOverlayGeometries, } from './interaction/hit-testing.js';
11
15
  import { shouldIgnoreStaleOverlayDeselection } from './interaction/selection-arbitration.js';
@@ -34,6 +38,8 @@ export class KLineChartsSceneAdapter {
34
38
  #chart;
35
39
  /** 引擎模块句柄,用于版本读取和精确销毁。 */
36
40
  #engine;
41
+ /** 引擎点击仲裁显式复位;每次绘制开始前调用,保证新绘制首击独立成单击。 */
42
+ #resetClickArbitration;
37
43
  /** 场景 ID 与引擎内部 ID 的双向映射。 */
38
44
  #idMap;
39
45
  /** 当前最后一次成功提交、可导出的规范化场景。 */
@@ -54,11 +60,29 @@ export class KLineChartsSceneAdapter {
54
60
  #interactivePriceMeasurement;
55
61
  /** 确定性 opaque 交互 ID 序号。 */
56
62
  #interactionSequence = 0;
63
+ /** 显式 Workspace 模式;Legacy 与 Workspace 状态严格隔离。 */
64
+ #workspaceMode = false;
65
+ /** Workspace 模式与引擎同步的 Overlay 几何(不写入 #scene.overlays)。 */
66
+ #workspaceOverlays = [];
67
+ /** Workspace 模式权威业务 Drawing(含 granularity/target/text)。 */
68
+ #workspaceSources = new Map();
69
+ /** 公共 Drawing 端口监听器。 */
70
+ #portListeners = new Set();
71
+ /** 交互启用开关。 */
72
+ #mutationsEnabled = true;
73
+ /** 主序列回滚失败后的只能销毁终止态。 */
74
+ #terminated = false;
75
+ /** 当前拖动会话的编辑维度。 */
76
+ #interactionDimensions = {
77
+ horizontal: false,
78
+ vertical: false,
79
+ };
57
80
  constructor(container, scene, handle, idMap, originalBackground) {
58
81
  this.#container = container;
59
82
  this.#scene = scene;
60
83
  this.#chart = handle.chart;
61
84
  this.#engine = handle.module;
85
+ this.#resetClickArbitration = handle.resetClickArbitration;
62
86
  this.#idMap = idMap;
63
87
  this.#originalBackground = originalBackground;
64
88
  this.#installInteractionListeners();
@@ -91,11 +115,126 @@ export class KLineChartsSceneAdapter {
91
115
  throw error;
92
116
  }
93
117
  }
118
+ /** 显式 Workspace factory:非空 Legacy overlays 由工作区语义校验拒绝。 */
119
+ static async createWorkspace(container, value) {
120
+ const workspace = parseDrawableWorkspaceDocument(value);
121
+ if (workspace.scene.kind !== 'chart') {
122
+ throw new SceneError('INVALID_REFERENCE', '/scene/kind', 'KLineChartsSceneAdapter requires a chart Workspace Scene.');
123
+ }
124
+ const scene = workspace.scene.document;
125
+ const originalBackground = container.style.backgroundColor;
126
+ let handle;
127
+ let adapter;
128
+ try {
129
+ handle = await createEngine(container, scene);
130
+ registerProjectOverlays(handle.module.registerOverlay);
131
+ const idMap = createEngineIdMap(scene, handle.chart);
132
+ applyPanes(scene, handle.chart, idMap);
133
+ adapter = new KLineChartsSceneAdapter(container, scene, handle, idMap, originalBackground);
134
+ applyViewport(handle.chart, scene.viewport);
135
+ container.style.backgroundColor = scene.chart.layout.backgroundColor;
136
+ adapter.#workspaceMode = true;
137
+ adapter.#restoreWorkspaceDrawings(workspace.drawings.drawings.map((drawing) => snapshotOfDrawing(drawing)));
138
+ return adapter;
139
+ }
140
+ catch (error) {
141
+ if (adapter !== undefined) {
142
+ adapter.dispose();
143
+ }
144
+ else if (handle !== undefined) {
145
+ handle.module.dispose(container);
146
+ }
147
+ container.replaceChildren();
148
+ container.style.backgroundColor = originalBackground;
149
+ throw error;
150
+ }
151
+ }
94
152
  #assertActive() {
95
153
  if (this.#disposed) {
96
154
  throw new SceneError('RUNTIME_INIT_FAILED', '/', 'The Adapter has already been disposed.');
97
155
  }
98
156
  }
157
+ #assertNotTerminated() {
158
+ if (this.#terminated) {
159
+ throw new MainSeriesPresentationError('MAIN_SERIES_PRESENTATION_ROLLBACK_FAILED', '/chart/candle', 'Adapter is in a destroy-only terminal error state.');
160
+ }
161
+ }
162
+ /** 当前模式下的 Overlay 列表:Legacy 用 #scene.overlays,Workspace 用独立状态。 */
163
+ #activeOverlays() {
164
+ return this.#workspaceMode ? this.#workspaceOverlays : this.#scene.overlays;
165
+ }
166
+ /** 提交 Overlay 列表变更;Workspace 模式不触碰 #scene.overlays。 */
167
+ #setActiveOverlays(next) {
168
+ if (this.#workspaceMode) {
169
+ this.#workspaceOverlays = structuredClone(next);
170
+ this.#syncWorkspaceSources();
171
+ return;
172
+ }
173
+ this.#scene = parseChartScene({
174
+ ...structuredClone(this.#scene),
175
+ overlays: structuredClone(next),
176
+ });
177
+ }
178
+ #syncWorkspaceSources() {
179
+ const next = new Map();
180
+ for (const overlay of this.#workspaceOverlays) {
181
+ const source = this.#workspaceSources.get(overlay.id);
182
+ if (source === undefined) {
183
+ continue;
184
+ }
185
+ next.set(overlay.id, sceneOverlayToDrawing(overlay, source));
186
+ }
187
+ this.#workspaceSources = next;
188
+ }
189
+ #paneIdFor(paneRole) {
190
+ if (paneRole === 'candle') {
191
+ const pane = this.#scene.panes.find((candidate) => candidate.kind === 'candle');
192
+ if (pane === undefined) {
193
+ throw new SceneError('INVALID_REFERENCE', '/panes', 'Candle pane is missing.');
194
+ }
195
+ return pane.id;
196
+ }
197
+ if (paneRole.startsWith('indicator:')) {
198
+ const indicatorId = paneRole.slice('indicator:'.length);
199
+ const pane = this.#scene.panes.find((candidate) => candidate.indicators.some((indicator) => indicator.id === indicatorId));
200
+ if (pane === undefined) {
201
+ throw new SceneError('INVALID_REFERENCE', '/panes', `Indicator pane is missing: ${indicatorId}.`);
202
+ }
203
+ return pane.id;
204
+ }
205
+ throw new SceneError('INVALID_REFERENCE', '/drawings/target', `Cannot map pane role: ${paneRole}.`);
206
+ }
207
+ #emitPort(event) {
208
+ for (const listener of this.#portListeners) {
209
+ listener(structuredClone(event));
210
+ }
211
+ }
212
+ #restoreWorkspaceDrawings(drawings) {
213
+ for (const overlay of this.#workspaceOverlays) {
214
+ this.#chart.removeOverlay({ id: overlay.id });
215
+ }
216
+ this.#workspaceOverlays = [];
217
+ this.#workspaceSources = new Map();
218
+ for (const snapshot of drawings) {
219
+ const drawing = drawingFromSnapshot(snapshot);
220
+ this.#workspaceSources.set(drawing.id, drawing);
221
+ const overlay = drawingToSceneOverlay(drawing, this.#paneIdFor(drawing.target.paneRole));
222
+ const result = this.#chart.createOverlay(toEngineOverlay(overlay, this.#idMap, `/drawings/${this.#workspaceOverlays.length}`, this.#overlayCallbacks(overlay)));
223
+ if (result !== overlay.id) {
224
+ throw new SceneError('RUNTIME_INIT_FAILED', `/drawings/${this.#workspaceOverlays.length}`, `KLineCharts failed to restore Drawing ${overlay.id}.`);
225
+ }
226
+ this.#workspaceOverlays.push(structuredClone(overlay));
227
+ }
228
+ }
229
+ #fromPixelToData(point, paneId) {
230
+ const filter = this.#primaryAxisFilter(paneId);
231
+ const converted = this.#chart.convertFromPixel([point], filter);
232
+ const value = converted[0];
233
+ return {
234
+ ...(value?.timestamp !== undefined ? { timestamp: value.timestamp } : {}),
235
+ ...(value?.value !== undefined ? { value: value.value } : {}),
236
+ };
237
+ }
99
238
  #engineOverlays() {
100
239
  return this.#chart.getOverlays();
101
240
  }
@@ -110,27 +249,63 @@ export class KLineChartsSceneAdapter {
110
249
  return;
111
250
  }
112
251
  this.#selectedOverlayId = id;
252
+ if (this.#workspaceMode) {
253
+ this.#emitPort({
254
+ type: id === null ? 'deselected' : 'selected',
255
+ id: id ?? previousId ?? '',
256
+ });
257
+ return;
258
+ }
113
259
  this.#emit({ type: 'overlay-selection-changed', previousId, id });
114
260
  if (id !== null) {
115
261
  this.#emit({ type: 'overlay-selected', id });
116
262
  }
117
263
  }
264
+ #dimensionsForHit(overlay, hit) {
265
+ if (hit.target !== 'anchor') {
266
+ return { horizontal: true, vertical: true };
267
+ }
268
+ switch (overlay.type) {
269
+ case 'horizontalStraightLine':
270
+ case 'priceLine':
271
+ case 'simpleTag':
272
+ case 'horizontalRayLine':
273
+ case 'horizontalSegment':
274
+ return { horizontal: false, vertical: true };
275
+ case 'verticalStraightLine':
276
+ case 'verticalRayLine':
277
+ case 'verticalSegment':
278
+ return { horizontal: true, vertical: false };
279
+ default:
280
+ return { horizontal: true, vertical: true };
281
+ }
282
+ }
118
283
  #commitEngineOverlay(engineOverlay, source, kind) {
119
- const existingIndex = this.#scene.overlays.findIndex((overlay) => overlay.id === source.id);
120
- const path = existingIndex < 0 ? `/overlays/${this.#scene.overlays.length}` : `/overlays/${existingIndex}`;
121
- const currentSource = existingIndex < 0 ? source : this.#scene.overlays[existingIndex];
284
+ const active = this.#activeOverlays();
285
+ const existingIndex = active.findIndex((overlay) => overlay.id === source.id);
286
+ const path = existingIndex < 0 ? `/overlays/${active.length}` : `/overlays/${existingIndex}`;
287
+ const currentSource = existingIndex < 0 ? source : active[existingIndex];
122
288
  const overlay = fromEngineOverlay(engineOverlay, currentSource, this.#idMap, path, this.#scene.symbol.pricePrecision);
123
- const overlays = structuredClone(this.#scene.overlays);
289
+ const overlays = structuredClone(active);
124
290
  if (existingIndex < 0) {
125
291
  overlays.push(overlay);
126
292
  }
127
293
  else {
128
294
  overlays[existingIndex] = overlay;
129
295
  }
130
- this.#scene = parseChartScene({
131
- ...structuredClone(this.#scene),
132
- overlays,
133
- });
296
+ this.#setActiveOverlays(overlays);
297
+ if (this.#workspaceMode) {
298
+ const drawing = this.#workspaceSources.get(overlay.id);
299
+ if (drawing !== undefined) {
300
+ this.#emitPort({
301
+ type: kind === 'created' ? 'created' : 'updated',
302
+ id: overlay.id,
303
+ drawing: snapshotOfDrawing(sceneOverlayToDrawing(overlay, drawing)),
304
+ editDimensions: structuredClone(this.#interactionDimensions),
305
+ });
306
+ }
307
+ return;
308
+ }
134
309
  this.#emit({
135
310
  type: kind === 'created' ? 'overlay-created' : 'overlay-updated',
136
311
  overlay,
@@ -172,14 +347,16 @@ export class KLineChartsSceneAdapter {
172
347
  if (this.#interactivePriceMeasurement?.source.id === overlay.id) {
173
348
  this.#interactivePriceMeasurement = undefined;
174
349
  }
175
- if (this.#scene.overlays.some((candidate) => candidate.id === overlay.id)) {
176
- this.#scene = parseChartScene({
177
- ...structuredClone(this.#scene),
178
- overlays: this.#scene.overlays.filter((candidate) => candidate.id !== overlay.id),
179
- });
350
+ if (this.#activeOverlays().some((candidate) => candidate.id === overlay.id)) {
351
+ this.#setActiveOverlays(this.#activeOverlays().filter((candidate) => candidate.id !== overlay.id));
180
352
  if (this.#selectedOverlayId === overlay.id) {
181
353
  this.#selectOverlay(null);
182
354
  }
355
+ if (this.#workspaceMode) {
356
+ this.#workspaceSources.delete(overlay.id);
357
+ this.#emitPort({ type: 'removed', id: overlay.id });
358
+ return;
359
+ }
183
360
  this.#emit({ type: 'overlay-removed', id: overlay.id });
184
361
  }
185
362
  },
@@ -283,8 +460,9 @@ export class KLineChartsSceneAdapter {
283
460
  }
284
461
  #overlayGeometries() {
285
462
  const geometries = [];
286
- for (let sceneIndex = 0; sceneIndex < this.#scene.overlays.length; sceneIndex++) {
287
- const overlay = this.#scene.overlays[sceneIndex];
463
+ const active = this.#activeOverlays();
464
+ for (let sceneIndex = 0; sceneIndex < active.length; sceneIndex++) {
465
+ const overlay = active[sceneIndex];
288
466
  if (overlay === undefined || !overlay.visible) {
289
467
  continue;
290
468
  }
@@ -361,11 +539,12 @@ export class KLineChartsSceneAdapter {
361
539
  return;
362
540
  }
363
541
  this.#pointerInteraction = undefined;
542
+ this.#interactionDimensions = { horizontal: false, vertical: false };
364
543
  this.#stopPointerCapture(interaction);
365
544
  if (!interaction.started) {
366
545
  return;
367
546
  }
368
- const index = this.#scene.overlays.findIndex((overlay) => overlay.id === interaction.before.id);
547
+ const index = this.#activeOverlays().findIndex((overlay) => overlay.id === interaction.before.id);
369
548
  if (index < 0 ||
370
549
  !this.#chart.overrideOverlay(toEngineOverlay(interaction.before, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(interaction.before)))) {
371
550
  throw new SceneError('RUNTIME_INIT_FAILED', '/overlays', `KLineCharts failed to restore Overlay ${interaction.before.id}.`);
@@ -380,7 +559,10 @@ export class KLineChartsSceneAdapter {
380
559
  }
381
560
  }
382
561
  #handlePointerDown = (event) => {
383
- if (this.#disposed || event.button !== 0 || this.#pointerInteraction !== undefined) {
562
+ if (this.#disposed ||
563
+ event.button !== 0 ||
564
+ this.#pointerInteraction !== undefined ||
565
+ !this.#mutationsEnabled) {
384
566
  return;
385
567
  }
386
568
  const coordinate = this.#pointerCoordinate(event);
@@ -409,13 +591,13 @@ export class KLineChartsSceneAdapter {
409
591
  }
410
592
  const hit = hitTestOverlayGeometries(coordinate, this.#overlayGeometries());
411
593
  if (hit === null) {
412
- const selected = this.#scene.overlays.find((overlay) => overlay.id === this.#selectedOverlayId);
594
+ const selected = this.#activeOverlays().find((overlay) => overlay.id === this.#selectedOverlayId);
413
595
  if (selected !== undefined && isControlledInteractionOverlay(selected)) {
414
596
  this.#selectOverlay(null);
415
597
  }
416
598
  return;
417
599
  }
418
- const before = this.#scene.overlays.find((overlay) => overlay.id === hit.overlayId);
600
+ const before = this.#activeOverlays().find((overlay) => overlay.id === hit.overlayId);
419
601
  if (before === undefined) {
420
602
  return;
421
603
  }
@@ -429,6 +611,7 @@ export class KLineChartsSceneAdapter {
429
611
  return;
430
612
  }
431
613
  this.#container.setPointerCapture(event.pointerId);
614
+ this.#interactionDimensions = this.#dimensionsForHit(before, hit);
432
615
  this.#pointerInteraction = {
433
616
  pointerId: event.pointerId,
434
617
  originClient: coordinate,
@@ -460,14 +643,20 @@ export class KLineChartsSceneAdapter {
460
643
  }
461
644
  try {
462
645
  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);
646
+ const active = this.#activeOverlays();
647
+ const index = active.findIndex((overlay) => overlay.id === candidate.id);
648
+ const overlays = structuredClone(active);
465
649
  overlays[index] = candidate;
466
- const parsed = parseChartScene({ ...structuredClone(this.#scene), overlays });
467
- const normalized = parsed.overlays[index];
650
+ const normalized = this.#workspaceMode
651
+ ? candidate
652
+ : parseChartScene({ ...structuredClone(this.#scene), overlays }).overlays[index];
468
653
  if (!this.#chart.overrideOverlay(toEngineOverlay(normalized, this.#idMap, `/overlays/${index}`, this.#overlayCallbacks(normalized)))) {
469
654
  throw new SceneError('RUNTIME_INIT_FAILED', `/overlays/${index}`, `KLineCharts failed to preview Overlay ${normalized.id}.`);
470
655
  }
656
+ if (this.#workspaceMode) {
657
+ this.#setActiveOverlays(overlays);
658
+ return;
659
+ }
471
660
  interaction.candidate = normalized;
472
661
  this.#emit({
473
662
  type: 'overlay-dragging',
@@ -491,14 +680,29 @@ export class KLineChartsSceneAdapter {
491
680
  event.preventDefault();
492
681
  event.stopImmediatePropagation();
493
682
  this.#pointerInteraction = undefined;
683
+ this.#interactionDimensions = { horizontal: false, vertical: false };
494
684
  this.#stopPointerCapture(interaction);
495
685
  if (!interaction.started) {
496
686
  return;
497
687
  }
498
688
  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);
689
+ const active = this.#activeOverlays();
690
+ const index = active.findIndex((candidate) => candidate.id === overlay.id);
691
+ const overlays = structuredClone(active);
501
692
  overlays[index] = overlay;
693
+ if (this.#workspaceMode) {
694
+ this.#setActiveOverlays(overlays);
695
+ const drawing = this.#workspaceSources.get(overlay.id);
696
+ if (drawing !== undefined) {
697
+ this.#emitPort({
698
+ type: 'updated',
699
+ id: overlay.id,
700
+ drawing: snapshotOfDrawing(sceneOverlayToDrawing(overlay, drawing)),
701
+ editDimensions: structuredClone(this.#interactionDimensions),
702
+ });
703
+ }
704
+ return;
705
+ }
502
706
  this.#scene = parseChartScene({ ...structuredClone(this.#scene), overlays });
503
707
  const committed = this.#scene.overlays[index];
504
708
  this.#emit({
@@ -571,6 +775,296 @@ export class KLineChartsSceneAdapter {
571
775
  this.#scene = candidate;
572
776
  return structuredClone(candidate);
573
777
  }
778
+ /** 同结构场景替换:symbol/period/data/viewport 原子更新,失败恢复旧场景。 */
779
+ replaceScene(value) {
780
+ this.#assertActive();
781
+ const candidate = parseChartScene(value);
782
+ const paneShape = (scene) => JSON.stringify(scene.panes.map((pane) => ({
783
+ id: pane.id,
784
+ kind: pane.kind,
785
+ order: pane.order,
786
+ })));
787
+ if (paneShape(candidate) !== paneShape(this.#scene)) {
788
+ throw new SceneError('INVALID_REFERENCE', '/panes', 'Scene replacement requires an identical Pane structure.');
789
+ }
790
+ const previous = this.#scene;
791
+ const previousBackground = this.#container.style.backgroundColor;
792
+ try {
793
+ this.#chart.setSymbol({
794
+ ticker: candidate.symbol.ticker,
795
+ pricePrecision: candidate.symbol.pricePrecision,
796
+ volumePrecision: candidate.symbol.volumePrecision,
797
+ ...(candidate.symbol.name === undefined ? {} : { name: candidate.symbol.name }),
798
+ });
799
+ this.#chart.setPeriod(structuredClone(candidate.period));
800
+ this.#chart.setDataLoader(createStaticDataLoader(candidate.data));
801
+ this.#chart.resetData();
802
+ applyViewport(this.#chart, candidate.viewport);
803
+ this.#scene = candidate;
804
+ this.#container.style.backgroundColor =
805
+ candidate.chart.layout.backgroundColor;
806
+ return structuredClone(candidate);
807
+ }
808
+ catch (error) {
809
+ try {
810
+ this.#chart.setSymbol({
811
+ ticker: previous.symbol.ticker,
812
+ pricePrecision: previous.symbol.pricePrecision,
813
+ volumePrecision: previous.symbol.volumePrecision,
814
+ ...(previous.symbol.name === undefined ? {} : { name: previous.symbol.name }),
815
+ });
816
+ this.#chart.setPeriod(structuredClone(previous.period));
817
+ this.#chart.setDataLoader(createStaticDataLoader(previous.data));
818
+ this.#chart.resetData();
819
+ applyViewport(this.#chart, previous.viewport);
820
+ this.#container.style.backgroundColor = previousBackground;
821
+ }
822
+ catch {
823
+ // 回滚失败:保留原错误,Adapter 状态由调用方决定是否终止。
824
+ }
825
+ throw error;
826
+ }
827
+ }
828
+ get sceneKind() {
829
+ return 'chart';
830
+ }
831
+ restoreDrawings(drawings) {
832
+ this.#assertActive();
833
+ this.#assertNotTerminated();
834
+ if (!this.#workspaceMode) {
835
+ throw new SceneError('INVALID_REFERENCE', '/', 'Drawing port operations require the Workspace factory.');
836
+ }
837
+ this.#restoreWorkspaceDrawings(drawings);
838
+ }
839
+ startDrawing(request) {
840
+ this.#assertActive();
841
+ this.#assertNotTerminated();
842
+ const drawing = placeholderDrawing(request);
843
+ if (this.#workspaceSources.has(request.id)) {
844
+ throw new SceneError('DUPLICATE_ID', `/drawings/${request.id}`, `Drawing ${request.id} already exists.`);
845
+ }
846
+ this.#workspaceSources.set(request.id, drawing);
847
+ const overlay = drawingToSceneOverlay(drawing, this.#paneIdFor(request.target.paneRole));
848
+ const result = this.#chart.createOverlay(toEngineOverlayDrawing({
849
+ ...structuredClone(request),
850
+ ...structuredClone(overlay),
851
+ }, this.#idMap, this.#overlayCallbacks(overlay, true)));
852
+ if (result !== request.id) {
853
+ this.#workspaceSources.delete(request.id);
854
+ throw new SceneError('RUNTIME_INIT_FAILED', '/drawings', `KLineCharts failed to start Drawing ${request.id}.`);
855
+ }
856
+ return request.id;
857
+ }
858
+ listDrawings() {
859
+ this.#assertActive();
860
+ return Array.from(this.#workspaceSources.values(), (drawing) => snapshotOfDrawing(drawing));
861
+ }
862
+ getDrawing(id) {
863
+ this.#assertActive();
864
+ const drawing = this.#workspaceSources.get(id);
865
+ return drawing === undefined ? undefined : snapshotOfDrawing(drawing);
866
+ }
867
+ updateDrawingStyles(id, styles) {
868
+ this.#assertActive();
869
+ this.#assertNotTerminated();
870
+ const source = this.#workspaceSources.get(id);
871
+ const overlay = this.#workspaceOverlays.find((candidate) => candidate.id === id);
872
+ if (source === undefined || overlay === undefined) {
873
+ throw new SceneError('INVALID_REFERENCE', `/drawings/${id}`, `Drawing ${id} does not exist.`);
874
+ }
875
+ if (!this.#chart.overrideOverlay({
876
+ id,
877
+ styles: toOverlayStyles(styles),
878
+ })) {
879
+ throw new SceneError('RUNTIME_INIT_FAILED', `/drawings/${id}/styles`, `KLineCharts failed to update Drawing ${id} styles.`);
880
+ }
881
+ const updated = {
882
+ ...structuredClone(source),
883
+ styles: structuredClone(styles),
884
+ };
885
+ this.#workspaceSources.set(id, updated);
886
+ this.#emitPort({
887
+ type: 'updated',
888
+ id,
889
+ drawing: snapshotOfDrawing(updated),
890
+ editDimensions: { horizontal: false, vertical: false },
891
+ });
892
+ return snapshotOfDrawing(updated);
893
+ }
894
+ updateDrawingText(id, text) {
895
+ this.#assertActive();
896
+ this.#assertNotTerminated();
897
+ const source = this.#workspaceSources.get(id);
898
+ const overlay = this.#workspaceOverlays.find((candidate) => candidate.id === id);
899
+ if (source === undefined || overlay === undefined) {
900
+ throw new SceneError('INVALID_REFERENCE', `/drawings/${id}`, `Drawing ${id} does not exist.`);
901
+ }
902
+ if (!this.#chart.overrideOverlay({ id, extendData: text })) {
903
+ throw new SceneError('RUNTIME_INIT_FAILED', `/drawings/${id}/text`, `KLineCharts failed to update Drawing ${id} text.`);
904
+ }
905
+ const updated = withDrawingText(structuredClone(source), text);
906
+ this.#workspaceSources.set(id, updated);
907
+ this.#emitPort({
908
+ type: 'updated',
909
+ id,
910
+ drawing: snapshotOfDrawing(updated),
911
+ editDimensions: { horizontal: false, vertical: false },
912
+ });
913
+ return snapshotOfDrawing(updated);
914
+ }
915
+ removeDrawing(id) {
916
+ this.#assertActive();
917
+ this.#assertNotTerminated();
918
+ return this.#chart.removeOverlay({ id });
919
+ }
920
+ restoreDrawing(snapshot) {
921
+ this.#assertActive();
922
+ this.#assertNotTerminated();
923
+ const drawing = drawingFromSnapshot(snapshot);
924
+ const existing = this.#workspaceOverlays.find((overlay) => overlay.id === drawing.id);
925
+ const overlay = drawingToSceneOverlay(drawing, this.#paneIdFor(drawing.target.paneRole));
926
+ if (existing !== undefined) {
927
+ if (!this.#chart.overrideOverlay(toEngineOverlay(overlay, this.#idMap, `/drawings/${drawing.id}`, this.#overlayCallbacks(overlay)))) {
928
+ throw new SceneError('RUNTIME_INIT_FAILED', `/drawings/${drawing.id}`, `KLineCharts failed to restore Drawing ${drawing.id}.`);
929
+ }
930
+ return;
931
+ }
932
+ const result = this.#chart.createOverlay(toEngineOverlay(overlay, this.#idMap, `/drawings/${drawing.id}`, this.#overlayCallbacks(overlay)));
933
+ if (result !== drawing.id) {
934
+ throw new SceneError('RUNTIME_INIT_FAILED', `/drawings/${drawing.id}`, `KLineCharts failed to restore Drawing ${drawing.id}.`);
935
+ }
936
+ }
937
+ selectDrawing(id) {
938
+ this.#assertActive();
939
+ this.#selectOverlay(id);
940
+ }
941
+ hitTestDrawing(point) {
942
+ this.#assertActive();
943
+ return this.hitTestOverlay(point)?.overlayId ?? null;
944
+ }
945
+ projectToPixel(anchor, paneRole) {
946
+ this.#assertActive();
947
+ const paneId = this.#paneIdFor(paneRole);
948
+ return this.#toPixel(anchor, paneId);
949
+ }
950
+ unprojectFromPixel(point, paneRole) {
951
+ this.#assertActive();
952
+ return this.#fromPixelToData(point, this.#paneIdFor(paneRole));
953
+ }
954
+ setMutationsEnabled(enabled) {
955
+ this.#assertActive();
956
+ this.#mutationsEnabled = enabled;
957
+ }
958
+ subscribeDrawingEvents(listener) {
959
+ this.#portListeners.add(listener);
960
+ return () => {
961
+ this.#portListeners.delete(listener);
962
+ };
963
+ }
964
+ applyMainSeriesPresentation(presentation) {
965
+ this.#assertActive();
966
+ this.#assertNotTerminated();
967
+ if (presentation.type === 'area') {
968
+ const expected = STANDARD_CLOSE_LINE_PRESENTATION;
969
+ if (presentation.value !== expected.value ||
970
+ presentation.line.color !== expected.line.color ||
971
+ presentation.line.size !== expected.line.size ||
972
+ presentation.backgroundColor !== expected.backgroundColor ||
973
+ presentation.smooth !== expected.smooth ||
974
+ presentation.pointVisible !== expected.pointVisible) {
975
+ throw new MainSeriesPresentationError('MAIN_SERIES_PRESENTATION_INVALID', '/chart/candle/area', 'Area presentation must use the standard frozen close-line configuration.');
976
+ }
977
+ }
978
+ const candidate = parseChartScene(presentationToSceneCandle(this.#scene, presentation));
979
+ const oldStyles = structuredClone(this.#chart.getStyles());
980
+ const overlayBefore = this.#chart.getOverlays().map((overlay) => ({
981
+ id: overlay.id,
982
+ currentStep: overlay.currentStep,
983
+ points: structuredClone(overlay.points),
984
+ }));
985
+ const selectionBefore = this.#selectedOverlayId;
986
+ const targetType = presentation.type;
987
+ const newStyles = presentation.type === 'area'
988
+ ? {
989
+ candle: {
990
+ type: 'area',
991
+ area: {
992
+ lineColor: presentation.line.color,
993
+ lineSize: presentation.line.size,
994
+ value: presentation.value,
995
+ backgroundColor: presentation.backgroundColor,
996
+ smooth: presentation.smooth,
997
+ point: { show: false, animation: false },
998
+ },
999
+ },
1000
+ }
1001
+ : {
1002
+ candle: {
1003
+ type: presentation.type,
1004
+ },
1005
+ };
1006
+ try {
1007
+ this.#chart.setStyles(newStyles);
1008
+ }
1009
+ catch (error) {
1010
+ this.#rollbackPresentation(oldStyles, overlayBefore, selectionBefore);
1011
+ }
1012
+ if (!this.#verifyPresentation(targetType) ||
1013
+ !this.#verifyOverlaySnapshot(overlayBefore, selectionBefore)) {
1014
+ this.#rollbackPresentation(oldStyles, overlayBefore, selectionBefore);
1015
+ }
1016
+ this.#scene = candidate;
1017
+ return { activeType: targetType };
1018
+ }
1019
+ #verifyPresentation(type) {
1020
+ const styles = this.#chart.getStyles();
1021
+ if (styles.candle.type !== type) {
1022
+ return false;
1023
+ }
1024
+ if (type !== 'area') {
1025
+ return true;
1026
+ }
1027
+ const area = styles.candle.area;
1028
+ return area.value === 'close'
1029
+ && area.lineColor === 'rgba(41, 98, 255, 1)'
1030
+ && area.lineSize === 2
1031
+ && area.backgroundColor === 'rgba(0, 0, 0, 0)'
1032
+ && area.smooth === false
1033
+ && area.point.show === false;
1034
+ }
1035
+ #verifyOverlaySnapshot(before, selection) {
1036
+ const after = this.#chart.getOverlays();
1037
+ if (after.length !== before.length) {
1038
+ return false;
1039
+ }
1040
+ for (let index = 0; index < after.length; index++) {
1041
+ const left = after[index];
1042
+ const right = before[index];
1043
+ if (left.id !== right.id ||
1044
+ left.currentStep !== right.currentStep ||
1045
+ JSON.stringify(left.points) !== JSON.stringify(right.points)) {
1046
+ return false;
1047
+ }
1048
+ }
1049
+ return this.#selectedOverlayId === selection;
1050
+ }
1051
+ #rollbackPresentation(oldStyles, overlayBefore, selection) {
1052
+ let rollbackOk = false;
1053
+ try {
1054
+ this.#chart.setStyles(oldStyles);
1055
+ rollbackOk =
1056
+ JSON.stringify(this.#chart.getStyles()) === JSON.stringify(oldStyles) &&
1057
+ this.#verifyOverlaySnapshot(overlayBefore, selection);
1058
+ }
1059
+ catch {
1060
+ rollbackOk = false;
1061
+ }
1062
+ if (rollbackOk) {
1063
+ throw new MainSeriesPresentationError('MAIN_SERIES_PRESENTATION_APPLY_FAILED', '/chart/candle', 'KLineCharts failed to apply the main series presentation; the previous styles were restored.');
1064
+ }
1065
+ this.#terminated = true;
1066
+ throw new MainSeriesPresentationError('MAIN_SERIES_PRESENTATION_ROLLBACK_FAILED', '/chart/candle', 'KLineCharts failed to roll back the main series presentation; the Adapter is destroy-only.');
1067
+ }
574
1068
  projectPoint(point, paneId) {
575
1069
  this.#assertActive();
576
1070
  const targetPane = paneId ?? this.#scene.panes.find((pane) => pane.kind === 'candle').id;
@@ -660,6 +1154,10 @@ export class KLineChartsSceneAdapter {
660
1154
  this.#engineOverlays().some((overlay) => overlay.id === request.id)) {
661
1155
  throw new SceneError('DUPLICATE_ID', '/overlays/id', `Overlay ${request.id} already exists.`);
662
1156
  }
1157
+ // 下一次绘制开始前显式复位引擎点击仲裁:klinecharts 10.0.0 会在 500ms 窗口内
1158
+ // 相距 ≥5px 的第二次 mouseup 上既不派发 click 也不派发 double-click,直接丢弃该次点击,
1159
+ // 导致“上一绘制提交后紧接着的新绘制首击”被吞。复位后新绘制首击总是独立的单击。
1160
+ this.#resetClickArbitration();
663
1161
  const candidate = request.type === 'priceMeasurement'
664
1162
  ? parseChartScene(promoteSceneToM2(this.#scene))
665
1163
  : this.#scene;
@@ -722,8 +1220,129 @@ export class KLineChartsSceneAdapter {
722
1220
  this.#disposed = true;
723
1221
  this.#removeInteractionListeners();
724
1222
  this.#listeners.clear();
1223
+ this.#portListeners.clear();
1224
+ this.#workspaceSources.clear();
1225
+ this.#workspaceOverlays = [];
725
1226
  this.#engine.dispose(this.#container);
726
1227
  this.#container.replaceChildren();
727
1228
  this.#container.style.backgroundColor = this.#originalBackground;
728
1229
  }
729
1230
  }
1231
+ function snapshotOfDrawing(drawing) {
1232
+ return {
1233
+ id: drawing.id,
1234
+ type: drawing.type,
1235
+ target: structuredClone(drawing.target),
1236
+ geometry: structuredClone(drawing.geometry),
1237
+ styles: structuredClone(drawing.styles),
1238
+ locked: drawing.locked,
1239
+ visible: drawing.visible,
1240
+ zLevel: drawing.zLevel,
1241
+ mode: drawing.mode,
1242
+ };
1243
+ }
1244
+ function drawingFromSnapshot(snapshot) {
1245
+ return {
1246
+ id: snapshot.id,
1247
+ type: snapshot.type,
1248
+ target: structuredClone(snapshot.target),
1249
+ geometry: structuredClone(snapshot.geometry),
1250
+ styles: structuredClone(snapshot.styles),
1251
+ visible: snapshot.visible,
1252
+ locked: snapshot.locked,
1253
+ zLevel: snapshot.zLevel,
1254
+ mode: snapshot.mode,
1255
+ };
1256
+ }
1257
+ function placeholderGeometry(type) {
1258
+ switch (type) {
1259
+ case 'horizontalStraightLine':
1260
+ case 'priceLine':
1261
+ return { value: 0 };
1262
+ case 'simpleTag':
1263
+ return { value: 0, text: '' };
1264
+ case 'verticalStraightLine':
1265
+ return { time: 0 };
1266
+ case 'horizontalRayLine':
1267
+ case 'horizontalSegment':
1268
+ return { value: 0, startTime: 0, endTime: 0 };
1269
+ case 'verticalRayLine':
1270
+ case 'verticalSegment':
1271
+ return { time: 0, startValue: 0, endValue: 0 };
1272
+ case 'rayLine':
1273
+ case 'segment':
1274
+ case 'straightLine':
1275
+ case 'fibonacciLine':
1276
+ return {
1277
+ points: [
1278
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1279
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1280
+ ],
1281
+ };
1282
+ case 'priceChannelLine':
1283
+ case 'parallelStraightLine':
1284
+ return {
1285
+ points: [
1286
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1287
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1288
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1289
+ ],
1290
+ };
1291
+ case 'brush':
1292
+ return {
1293
+ points: [
1294
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1295
+ { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1296
+ ],
1297
+ };
1298
+ case 'simpleAnnotation':
1299
+ case 'callout':
1300
+ case 'text':
1301
+ return {
1302
+ point: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1303
+ text: '',
1304
+ };
1305
+ case 'crossLine':
1306
+ return {
1307
+ point: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1308
+ };
1309
+ case 'rectangle':
1310
+ case 'arrow':
1311
+ case 'priceMeasurement':
1312
+ return {
1313
+ start: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1314
+ end: { timestamp: 0, granularity: { type: 'day', span: 1 }, value: 0 },
1315
+ };
1316
+ }
1317
+ }
1318
+ function placeholderDrawing(request) {
1319
+ return {
1320
+ id: request.id,
1321
+ type: request.type,
1322
+ target: structuredClone(request.target),
1323
+ geometry: placeholderGeometry(request.type),
1324
+ styles: structuredClone(request.styles),
1325
+ visible: true,
1326
+ locked: false,
1327
+ zLevel: 0,
1328
+ mode: 'normal',
1329
+ };
1330
+ }
1331
+ function withDrawingText(drawing, text) {
1332
+ switch (drawing.type) {
1333
+ case 'simpleTag':
1334
+ return {
1335
+ ...structuredClone(drawing),
1336
+ geometry: { ...drawing.geometry, text },
1337
+ };
1338
+ case 'simpleAnnotation':
1339
+ case 'callout':
1340
+ case 'text':
1341
+ return {
1342
+ ...structuredClone(drawing),
1343
+ geometry: { ...drawing.geometry, text },
1344
+ };
1345
+ default:
1346
+ return structuredClone(drawing);
1347
+ }
1348
+ }