@baron1996/klinecharts-adapter 0.2.3 → 0.4.0

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