@363045841yyt/klinechart-core 0.8.10-alpha.2 → 0.8.10

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 (89) hide show
  1. package/dist/controllers/index.d.ts +1 -1
  2. package/dist/controllers/index.d.ts.map +1 -1
  3. package/dist/controllers/index.js.map +1 -1
  4. package/dist/data-fetchers/dataBuffer.d.ts +0 -1
  5. package/dist/data-fetchers/dataBuffer.d.ts.map +1 -1
  6. package/dist/data-fetchers/dataBuffer.effects.d.ts +21 -0
  7. package/dist/data-fetchers/dataBuffer.effects.d.ts.map +1 -0
  8. package/dist/data-fetchers/dataBuffer.effects.js +55 -0
  9. package/dist/data-fetchers/dataBuffer.effects.js.map +1 -0
  10. package/dist/data-fetchers/dataBuffer.js +58 -93
  11. package/dist/data-fetchers/dataBuffer.js.map +1 -1
  12. package/dist/data-fetchers/index.d.ts +1 -0
  13. package/dist/data-fetchers/index.d.ts.map +1 -1
  14. package/dist/data-fetchers/index.js +1 -0
  15. package/dist/data-fetchers/index.js.map +1 -1
  16. package/dist/data-fetchers/timeShareBuffer.d.ts +2 -1
  17. package/dist/data-fetchers/timeShareBuffer.d.ts.map +1 -1
  18. package/dist/data-fetchers/timeShareBuffer.js +36 -14
  19. package/dist/data-fetchers/timeShareBuffer.js.map +1 -1
  20. package/dist/engine/data/chartDataManager.d.ts.map +1 -1
  21. package/dist/engine/data/chartDataManager.js +2 -1
  22. package/dist/engine/data/chartDataManager.js.map +1 -1
  23. package/dist/engine/drawing/AnchorCollector.d.ts +26 -0
  24. package/dist/engine/drawing/AnchorCollector.d.ts.map +1 -0
  25. package/dist/engine/drawing/AnchorCollector.js +47 -0
  26. package/dist/engine/drawing/AnchorCollector.js.map +1 -0
  27. package/dist/engine/drawing/DragHandler.d.ts +38 -0
  28. package/dist/engine/drawing/DragHandler.d.ts.map +1 -0
  29. package/dist/engine/drawing/DragHandler.js +92 -0
  30. package/dist/engine/drawing/DragHandler.js.map +1 -0
  31. package/dist/engine/drawing/DrawingState.d.ts +51 -0
  32. package/dist/engine/drawing/DrawingState.d.ts.map +1 -0
  33. package/dist/engine/drawing/DrawingState.js +115 -0
  34. package/dist/engine/drawing/DrawingState.js.map +1 -0
  35. package/dist/engine/drawing/HitTester.d.ts +59 -0
  36. package/dist/engine/drawing/HitTester.d.ts.map +1 -0
  37. package/dist/engine/drawing/HitTester.js +219 -0
  38. package/dist/engine/drawing/HitTester.js.map +1 -0
  39. package/dist/engine/drawing/PreviewRenderer.d.ts +26 -0
  40. package/dist/engine/drawing/PreviewRenderer.d.ts.map +1 -0
  41. package/dist/engine/drawing/PreviewRenderer.js +131 -0
  42. package/dist/engine/drawing/PreviewRenderer.js.map +1 -0
  43. package/dist/engine/drawing/coordinateUtils.d.ts +57 -0
  44. package/dist/engine/drawing/coordinateUtils.d.ts.map +1 -0
  45. package/dist/engine/drawing/coordinateUtils.js +103 -0
  46. package/dist/engine/drawing/coordinateUtils.js.map +1 -0
  47. package/dist/engine/drawing/index.d.ts.map +1 -1
  48. package/dist/engine/drawing/index.js +11 -3
  49. package/dist/engine/drawing/index.js.map +1 -1
  50. package/dist/engine/drawing/interaction.d.ts +44 -40
  51. package/dist/engine/drawing/interaction.d.ts.map +1 -1
  52. package/dist/engine/drawing/interaction.js +132 -571
  53. package/dist/engine/drawing/interaction.js.map +1 -1
  54. package/dist/engine/drawing/toolConfig.d.ts +24 -0
  55. package/dist/engine/drawing/toolConfig.d.ts.map +1 -0
  56. package/dist/engine/drawing/toolConfig.js +76 -0
  57. package/dist/engine/drawing/toolConfig.js.map +1 -0
  58. package/dist/plugin/types.d.ts +1 -0
  59. package/dist/plugin/types.d.ts.map +1 -1
  60. package/dist/plugin/types.js.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/dist/version.d.ts.map +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/version.js.map +1 -1
  65. package/package.json +4 -1
  66. package/src/controllers/index.ts +1 -0
  67. package/src/data-fetchers/__tests__/dataBuffer.test.ts +1 -3
  68. package/src/data-fetchers/dataBuffer.effects.ts +118 -0
  69. package/src/data-fetchers/dataBuffer.ts +45 -86
  70. package/src/data-fetchers/index.ts +7 -0
  71. package/src/data-fetchers/timeShareBuffer.ts +58 -19
  72. package/src/engine/__tests__/paneRenderer.resize.test.ts +3 -0
  73. package/src/engine/__tests__/subPaneManager.test.ts +13 -3
  74. package/src/engine/data/chartDataManager.ts +2 -1
  75. package/src/engine/drawing/AnchorCollector.ts +57 -0
  76. package/src/engine/drawing/DragHandler.ts +121 -0
  77. package/src/engine/drawing/DrawingState.ts +132 -0
  78. package/src/engine/drawing/HitTester.ts +288 -0
  79. package/src/engine/drawing/PreviewRenderer.ts +157 -0
  80. package/src/engine/drawing/coordinateUtils.ts +139 -0
  81. package/src/engine/drawing/index.ts +10 -3
  82. package/src/engine/drawing/interaction.ts +177 -687
  83. package/src/engine/drawing/toolConfig.ts +103 -0
  84. package/src/engine/indicators/__tests__/chartIndicatorManager.test.ts +1 -0
  85. package/src/engine/indicators/__tests__/stateComposer.test.ts +5 -4
  86. package/src/engine/renderers/Indicator/__tests__/createSubIndicatorRenderer.test.ts +1 -0
  87. package/src/plugin/types.ts +1 -0
  88. package/src/tokens/__tests__/tokens.test.ts +2 -1
  89. package/src/version.ts +1 -1
@@ -1,615 +1,203 @@
1
- import { getPhysicalKLineConfig } from '../utils/klineConfig';
2
- import { computeLinearRegression } from './linearRegression';
3
- const ANCHOR_HIT_RADIUS = 8;
4
- const LINE_HIT_RADIUS = 6;
1
+ import { DrawingState } from './DrawingState';
2
+ import { AnchorCollector } from './AnchorCollector';
3
+ import { PreviewRenderer } from './PreviewRenderer';
4
+ import { HitTester } from './HitTester';
5
+ import { DragHandler } from './DragHandler';
6
+ import { resolveAnchorFromPointer } from './coordinateUtils';
7
+ import { getAnchorCountForTool, getDrawingKind, CHANNEL_KINDS } from './toolConfig';
5
8
  /**
6
- * 绘图交互控制器
7
- * 封装绘图工具的交互逻辑,与 Vue 组件解耦
9
+ * 绘图交互控制器 v2 — 精简事件路由,组合子模块。
10
+ *
11
+ * ┌─────────────────────────────────────┐
12
+ * │ DrawingInteractionController │
13
+ * │ ├─ DrawingState (图元 CRUD) │
14
+ * │ ├─ AnchorCollector (锚点累积) │
15
+ * │ ├─ PreviewRenderer (预览构建) │
16
+ * │ ├─ HitTester (命中检测) │
17
+ * │ └─ DragHandler (拖拽管理) │
18
+ * └─────────────────────────────────────┘
8
19
  */
9
20
  export class DrawingInteractionController {
10
- adapter;
11
21
  activeTool = 'cursor';
12
- pendingAnchors = [];
13
- drawings = [];
22
+ adapter;
14
23
  callbacks = {};
15
- previewDrawingId = '__preview__';
16
- dragState = null;
17
- selectedDrawingId = null;
18
- // 单锚点工具列表
19
- static SINGLE_ANCHOR_TOOLS = [
20
- 'h-line',
21
- 'h-ray',
22
- 'v-line',
23
- 'crosshair-line',
24
- ];
25
- // 双锚点工具列表
26
- static DOUBLE_ANCHOR_TOOLS = [
27
- 'trend-line',
28
- 'ray',
29
- 'info-line',
30
- 'regression-channel',
31
- ];
32
- // 三锚点工具列表
33
- static TRIPLE_ANCHOR_TOOLS = [
34
- 'parallel-channel',
35
- 'flat-line',
36
- 'disjoint-channel',
37
- ];
24
+ drawingState;
25
+ anchorCollector;
26
+ previewRenderer;
27
+ hitTester;
28
+ dragHandler;
38
29
  constructor(adapter) {
39
30
  this.adapter = adapter;
40
- }
31
+ this.drawingState = new DrawingState(adapter);
32
+ this.anchorCollector = new AnchorCollector();
33
+ this.previewRenderer = new PreviewRenderer();
34
+ this.hitTester = new HitTester();
35
+ this.dragHandler = new DragHandler();
36
+ }
37
+ // ============ 配置 ============
38
+ /** 注册回调(创建/选中/工具切换事件通知) */
41
39
  setCallbacks(callbacks) {
42
40
  this.callbacks = callbacks;
43
41
  }
42
+ // ============ 工具状态 ============
43
+ /** 返回当前激活的绘图工具 ID */
44
44
  getActiveTool() {
45
45
  return this.activeTool;
46
46
  }
47
+ /**
48
+ * 切换绘图工具。
49
+ * 切换时自动清空锚点累积、移除预览、结束拖拽、清除选中,并通知外部。
50
+ */
47
51
  setTool(toolId) {
48
52
  this.activeTool = toolId;
49
- this.pendingAnchors = [];
50
- this.removePreview();
51
- this.dragState = null;
53
+ this.anchorCollector.reset();
54
+ this.drawingState.removePreview();
55
+ this.dragHandler.endDrag();
52
56
  this.setSelected(null);
53
57
  this.callbacks.onToolChange?.(toolId);
54
58
  }
59
+ // ============ 图元 CRUD(委托 DrawingState) ============
60
+ /** 返回所有图元(含预览) */
55
61
  getDrawings() {
56
- return this.drawings;
62
+ return this.drawingState.getAll();
57
63
  }
64
+ /** 整体替换图元列表 */
58
65
  setDrawings(drawings) {
59
- this.drawings = drawings;
60
- this.adapter.setDrawings(drawings);
66
+ this.drawingState.setDrawings(drawings);
61
67
  }
68
+ /** 清空锚点累积、预览、拖拽状态及所有图元 */
62
69
  clear() {
63
- this.pendingAnchors = [];
64
- this.removePreview();
65
- this.dragState = null;
66
- this.setSelected(null);
67
- }
68
- getSelectedDrawing() {
69
- if (!this.selectedDrawingId)
70
- return null;
71
- return this.drawings.find((d) => d.id === this.selectedDrawingId) ?? null;
70
+ this.anchorCollector.reset();
71
+ this.drawingState.removePreview();
72
+ this.dragHandler.endDrag();
73
+ this.drawingState.clear();
72
74
  }
75
+ /** 更新指定图元的样式(合并) */
73
76
  updateDrawingStyle(drawingId, style) {
74
- this.drawings = this.drawings.map((d) => d.id === drawingId ? { ...d, style: { ...d.style, ...style } } : d);
75
- this.adapter.setDrawings(this.drawings);
77
+ this.drawingState.updateDrawingStyle(drawingId, style);
76
78
  }
79
+ /** 删除指定图元 */
77
80
  removeDrawing(drawingId) {
78
- this.drawings = this.drawings.filter((d) => d.id !== drawingId);
79
- if (this.selectedDrawingId === drawingId) {
80
- this.setSelected(null);
81
- }
82
- this.adapter.setDrawings(this.drawings);
81
+ this.drawingState.removeDrawing(drawingId);
82
+ }
83
+ // ============ 选中状态 ============
84
+ /** 返回当前选中的图元 */
85
+ getSelectedDrawing() {
86
+ return this.drawingState.getSelected();
83
87
  }
88
+ // ============ 事件处理 ============
84
89
  /**
85
- * 处理指针移动事件
86
- * @returns 是否处理了事件(阻止冒泡)
90
+ * 指针移动事件入口。
91
+ * 拖拽中 → 委托 DragHandler 更新锚点;绘图模式 → 构建预览图元。
92
+ * @returns true 表示事件已消费,需要重绘
87
93
  */
88
94
  onPointerMove(e, container) {
89
- // 拖拽已有图元
90
- if (this.dragState) {
91
- return this.handleDragMove(e, container);
95
+ // 1) 正在拖拽
96
+ if (this.dragHandler.isDragging()) {
97
+ const drawing = this.drawingState.getById(this.dragHandler.getDraggingDrawingId() ?? '');
98
+ if (!drawing) {
99
+ this.dragHandler.endDrag();
100
+ return false;
101
+ }
102
+ const updated = this.dragHandler.handleDragMove(drawing, e, container, this.adapter);
103
+ if (!updated)
104
+ return false;
105
+ this.drawingState.addOrUpdate(updated);
106
+ return true;
92
107
  }
93
- // 创建预览
108
+ // 2) 绘图工具预览
94
109
  if (this.activeTool !== 'cursor') {
95
- return this.handlePreviewMove(e, container);
110
+ const anchor = resolveAnchorFromPointer(e, container, this.adapter);
111
+ if (!anchor) {
112
+ this.drawingState.removePreview();
113
+ return false;
114
+ }
115
+ const preview = this.previewRenderer.buildPreview(this.activeTool, this.anchorCollector.pendingAnchors, anchor);
116
+ if (!preview) {
117
+ this.drawingState.removePreview();
118
+ return false;
119
+ }
120
+ this.drawingState.setPreview(preview);
121
+ return true;
96
122
  }
97
123
  return false;
98
124
  }
99
125
  /**
100
- * 处理指针按下事件
101
- * @returns 是否处理了事件(阻止冒泡)
126
+ * 指针按下事件入口。
127
+ * 光标模式 → 命中检测 + 选中 + 拖拽开始;绘图模式 → 创建或累积锚点。
128
+ * @returns true 表示事件已消费
102
129
  */
103
130
  onPointerDown(e, container) {
104
- // 光标模式:命中检测已有图元
131
+ // 光标模式:命中检测 → 选中 → 开始拖拽
105
132
  if (this.activeTool === 'cursor') {
106
133
  return this.handleCursorDown(e, container);
107
134
  }
108
- const anchor = this.resolveAnchorFromPointer(e, container);
135
+ // 绘图模式
136
+ const anchor = resolveAnchorFromPointer(e, container, this.adapter);
109
137
  if (!anchor)
110
138
  return false;
111
- // 单锚点工具:点击一次立即创建
112
- if (DrawingInteractionController.SINGLE_ANCHOR_TOOLS.includes(this.activeTool)) {
139
+ const anchorCount = getAnchorCountForTool(this.activeTool);
140
+ // 单锚点工具:点击即创建
141
+ if (anchorCount === 1) {
113
142
  this.createSingleAnchorDrawing(anchor);
114
143
  return true;
115
144
  }
116
- // 双/三锚点工具:累积锚点
117
- const isDouble = DrawingInteractionController.DOUBLE_ANCHOR_TOOLS.includes(this.activeTool);
118
- const isTriple = DrawingInteractionController.TRIPLE_ANCHOR_TOOLS.includes(this.activeTool);
119
- if (!isDouble && !isTriple)
120
- return false;
121
- this.pendingAnchors.push(anchor);
122
- const requiredAnchors = isDouble ? 2 : 3;
123
- if (this.pendingAnchors.length >= requiredAnchors) {
124
- this.createMultiAnchorDrawing(this.pendingAnchors);
125
- this.pendingAnchors = [];
145
+ // 多锚点工具:累积
146
+ if (anchorCount === 2 || anchorCount === 3) {
147
+ const result = this.anchorCollector.addAnchor(anchor, this.activeTool);
148
+ if (result) {
149
+ this.createMultiAnchorDrawing(result);
150
+ }
151
+ return true;
126
152
  }
127
- return true;
153
+ return false;
128
154
  }
129
155
  /**
130
- * 处理指针释放事件
131
- * @returns 是否处理了事件(阻止冒泡)
156
+ * 指针抬起事件入口。结束拖拽。
157
+ * @returns true 表示事件已消费
132
158
  */
133
159
  onPointerUp(_e, _container) {
134
- if (!this.dragState)
160
+ if (!this.dragHandler.isDragging())
135
161
  return false;
136
- this.dragState = null;
162
+ this.dragHandler.endDrag();
137
163
  return true;
138
164
  }
139
- // ============ 光标模式:命中检测与拖拽 ============
165
+ // ============ 私有方法 ============
166
+ /** 光标模式下指针按下:命中检测 → 选中 → 开始拖拽 */
140
167
  handleCursorDown(e, container) {
141
168
  const rect = container.getBoundingClientRect();
142
169
  const mouseX = e.clientX - rect.left;
143
170
  const mouseY = e.clientY - rect.top;
144
- const hit = this.hitTest(mouseX, mouseY);
171
+ const hit = this.hitTester.hitTest(mouseX, mouseY, this.drawingState.getNonPreview(), this.adapter);
145
172
  if (!hit) {
146
173
  this.setSelected(null);
147
174
  return false;
148
175
  }
149
176
  this.setSelected(hit.drawing);
150
- this.dragState = {
151
- drawingId: hit.drawing.id,
152
- anchorIndex: 'anchorIndex' in hit ? hit.anchorIndex : undefined,
153
- snapshot: hit.drawing.anchors.map((a) => ({ ...a })),
154
- startMouse: { x: mouseX, y: mouseY },
155
- };
177
+ this.dragHandler.startDrag(hit.drawing, 'anchorIndex' in hit ? hit.anchorIndex : undefined, mouseX, mouseY);
156
178
  return true;
157
179
  }
158
- handleDragMove(e, container) {
159
- if (!this.dragState)
160
- return false;
161
- const drawing = this.drawings.find((d) => d.id === this.dragState.drawingId);
162
- if (!drawing) {
163
- this.dragState = null;
164
- return false;
165
- }
166
- const newAnchor = this.resolveAnchorFromPointer(e, container);
167
- if (this.dragState.anchorIndex !== undefined) {
168
- // 拖拽单个锚点
169
- if (newAnchor) {
170
- const idx = this.dragState.anchorIndex;
171
- drawing.anchors[idx] = {
172
- ...drawing.anchors[idx],
173
- index: newAnchor.index,
174
- time: newAnchor.time,
175
- price: newAnchor.price,
176
- };
177
- // flat-line:第三个锚点的 index/time 始终跟随第二个锚点
178
- if (drawing.kind === 'flat-line' && idx === 1 && drawing.anchors.length >= 3) {
179
- drawing.anchors[2] = {
180
- ...drawing.anchors[2],
181
- index: newAnchor.index,
182
- time: newAnchor.time,
183
- };
184
- }
185
- }
186
- }
187
- else {
188
- // 拖拽整条线:基于鼠标偏移量移动所有锚点
189
- const rect = container.getBoundingClientRect();
190
- const mouseX = e.clientX - rect.left;
191
- const mouseY = e.clientY - rect.top;
192
- const dx = mouseX - this.dragState.startMouse.x;
193
- const dy = mouseY - this.dragState.startMouse.y;
194
- for (let i = 0; i < drawing.anchors.length; i++) {
195
- const snap = this.dragState.snapshot[i];
196
- const snapScreen = this.anchorToScreen(snap);
197
- if (!snapScreen)
198
- continue;
199
- const targetX = snapScreen.x + dx;
200
- const targetY = snapScreen.y + dy;
201
- const newFromScreen = this.screenToAnchor(targetX, targetY);
202
- if (newFromScreen) {
203
- drawing.anchors[i] = {
204
- ...drawing.anchors[i],
205
- index: newFromScreen.index,
206
- time: newFromScreen.time,
207
- price: newFromScreen.price,
208
- };
209
- }
210
- }
211
- }
212
- this.adapter.setDrawings([...this.drawings]);
213
- return true;
214
- }
215
- // ============ 预览模式 ============
216
- handlePreviewMove(e, container) {
217
- const anchor = this.resolveAnchorFromPointer(e, container);
218
- if (!anchor) {
219
- this.removePreview();
220
- return false;
221
- }
222
- const isSingle = DrawingInteractionController.SINGLE_ANCHOR_TOOLS.includes(this.activeTool);
223
- const isDouble = DrawingInteractionController.DOUBLE_ANCHOR_TOOLS.includes(this.activeTool);
224
- const isTriple = DrawingInteractionController.TRIPLE_ANCHOR_TOOLS.includes(this.activeTool);
225
- if (!isSingle && !isDouble && !isTriple)
226
- return false;
227
- let preview;
228
- if (isSingle) {
229
- preview = {
230
- id: this.previewDrawingId,
231
- kind: this.getDrawingKind(this.activeTool),
232
- paneId: 'main',
233
- visible: true,
234
- anchors: [{ id: `${this.previewDrawingId}-a`, index: anchor.index, time: anchor.time, price: anchor.price }],
235
- params: {},
236
- style: {
237
- stroke: '#2962ff',
238
- strokeWidth: 1,
239
- strokeStyle: 'dashed',
240
- },
241
- };
242
- }
243
- else if (isDouble && this.pendingAnchors.length >= 1) {
244
- preview = {
245
- id: this.previewDrawingId,
246
- kind: this.getDrawingKind(this.activeTool),
247
- paneId: 'main',
248
- visible: true,
249
- anchors: [
250
- { id: `${this.previewDrawingId}-a`, index: this.pendingAnchors[0].index, time: this.pendingAnchors[0].time, price: this.pendingAnchors[0].price },
251
- { id: `${this.previewDrawingId}-b`, index: anchor.index, time: anchor.time, price: anchor.price },
252
- ],
253
- params: this.activeTool === 'regression-channel' ? { sigma: 2 } : {},
254
- style: {
255
- stroke: '#2962ff',
256
- strokeWidth: 1,
257
- strokeStyle: 'dashed',
258
- ...(this.activeTool === 'regression-channel' ? { fillOpacity: 0.1 } : {}),
259
- },
260
- };
261
- }
262
- else if (isTriple) {
263
- if (this.pendingAnchors.length === 0)
264
- return false;
265
- if (this.pendingAnchors.length === 1) {
266
- // 修复:用 trend-line 渲染线段预览(2 个锚点),三锚点工具的 definition 需要 3 个锚点才能渲染
267
- preview = {
268
- id: this.previewDrawingId,
269
- kind: 'trend-line',
270
- paneId: 'main',
271
- visible: true,
272
- anchors: [
273
- { id: `${this.previewDrawingId}-a`, index: this.pendingAnchors[0].index, time: this.pendingAnchors[0].time, price: this.pendingAnchors[0].price },
274
- { id: `${this.previewDrawingId}-b`, index: anchor.index, time: anchor.time, price: anchor.price },
275
- ],
276
- params: {},
277
- style: {
278
- stroke: '#2962ff',
279
- strokeWidth: 1,
280
- strokeStyle: 'dashed',
281
- },
282
- };
283
- }
284
- else {
285
- const thirdAnchor = this.activeTool === 'flat-line'
286
- ? {
287
- id: `${this.previewDrawingId}-c`,
288
- index: this.pendingAnchors[1].index,
289
- time: this.pendingAnchors[1].time,
290
- price: anchor.price,
291
- }
292
- : {
293
- id: `${this.previewDrawingId}-c`,
294
- index: anchor.index,
295
- time: anchor.time,
296
- price: anchor.price,
297
- };
298
- preview = {
299
- id: this.previewDrawingId,
300
- kind: this.getDrawingKind(this.activeTool),
301
- paneId: 'main',
302
- visible: true,
303
- anchors: [
304
- { id: `${this.previewDrawingId}-a`, index: this.pendingAnchors[0].index, time: this.pendingAnchors[0].time, price: this.pendingAnchors[0].price },
305
- { id: `${this.previewDrawingId}-b`, index: this.pendingAnchors[1].index, time: this.pendingAnchors[1].time, price: this.pendingAnchors[1].price },
306
- thirdAnchor,
307
- ],
308
- params: {},
309
- style: {
310
- stroke: '#2962ff',
311
- strokeWidth: 1,
312
- strokeStyle: 'dashed',
313
- fillOpacity: 0.1,
314
- },
315
- };
316
- }
317
- }
318
- else {
319
- return false;
320
- }
321
- this.drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId);
322
- this.drawings = [...this.drawings, preview];
323
- this.adapter.setDrawings(this.drawings);
324
- return true;
325
- }
326
- // ============ 命中检测 ============
327
- hitTest(mouseX, mouseY) {
328
- const drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId && d.visible);
329
- const regressionGeometryCache = new Map();
330
- // 锚点优先
331
- for (const drawing of drawings) {
332
- // regression-channel:回归线端点也是可拖拽区域
333
- if (drawing.kind === 'regression-channel' && drawing.anchors.length >= 2) {
334
- const hit = this.hitTestRegressionEndpoints(drawing, mouseX, mouseY, regressionGeometryCache);
335
- if (hit)
336
- return hit;
337
- }
338
- for (let i = 0; i < drawing.anchors.length; i++) {
339
- const screen = this.anchorToScreen(drawing.anchors[i]);
340
- if (!screen)
341
- continue;
342
- const dist = Math.hypot(mouseX - screen.x, mouseY - screen.y);
343
- if (dist <= ANCHOR_HIT_RADIUS) {
344
- return { drawing, anchorIndex: i };
345
- }
346
- }
347
- }
348
- // 线条其次
349
- for (const drawing of drawings) {
350
- const segments = this.getDrawingLineSegments(drawing, regressionGeometryCache);
351
- for (const seg of segments) {
352
- const dist = pointToSegmentDist(mouseX, mouseY, seg.a, seg.b);
353
- if (dist <= LINE_HIT_RADIUS) {
354
- return { drawing };
355
- }
356
- }
357
- }
358
- return null;
359
- }
360
- getDrawingLineSegments(drawing, regressionGeometryCache) {
361
- const viewport = this.adapter.getViewport();
362
- if (!viewport)
363
- return [];
364
- if (drawing.kind === 'regression-channel') {
365
- return this.getRegressionChannelGeometry(drawing, regressionGeometryCache)?.segments ?? [];
366
- }
367
- // 单锚点图元:根据 kind 构造屏幕线段
368
- if (drawing.anchors.length === 1) {
369
- const screen = this.anchorToScreen(drawing.anchors[0]);
370
- if (!screen)
371
- return [];
372
- const paneInfo = this.adapter.getPaneInfo('main');
373
- if (!paneInfo)
374
- return [];
375
- const right = viewport.plotWidth;
376
- const bottom = paneInfo.height;
377
- switch (drawing.kind) {
378
- case 'horizontal-line':
379
- return [{ a: { x: 0, y: screen.y }, b: { x: right, y: screen.y } }];
380
- case 'horizontal-ray':
381
- return [{ a: screen, b: { x: right, y: screen.y } }];
382
- case 'vertical-line':
383
- return [{ a: { x: screen.x, y: 0 }, b: { x: screen.x, y: bottom } }];
384
- case 'cross-line':
385
- return [
386
- { a: { x: 0, y: screen.y }, b: { x: right, y: screen.y } },
387
- { a: { x: screen.x, y: 0 }, b: { x: screen.x, y: bottom } },
388
- ];
389
- default:
390
- return [];
391
- }
392
- }
393
- // 多锚点图元:按 kind 特殊处理
394
- const points = drawing.anchors.map((a) => this.anchorToScreen(a)).filter(Boolean);
395
- if (points.length < 2)
396
- return [];
397
- const segments = [];
398
- if (points.length === 2) {
399
- const a = points[0];
400
- const b = points[1];
401
- // 其他双锚点工具:标准线段
402
- const dx = b.x - a.x;
403
- const dy = b.y - a.y;
404
- let start = a;
405
- let end = b;
406
- const extend = this.getExtendMode(drawing);
407
- const maxLen = Math.max(viewport.plotWidth, viewport.plotHeight) * 4;
408
- if (extend === 'right' || extend === 'both') {
409
- end = { x: b.x + dx * maxLen, y: b.y + dy * maxLen };
410
- }
411
- if (extend === 'left' || extend === 'both') {
412
- start = { x: a.x - dx * maxLen, y: a.y - dy * maxLen };
413
- }
414
- segments.push({ a: start, b: end });
415
- }
416
- else if (points.length >= 3) {
417
- switch (drawing.kind) {
418
- case 'parallel-channel': {
419
- const [p1, p2, p3] = points;
420
- const dx = p2.x - p1.x;
421
- const dy = p2.y - p1.y;
422
- const p4 = { x: p3.x + dx, y: p3.y + dy };
423
- segments.push({ a: p1, b: p2 }, { a: p3, b: p4 });
424
- break;
425
- }
426
- case 'flat-line': {
427
- const [p1, p2, p3] = points;
428
- const h1 = { x: p1.x, y: p3.y };
429
- const h2 = { x: p2.x, y: p3.y };
430
- segments.push({ a: p1, b: p2 });
431
- segments.push({ a: h1, b: h2 });
432
- break;
433
- }
434
- case 'disjoint-channel': {
435
- const [p1, p2, p3] = points;
436
- const dx = p2.x - p1.x;
437
- const dy = p2.y - p1.y;
438
- const p4 = { x: p3.x + dx, y: p3.y - dy };
439
- segments.push({ a: p1, b: p2 });
440
- segments.push({ a: p3, b: p4 });
441
- break;
442
- }
443
- default:
444
- for (let i = 0; i < points.length - 1; i++) {
445
- segments.push({ a: points[i], b: points[i + 1] });
446
- }
447
- }
448
- }
449
- return segments;
450
- }
451
- /**
452
- * regression-channel 专用:回归线端点也是可拖拽的锚点区域
453
- * 回归线端点可能远离存储的锚点,需要额外检测
454
- */
455
- hitTestRegressionEndpoints(drawing, mouseX, mouseY, regressionGeometryCache) {
456
- const geometry = this.getRegressionChannelGeometry(drawing, regressionGeometryCache);
457
- if (!geometry)
458
- return null;
459
- for (const endpoint of geometry.endpoints) {
460
- const dist = Math.hypot(mouseX - endpoint.point.x, mouseY - endpoint.point.y);
461
- if (dist <= ANCHOR_HIT_RADIUS) {
462
- return { drawing, anchorIndex: endpoint.anchorIndex };
463
- }
464
- }
465
- return null;
466
- }
467
- getRegressionChannelGeometry(drawing, regressionGeometryCache) {
468
- const cached = regressionGeometryCache?.get(drawing.id);
469
- if (cached !== undefined)
470
- return cached;
471
- const data = this.adapter.getData();
472
- if (data.length === 0 || drawing.anchors.length < 2) {
473
- regressionGeometryCache?.set(drawing.id, null);
474
- return null;
475
- }
476
- const firstIndex = Math.round(drawing.anchors[0].index);
477
- const secondIndex = Math.round(drawing.anchors[1].index);
478
- const clampedFirst = Math.min(Math.max(firstIndex, 0), data.length - 1);
479
- const clampedSecond = Math.min(Math.max(secondIndex, 0), data.length - 1);
480
- const startIndex = Math.min(clampedFirst, clampedSecond);
481
- const endIndex = Math.max(clampedFirst, clampedSecond);
482
- const slice = data.slice(startIndex, endIndex + 1);
483
- const regression = computeLinearRegression(slice.map((item) => item.close));
484
- if (!regression) {
485
- regressionGeometryCache?.set(drawing.id, null);
486
- return null;
487
- }
488
- const sigma = drawing.params?.sigma ?? 2;
489
- const offset = regression.stdDev * sigma;
490
- const firstValue = regression.intercept;
491
- const lastValue = regression.intercept + regression.slope * (slice.length - 1);
492
- const middleStart = this.anchorToScreen({ id: '', index: firstIndex, price: firstValue });
493
- const middleEnd = this.anchorToScreen({ id: '', index: secondIndex, price: lastValue });
494
- const upperStart = this.anchorToScreen({ id: '', index: firstIndex, price: firstValue + offset });
495
- const upperEnd = this.anchorToScreen({ id: '', index: secondIndex, price: lastValue + offset });
496
- const lowerStart = this.anchorToScreen({ id: '', index: firstIndex, price: firstValue - offset });
497
- const lowerEnd = this.anchorToScreen({ id: '', index: secondIndex, price: lastValue - offset });
498
- const segments = [];
499
- if (middleStart && middleEnd)
500
- segments.push({ a: middleStart, b: middleEnd });
501
- if (upperStart && upperEnd)
502
- segments.push({ a: upperStart, b: upperEnd });
503
- if (lowerStart && lowerEnd)
504
- segments.push({ a: lowerStart, b: lowerEnd });
505
- const endpoints = [];
506
- if (middleStart)
507
- endpoints.push({ point: middleStart, anchorIndex: 0 });
508
- if (middleEnd)
509
- endpoints.push({ point: middleEnd, anchorIndex: 1 });
510
- if (upperStart)
511
- endpoints.push({ point: upperStart, anchorIndex: 0 });
512
- if (upperEnd)
513
- endpoints.push({ point: upperEnd, anchorIndex: 1 });
514
- if (lowerStart)
515
- endpoints.push({ point: lowerStart, anchorIndex: 0 });
516
- if (lowerEnd)
517
- endpoints.push({ point: lowerEnd, anchorIndex: 1 });
518
- const geometry = { segments, endpoints };
519
- regressionGeometryCache?.set(drawing.id, geometry);
520
- return geometry;
521
- }
522
- getExtendMode(drawing) {
523
- switch (drawing.kind) {
524
- case 'ray':
525
- return 'right';
526
- case 'extended-line':
527
- return 'both';
528
- default:
529
- return 'none';
530
- }
531
- }
532
- // ============ 坐标转换 ============
533
- anchorToScreen(anchor) {
534
- const viewport = this.adapter.getViewport();
535
- if (!viewport)
536
- return null;
537
- const { kWidth, kGap } = this.adapter.getKWidthKGap();
538
- const dpr = this.adapter.getCurrentDpr();
539
- const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr);
540
- if (!Number.isFinite(anchor.index))
541
- return null;
542
- const x = (startXPx + anchor.index * unitPx + (unitPx - 1) / 2) / dpr - viewport.scrollLeft;
543
- const y = this.adapter.priceToY('main', anchor.price);
544
- return { x, y };
545
- }
546
- screenToAnchor(screenX, screenY) {
547
- const data = this.adapter.getData();
548
- const viewport = this.adapter.getViewport();
549
- if (!viewport || data.length === 0)
550
- return null;
551
- const logicalIndex = this.adapter.getLogicalIndexAtX(screenX);
552
- if (logicalIndex === null)
553
- return null;
554
- const paneInfo = this.adapter.getPaneInfo('main');
555
- if (!paneInfo)
556
- return null;
557
- const timestamp = this.adapter.getTimestampAtLogicalIndex(logicalIndex) ?? undefined;
558
- return {
559
- index: logicalIndex,
560
- time: timestamp ?? undefined,
561
- price: this.adapter.yToPrice('main', screenY - paneInfo.top),
562
- };
563
- }
564
- // ============ 工具方法 ============
180
+ /** 设置选中图元并通知回调 */
565
181
  setSelected(drawing) {
566
- const newId = drawing?.id ?? null;
567
- if (this.selectedDrawingId === newId)
568
- return;
569
- this.selectedDrawingId = newId;
570
- this.adapter.setSelectedDrawingId(newId);
182
+ this.drawingState.setSelected(drawing);
571
183
  this.callbacks.onDrawingSelected?.(drawing);
572
184
  }
573
- removePreview() {
574
- if (!this.drawings.some((d) => d.id === this.previewDrawingId))
575
- return;
576
- this.drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId);
577
- this.adapter.setDrawings(this.drawings);
578
- }
579
- resolveAnchorFromPointer(e, container) {
580
- const data = this.adapter.getData();
581
- const viewport = this.adapter.getViewport();
582
- if (!viewport || data.length === 0)
583
- return null;
584
- const rect = container.getBoundingClientRect();
585
- const mouseX = e.clientX - rect.left;
586
- const mouseY = e.clientY - rect.top;
587
- if (mouseX < 0 || mouseY < 0 || mouseX > viewport.plotWidth || mouseY > viewport.plotHeight) {
588
- return null;
589
- }
590
- const paneInfo = this.adapter.getPaneInfo('main');
591
- if (!paneInfo)
592
- return null;
593
- if (mouseY < paneInfo.top || mouseY > paneInfo.top + paneInfo.height)
594
- return null;
595
- const logicalIndex = this.adapter.getLogicalIndexAtX(mouseX);
596
- if (logicalIndex === null)
597
- return null;
598
- const timestamp = this.adapter.getTimestampAtLogicalIndex(logicalIndex) ?? undefined;
599
- return {
600
- index: logicalIndex,
601
- time: timestamp ?? undefined,
602
- price: this.adapter.yToPrice('main', mouseY - paneInfo.top),
603
- };
604
- }
185
+ /** 单锚点工具:点击即创建图元,完成后切回光标模式 */
605
186
  createSingleAnchorDrawing(anchor) {
606
- this.drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId);
187
+ this.drawingState.removePreview();
607
188
  const drawing = {
608
189
  id: `drawing-${Date.now()}`,
609
- kind: this.getDrawingKind(this.activeTool),
190
+ kind: getDrawingKind(this.activeTool),
610
191
  paneId: 'main',
611
192
  visible: true,
612
- anchors: [{ id: `${Date.now()}-a`, index: anchor.index, time: anchor.time, price: anchor.price }],
193
+ anchors: [
194
+ {
195
+ id: `${Date.now()}-a`,
196
+ index: anchor.index,
197
+ time: anchor.time,
198
+ price: anchor.price,
199
+ },
200
+ ],
613
201
  params: {},
614
202
  style: {
615
203
  stroke: '#2962ff',
@@ -617,15 +205,15 @@ export class DrawingInteractionController {
617
205
  strokeStyle: 'solid',
618
206
  },
619
207
  };
620
- this.drawings = [...this.drawings, drawing];
621
- this.adapter.setDrawings(this.drawings);
208
+ this.drawingState.addOrUpdate(drawing);
622
209
  this.callbacks.onDrawingCreated?.(drawing);
623
210
  this.activeTool = 'cursor';
624
211
  this.callbacks.onToolChange?.('cursor');
625
212
  }
213
+ /** 多锚点工具(2-3 锚点):锚点累积满后创建图元,完成后切回光标模式 */
626
214
  createMultiAnchorDrawing(anchors) {
627
- this.drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId);
628
- const kind = this.getDrawingKind(this.activeTool);
215
+ this.drawingState.removePreview();
216
+ const kind = getDrawingKind(this.activeTool);
629
217
  const params = kind === 'regression-channel' ? { sigma: 2 } : {};
630
218
  const normalizedAnchors = kind === 'flat-line' && anchors.length >= 3
631
219
  ? [
@@ -638,7 +226,7 @@ export class DrawingInteractionController {
638
226
  },
639
227
  ]
640
228
  : anchors;
641
- const isChannel = ['parallel-channel', 'regression-channel', 'flat-line', 'disjoint-channel'].includes(kind);
229
+ const isChannel = CHANNEL_KINDS.includes(kind);
642
230
  const drawing = {
643
231
  id: `drawing-${Date.now()}`,
644
232
  kind,
@@ -658,37 +246,10 @@ export class DrawingInteractionController {
658
246
  ...(isChannel ? { fillOpacity: 0.1 } : {}),
659
247
  },
660
248
  };
661
- this.drawings = [...this.drawings, drawing];
662
- this.adapter.setDrawings(this.drawings);
249
+ this.drawingState.addOrUpdate(drawing);
663
250
  this.callbacks.onDrawingCreated?.(drawing);
664
251
  this.activeTool = 'cursor';
665
252
  this.callbacks.onToolChange?.('cursor');
666
253
  }
667
- getDrawingKind(toolId) {
668
- switch (toolId) {
669
- case 'cursor':
670
- throw new Error('cursor is not a drawing kind');
671
- case 'h-line':
672
- return 'horizontal-line';
673
- case 'h-ray':
674
- return 'horizontal-ray';
675
- case 'v-line':
676
- return 'vertical-line';
677
- case 'crosshair-line':
678
- return 'cross-line';
679
- default:
680
- return toolId;
681
- }
682
- }
683
- }
684
- function pointToSegmentDist(px, py, a, b) {
685
- const dx = b.x - a.x;
686
- const dy = b.y - a.y;
687
- const lenSq = dx * dx + dy * dy;
688
- if (lenSq === 0)
689
- return Math.hypot(px - a.x, py - a.y);
690
- let t = ((px - a.x) * dx + (py - a.y) * dy) / lenSq;
691
- t = Math.max(0, Math.min(1, t));
692
- return Math.hypot(px - (a.x + t * dx), py - (a.y + t * dy));
693
254
  }
694
255
  //# sourceMappingURL=interaction.js.map