@363045841yyt/klinechart-core 0.7.5 → 0.7.6

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 (59) hide show
  1. package/README.md +8 -8
  2. package/README.zh-CN.md +8 -8
  3. package/dist/controllers/createChartController.d.ts.map +1 -1
  4. package/dist/controllers/createChartController.js +145 -21
  5. package/dist/controllers/createChartController.js.map +1 -1
  6. package/dist/controllers/index.d.ts +9 -1
  7. package/dist/controllers/index.d.ts.map +1 -1
  8. package/dist/controllers/index.js +9 -0
  9. package/dist/controllers/index.js.map +1 -1
  10. package/dist/controllers/types.d.ts +65 -8
  11. package/dist/controllers/types.d.ts.map +1 -1
  12. package/dist/engine/chart.d.ts +2 -12
  13. package/dist/engine/chart.d.ts.map +1 -1
  14. package/dist/engine/chart.js +28 -31
  15. package/dist/engine/chart.js.map +1 -1
  16. package/dist/engine/controller/interaction.d.ts +1 -1
  17. package/dist/engine/controller/interaction.d.ts.map +1 -1
  18. package/dist/engine/controller/interaction.js +10 -2
  19. package/dist/engine/controller/interaction.js.map +1 -1
  20. package/dist/engine/drawing/interaction.d.ts +3 -3
  21. package/dist/engine/drawing/interaction.d.ts.map +1 -1
  22. package/dist/engine/drawing/interaction.js +38 -46
  23. package/dist/engine/drawing/interaction.js.map +1 -1
  24. package/dist/engine/renderers/paneTitle.d.ts +5 -24
  25. package/dist/engine/renderers/paneTitle.d.ts.map +1 -1
  26. package/dist/engine/renderers/paneTitle.js +10 -5
  27. package/dist/engine/renderers/paneTitle.js.map +1 -1
  28. package/dist/engine/renderers/webgl/candleSurface.d.ts +4 -4
  29. package/dist/engine/renderers/webgl/candleSurface.d.ts.map +1 -1
  30. package/dist/engine/renderers/webgl/candleSurface.js +36 -56
  31. package/dist/engine/renderers/webgl/candleSurface.js.map +1 -1
  32. package/dist/engine/subPaneManager.d.ts +2 -0
  33. package/dist/engine/subPaneManager.d.ts.map +1 -1
  34. package/dist/engine/subPaneManager.js +25 -1
  35. package/dist/engine/subPaneManager.js.map +1 -1
  36. package/dist/semantic/controller.d.ts +1 -2
  37. package/dist/semantic/controller.d.ts.map +1 -1
  38. package/dist/semantic/index.d.ts +1 -1
  39. package/dist/semantic/index.d.ts.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +6 -6
  43. package/src/controllers/createChartController.ts +158 -29
  44. package/src/controllers/index.ts +33 -0
  45. package/src/controllers/types.ts +79 -8
  46. package/src/engine/chart.ts +28 -37
  47. package/src/engine/controller/interaction.ts +9 -2
  48. package/src/engine/drawing/interaction.ts +38 -47
  49. package/src/engine/renderers/paneTitle.ts +16 -25
  50. package/src/engine/renderers/webgl/candleSurface.ts +40 -56
  51. package/src/engine/subPaneManager.ts +28 -1
  52. package/src/semantic/controller.ts +1 -1
  53. package/src/semantic/index.ts +1 -1
  54. package/src/version.ts +1 -1
  55. package/dist/engine/chart-store.d.ts +0 -75
  56. package/dist/engine/chart-store.d.ts.map +0 -1
  57. package/dist/engine/chart-store.js +0 -88
  58. package/dist/engine/chart-store.js.map +0 -1
  59. package/src/engine/chart-store.ts +0 -121
@@ -9,7 +9,6 @@ import { PaneRenderer } from './paneRenderer'
9
9
  import { SharedWebGLSurface } from './renderers/webgl/sharedWebGLSurface'
10
10
  import { MarkerManager, type CustomMarkerEntity } from './marker/registry'
11
11
  import { getPhysicalKLineConfig, calcKWidthPx } from './utils/klineConfig'
12
- import { computeContentWidth } from './chart-store'
13
12
  import { computeZoom, computeZoomToLevel, type ZoomConfig } from './utils/zoom'
14
13
  import { IndicatorScheduler } from './indicators/scheduler'
15
14
  import { getRegisteredIndicatorDefinitions } from './indicators/indicatorDefinitionRegistry'
@@ -214,21 +213,12 @@ export class Chart {
214
213
  /** pane ratio 状态(按 paneId 维护,sum=1 仅对可见 pane) */
215
214
  private _internalPaneRatios: Map<string, number> = new Map()
216
215
 
217
- /** 视口变化回调(供外部同步 DPR/尺寸) */
218
- private onViewportChange?: (viewport: Viewport) => void
219
-
220
216
  /** 共享 X 轴上下文缓存 */
221
217
  private xAxisCtx: CanvasRenderingContext2D | null = null
222
218
 
223
219
  /** Chart 级共享 WebGL canvas/context */
224
220
  private sharedWebGLSurface: SharedWebGLSurface
225
221
 
226
- /** pane 布局回流回调(Chart -> UI 单向) */
227
- private onPaneLayoutChange?: (panes: PaneSpec[]) => void
228
-
229
- /** 数据变化回调(供外部同步 dataLength) */
230
- private onDataChange?: (data: KLineData[]) => void
231
-
232
222
  /** 当前缩放级别(1 ~ zoomLevelCount) */
233
223
  private currentZoomLevel: number = 1
234
224
 
@@ -1243,21 +1233,6 @@ export class Chart {
1243
1233
  return this.zoomLevelCount
1244
1234
  }
1245
1235
 
1246
- /** 注册视口变化回调 */
1247
- setOnViewportChange(cb: (viewport: Viewport) => void) {
1248
- this.onViewportChange = cb
1249
- }
1250
-
1251
- /** 注册 pane 布局回流回调 */
1252
- setOnPaneLayoutChange(cb: (panes: PaneSpec[]) => void) {
1253
- this.onPaneLayoutChange = cb
1254
- }
1255
-
1256
- /** 注册数据变化回调 */
1257
- setOnDataChange(cb: (data: KLineData[]) => void) {
1258
- this.onDataChange = cb
1259
- }
1260
-
1261
1236
  /** 获取所有 PaneRenderer */
1262
1237
  getPaneRenderers(): PaneRenderer[] {
1263
1238
  return this.paneRenderers
@@ -1436,7 +1411,7 @@ export class Chart {
1436
1411
  })
1437
1412
  this._paneRatiosSignal.set(ratios)
1438
1413
 
1439
- this.onPaneLayoutChange?.(this.getPaneLayoutSpecs())
1414
+ this._paneLayoutSignal.set(this.getPaneLayoutSpecs())
1440
1415
  }
1441
1416
 
1442
1417
  private applyPaneLayoutSpecs(panes: PaneSpec[]): void {
@@ -1781,7 +1756,6 @@ export class Chart {
1781
1756
  updateData(data: KLineData[]) {
1782
1757
  this._internalData = data ?? []
1783
1758
  this._dataSignal.set([...this._internalData])
1784
- this.onDataChange?.(this._internalData)
1785
1759
 
1786
1760
  // 重算 DOM scrollLeft 状态, 防止左右滚动超出数据长度范围
1787
1761
  const container = this.dom.container
@@ -1848,13 +1822,16 @@ export class Chart {
1848
1822
 
1849
1823
  /** 获取内容总宽度(用于外部 scroll-content 撑开 scrollWidth) */
1850
1824
  getContentWidth(): number {
1851
- return computeContentWidth({
1852
- dataLength: this._internalData.length,
1853
- kWidth: this.opt.kWidth,
1854
- kGap: this.opt.kGap,
1855
- viewWidth: this._internalViewport?.plotWidth ?? 0,
1856
- viewportDpr: this.getEffectiveDpr(),
1857
- })
1825
+ const dataLength = this._internalData.length
1826
+ if (dataLength === 0) return 0
1827
+ const kWidth = this.opt.kWidth
1828
+ const kGap = this.opt.kGap
1829
+ const viewWidth = this._internalViewport?.plotWidth ?? 0
1830
+ const dpr = this.getEffectiveDpr()
1831
+ const TRAILING_DRAWING_SLOTS = 24
1832
+ const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
1833
+ const dataPlotWidth = (startXPx + (dataLength + TRAILING_DRAWING_SLOTS) * unitPx) / dpr
1834
+ return Math.max(dataPlotWidth, viewWidth)
1858
1835
  }
1859
1836
 
1860
1837
 
@@ -1936,8 +1913,6 @@ export class Chart {
1936
1913
  // 清理渲染器插件管理器(会调用所有 onUninstall)
1937
1914
  this.rendererPluginManager.clear()
1938
1915
 
1939
- this.onViewportChange = undefined
1940
- this.onPaneLayoutChange = undefined
1941
1916
  this.indicatorScheduler.destroy()
1942
1917
  await this.pluginHost.destroy()
1943
1918
  }
@@ -2255,7 +2230,18 @@ export class Chart {
2255
2230
 
2256
2231
  this._internalViewport = vp
2257
2232
  if (viewportChanged) {
2258
- this.onViewportChange?.(vp)
2233
+ const current = this._viewportSignal.peek()
2234
+ this._viewportSignal.set({
2235
+ zoomLevel: current.zoomLevel,
2236
+ plotWidth: vp.plotWidth,
2237
+ plotHeight: vp.plotHeight,
2238
+ dpr: vp.dpr > 0 ? vp.dpr : current.dpr,
2239
+ visibleFrom: current.visibleFrom,
2240
+ visibleTo: current.visibleTo,
2241
+ desiredScrollLeft: current.desiredScrollLeft,
2242
+ kWidth: current.kWidth,
2243
+ kGap: current.kGap,
2244
+ })
2259
2245
  }
2260
2246
  return vp
2261
2247
  }
@@ -2280,6 +2266,7 @@ export class Chart {
2280
2266
  private _drawingToolSignal = createSignal<DrawingToolType | null>(null)
2281
2267
  private _drawingsSignal = createSignal<ReadonlyArray<import('../plugin').DrawingObject>>([])
2282
2268
  private _paneRatiosSignal = createSignal<Readonly<Record<string, number>>>({})
2269
+ private _paneLayoutSignal = createSignal<PaneSpec[]>([])
2283
2270
  private _interactionSignal = createSignal<InteractionSnapshot>({
2284
2271
  crosshairPos: null,
2285
2272
  crosshairIndex: null,
@@ -2369,6 +2356,10 @@ export class Chart {
2369
2356
  return this._paneRatiosSignal
2370
2357
  }
2371
2358
 
2359
+ get paneLayout(): Signal<PaneSpec[]> {
2360
+ return this._paneLayoutSignal
2361
+ }
2362
+
2372
2363
  /** 交互状态信号 */
2373
2364
  get interactionState(): Signal<InteractionSnapshot> {
2374
2365
  return this._interactionSignal
@@ -102,10 +102,17 @@ export class InteractionController {
102
102
 
103
103
  constructor(chart: Chart) {
104
104
  this.chart = chart
105
+ this.setupPinchZoom()
105
106
  }
106
107
 
107
- setOnPinchZoom(callback: (delta: number, centerX: number) => void) {
108
- this.pinchTracker.setOnPinchZoom(callback)
108
+ private setupPinchZoom(): void {
109
+ this.pinchTracker.setOnPinchZoom((delta, centerClientX) => {
110
+ const container = this.chart.getDom().container
111
+ if (!container) return
112
+ const rect = container.getBoundingClientRect()
113
+ const centerX = centerClientX - rect.left
114
+ this.chart.handlePinchZoom(delta, centerX)
115
+ })
109
116
  }
110
117
 
111
118
  /** 更新用户设置 */
@@ -1,5 +1,5 @@
1
1
  import type { DrawingObject, DrawingKind, DrawingAnchor, DrawingStyle } from '../../plugin'
2
- import type { Chart } from '../chart'
2
+ import type { DrawingChartAdapter } from '../../controllers/types'
3
3
  import { getPhysicalKLineConfig } from '../utils/klineConfig'
4
4
  import { computeLinearRegression } from './index'
5
5
 
@@ -55,7 +55,7 @@ const LINE_HIT_RADIUS = 6
55
55
  * 封装绘图工具的交互逻辑,与 Vue 组件解耦
56
56
  */
57
57
  export class DrawingInteractionController {
58
- private chart: Chart
58
+ private adapter: DrawingChartAdapter
59
59
  private activeTool: DrawingToolId = 'cursor'
60
60
  private pendingAnchors: DrawingAnchorInput[] = []
61
61
  private drawings: DrawingObject[] = []
@@ -87,8 +87,8 @@ export class DrawingInteractionController {
87
87
  'disjoint-channel',
88
88
  ]
89
89
 
90
- constructor(chart: Chart) {
91
- this.chart = chart
90
+ constructor(adapter: DrawingChartAdapter) {
91
+ this.adapter = adapter
92
92
  }
93
93
 
94
94
  setCallbacks(callbacks: DrawingInteractionCallbacks) {
@@ -114,7 +114,7 @@ export class DrawingInteractionController {
114
114
 
115
115
  setDrawings(drawings: DrawingObject[]) {
116
116
  this.drawings = drawings
117
- this.chart.setDrawings(drawings)
117
+ this.adapter.setDrawings(drawings)
118
118
  }
119
119
 
120
120
  clear() {
@@ -133,7 +133,7 @@ export class DrawingInteractionController {
133
133
  this.drawings = this.drawings.map((d) =>
134
134
  d.id === drawingId ? { ...d, style: { ...d.style, ...style } } : d
135
135
  )
136
- this.chart.setDrawings(this.drawings)
136
+ this.adapter.setDrawings(this.drawings)
137
137
  }
138
138
 
139
139
  removeDrawing(drawingId: string): void {
@@ -141,7 +141,7 @@ export class DrawingInteractionController {
141
141
  if (this.selectedDrawingId === drawingId) {
142
142
  this.setSelected(null)
143
143
  }
144
- this.chart.setDrawings(this.drawings)
144
+ this.adapter.setDrawings(this.drawings)
145
145
  }
146
146
 
147
147
  /**
@@ -287,7 +287,7 @@ export class DrawingInteractionController {
287
287
  }
288
288
  }
289
289
 
290
- this.chart.setDrawings([...this.drawings])
290
+ this.adapter.setDrawings([...this.drawings])
291
291
  return true
292
292
  }
293
293
 
@@ -400,7 +400,7 @@ export class DrawingInteractionController {
400
400
 
401
401
  this.drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId)
402
402
  this.drawings = [...this.drawings, preview]
403
- this.chart.setDrawings(this.drawings)
403
+ this.adapter.setDrawings(this.drawings)
404
404
  return true
405
405
  }
406
406
 
@@ -446,7 +446,7 @@ export class DrawingInteractionController {
446
446
  drawing: DrawingObject,
447
447
  regressionGeometryCache?: Map<string, RegressionChannelGeometry | null>,
448
448
  ): LineSegment[] {
449
- const viewport = this.chart.getViewport()
449
+ const viewport = this.adapter.getViewport()
450
450
  if (!viewport) return []
451
451
 
452
452
  if (drawing.kind === 'regression-channel') {
@@ -458,12 +458,11 @@ export class DrawingInteractionController {
458
458
  const screen = this.anchorToScreen(drawing.anchors[0]!)
459
459
  if (!screen) return []
460
460
 
461
- const paneRenderer = this.chart.getPaneRenderers().find((item) => item.getPane().id === 'main')
462
- const pane = paneRenderer?.getPane()
463
- if (!pane) return []
461
+ const paneInfo = this.adapter.getPaneInfo('main')
462
+ if (!paneInfo) return []
464
463
 
465
464
  const right = viewport.plotWidth
466
- const bottom = pane.height
465
+ const bottom = paneInfo.height
467
466
 
468
467
  switch (drawing.kind) {
469
468
  case 'horizontal-line':
@@ -582,7 +581,7 @@ export class DrawingInteractionController {
582
581
  const cached = regressionGeometryCache?.get(drawing.id)
583
582
  if (cached !== undefined) return cached
584
583
 
585
- const data = this.chart.getData()
584
+ const data = this.adapter.getData()
586
585
  if (data.length === 0 || drawing.anchors.length < 2) {
587
586
  regressionGeometryCache?.set(drawing.id, null)
588
587
  return null
@@ -645,21 +644,17 @@ export class DrawingInteractionController {
645
644
  // ============ 坐标转换 ============
646
645
 
647
646
  private anchorToScreen(anchor: DrawingAnchor): { x: number; y: number } | null {
648
- const viewport = this.chart.getViewport()
647
+ const viewport = this.adapter.getViewport()
649
648
  if (!viewport) return null
650
649
 
651
- const opt = this.chart.getOption()
652
- const dpr = this.chart.getCurrentDpr()
653
- const { startXPx, unitPx } = getPhysicalKLineConfig(opt.kWidth, opt.kGap, dpr)
650
+ const { kWidth, kGap } = this.adapter.getKWidthKGap()
651
+ const dpr = this.adapter.getCurrentDpr()
652
+ const { startXPx, unitPx } = getPhysicalKLineConfig(kWidth, kGap, dpr)
654
653
  if (!Number.isFinite(anchor.index)) return null
655
654
 
656
655
  const x = (startXPx + anchor.index * unitPx + (unitPx - 1) / 2) / dpr - viewport.scrollLeft
657
656
 
658
- const paneRenderer = this.chart.getPaneRenderers().find((item) => item.getPane().id === 'main')
659
- const pane = paneRenderer?.getPane()
660
- if (!pane) return null
661
-
662
- const y = pane.yAxis.priceToY(anchor.price)
657
+ const y = this.adapter.priceToY('main', anchor.price)
663
658
  return { x, y }
664
659
  }
665
660
 
@@ -667,23 +662,22 @@ export class DrawingInteractionController {
667
662
  screenX: number,
668
663
  screenY: number
669
664
  ): DrawingAnchorInput | null {
670
- const data = this.chart.getData()
671
- const viewport = this.chart.getViewport()
665
+ const data = this.adapter.getData()
666
+ const viewport = this.adapter.getViewport()
672
667
  if (!viewport || data.length === 0) return null
673
668
 
674
- const logicalIndex = this.chart.getLogicalIndexAtX(screenX)
669
+ const logicalIndex = this.adapter.getLogicalIndexAtX(screenX)
675
670
  if (logicalIndex === null) return null
676
671
 
677
- const paneRenderer = this.chart.getPaneRenderers().find((item) => item.getPane().id === 'main')
678
- const pane = paneRenderer?.getPane()
679
- if (!pane) return null
672
+ const paneInfo = this.adapter.getPaneInfo('main')
673
+ if (!paneInfo) return null
680
674
 
681
- const timestamp = this.chart.getTimestampAtLogicalIndex(logicalIndex) ?? undefined
675
+ const timestamp = this.adapter.getTimestampAtLogicalIndex(logicalIndex) ?? undefined
682
676
 
683
677
  return {
684
678
  index: logicalIndex,
685
679
  time: timestamp ?? undefined,
686
- price: pane.yAxis.yToPrice(screenY - pane.top),
680
+ price: this.adapter.yToPrice('main', screenY - paneInfo.top),
687
681
  }
688
682
  }
689
683
 
@@ -693,22 +687,22 @@ export class DrawingInteractionController {
693
687
  const newId = drawing?.id ?? null
694
688
  if (this.selectedDrawingId === newId) return
695
689
  this.selectedDrawingId = newId
696
- this.chart.setSelectedDrawingId(newId)
690
+ this.adapter.setSelectedDrawingId(newId)
697
691
  this.callbacks.onDrawingSelected?.(drawing)
698
692
  }
699
693
 
700
694
  private removePreview() {
701
695
  if (!this.drawings.some((d) => d.id === this.previewDrawingId)) return
702
696
  this.drawings = this.drawings.filter((d) => d.id !== this.previewDrawingId)
703
- this.chart.setDrawings(this.drawings)
697
+ this.adapter.setDrawings(this.drawings)
704
698
  }
705
699
 
706
700
  private resolveAnchorFromPointer(
707
701
  e: PointerEvent,
708
702
  container: HTMLElement
709
703
  ): DrawingAnchorInput | null {
710
- const data = this.chart.getData()
711
- const viewport = this.chart.getViewport()
704
+ const data = this.adapter.getData()
705
+ const viewport = this.adapter.getViewport()
712
706
  if (!viewport || data.length === 0) return null
713
707
 
714
708
  const rect = container.getBoundingClientRect()
@@ -718,21 +712,18 @@ export class DrawingInteractionController {
718
712
  return null
719
713
  }
720
714
 
721
- const paneRenderer = this.chart.getPaneRenderers().find((item) => {
722
- const pane = item.getPane()
723
- return pane.id === 'main' && mouseY >= pane.top && mouseY <= pane.top + pane.height
724
- })
725
- const pane = paneRenderer?.getPane()
726
- if (!pane) return null
715
+ const paneInfo = this.adapter.getPaneInfo('main')
716
+ if (!paneInfo) return null
717
+ if (mouseY < paneInfo.top || mouseY > paneInfo.top + paneInfo.height) return null
727
718
 
728
- const logicalIndex = this.chart.getLogicalIndexAtX(mouseX)
719
+ const logicalIndex = this.adapter.getLogicalIndexAtX(mouseX)
729
720
  if (logicalIndex === null) return null
730
- const timestamp = this.chart.getTimestampAtLogicalIndex(logicalIndex) ?? undefined
721
+ const timestamp = this.adapter.getTimestampAtLogicalIndex(logicalIndex) ?? undefined
731
722
 
732
723
  return {
733
724
  index: logicalIndex,
734
725
  time: timestamp ?? undefined,
735
- price: pane.yAxis.yToPrice(mouseY - pane.top),
726
+ price: this.adapter.yToPrice('main', mouseY - paneInfo.top),
736
727
  }
737
728
  }
738
729
 
@@ -754,7 +745,7 @@ export class DrawingInteractionController {
754
745
  }
755
746
 
756
747
  this.drawings = [...this.drawings, drawing]
757
- this.chart.setDrawings(this.drawings)
748
+ this.adapter.setDrawings(this.drawings)
758
749
  this.callbacks.onDrawingCreated?.(drawing)
759
750
  this.activeTool = 'cursor'
760
751
  this.callbacks.onToolChange?.('cursor')
@@ -801,7 +792,7 @@ export class DrawingInteractionController {
801
792
  }
802
793
 
803
794
  this.drawings = [...this.drawings, drawing]
804
- this.chart.setDrawings(this.drawings)
795
+ this.adapter.setDrawings(this.drawings)
805
796
  this.callbacks.onDrawingCreated?.(drawing)
806
797
  this.activeTool = 'cursor'
807
798
  this.callbacks.onToolChange?.('cursor')
@@ -1,7 +1,9 @@
1
- import type { RendererPlugin, RenderContext } from '../../plugin'
1
+ import type { RendererPluginWithHost, RenderContext, PluginHost } from '../../plugin'
2
2
  import { RENDERER_PRIORITY } from '../../plugin'
3
3
  import { getColors } from '../theme/colors'
4
4
  import { getFont, setCanvasFont } from '../theme/fonts'
5
+ import { SUB_PANE_INDICATOR_CONFIGS } from './Indicator/subPaneConfig'
6
+ import type { SubIndicatorType } from './Indicator'
5
7
 
6
8
  const textWidthCache = new Map<string, number>()
7
9
  const TEXT_WIDTH_CACHE_LIMIT = 256
@@ -21,49 +23,30 @@ function measureTextWidth(ctx: CanvasRenderingContext2D, text: string): number {
21
23
  return width
22
24
  }
23
25
 
24
- /**
25
- * 单个数值项
26
- */
27
26
  export interface TitleValueItem {
28
- /** 标签(如 "DIF"、"DEA") */
29
27
  label: string
30
- /** 数值 */
31
28
  value: number
32
- /** 颜色 */
33
29
  color: string
34
30
  }
35
31
 
36
- /**
37
- * 标题信息(由指标渲染器提供)
38
- */
39
32
  export interface TitleInfo {
40
- /** 指标名称(如 "MACD") */
41
33
  name: string
42
- /** 参数列表(如 [12, 26, 9]) */
43
34
  params?: number[]
44
- /** 数值项列表 */
45
35
  values?: TitleValueItem[]
46
36
  }
47
37
 
48
38
  export interface PaneTitleOptions {
49
- /** 面板 ID */
50
39
  paneId: string
51
- /** 标题文本(静态模式) */
52
40
  title: string
53
- /** 副标题/描述 */
54
41
  description?: string
55
- /** Y 偏移(逻辑像素) */
56
42
  yOffset?: number
57
- /** 动态标题信息提供函数 */
58
- getTitleInfo?: () => TitleInfo | null
43
+ indicatorId: SubIndicatorType
44
+ params: Record<string, unknown>
59
45
  }
60
46
 
61
- /**
62
- * 创建面板标题渲染器插件
63
- * 在面板左上角显示标题,支持动态指标数值显示
64
- */
65
- export function createPaneTitleRendererPlugin(options: PaneTitleOptions): RendererPlugin {
47
+ export function createPaneTitleRendererPlugin(options: PaneTitleOptions): RendererPluginWithHost {
66
48
  let currentOptions = { ...options }
49
+ let pluginHost: PluginHost | null = null
67
50
 
68
51
  return {
69
52
  name: `paneTitle_${options.paneId}`,
@@ -74,6 +57,10 @@ export function createPaneTitleRendererPlugin(options: PaneTitleOptions): Render
74
57
  priority: RENDERER_PRIORITY.FOREGROUND,
75
58
  layer: 'overlay',
76
59
 
60
+ onInstall(host: PluginHost) {
61
+ pluginHost = host
62
+ },
63
+
77
64
  draw(context: RenderContext) {
78
65
  const { overlayCtx, pane, paneWidth } = context
79
66
  const colors = getColors(context.theme)
@@ -89,7 +76,11 @@ export function createPaneTitleRendererPlugin(options: PaneTitleOptions): Render
89
76
  overlayCtx.textAlign = 'left'
90
77
  overlayCtx.textBaseline = 'top'
91
78
 
92
- const titleInfo = currentOptions.getTitleInfo?.()
79
+ const config = SUB_PANE_INDICATOR_CONFIGS[currentOptions.indicatorId]
80
+ const crosshairIndex = context.crosshairIndex ?? null
81
+ const titleInfo = config && pluginHost
82
+ ? config.getTitleInfo(context.data, crosshairIndex, currentOptions.params as Record<string, number | boolean | string>, pluginHost, currentOptions.paneId)
83
+ : null
93
84
 
94
85
  if (titleInfo) {
95
86
  let currentX = x
@@ -46,7 +46,7 @@ type LineWebGLHandles = {
46
46
  basic: BasicLineWebGLHandles
47
47
  }
48
48
 
49
- type LineMsaaTargets = {
49
+ type MsaaTargets = {
50
50
  samples: number
51
51
  widthPx: number
52
52
  heightPx: number
@@ -325,7 +325,7 @@ export class LineWebGLSurface {
325
325
  private fillScratch = new Float32Array(0)
326
326
  private lineScratch = new Float32Array(0)
327
327
  private region: WebGLRegion | null = null
328
- private msaaTargets: LineMsaaTargets | null = null
328
+ private msaaTargets: MsaaTargets | null = null
329
329
 
330
330
  // Geometry cache: 以 points 数组引用 + halfWidth 为 key,避免每帧重算法线/miter
331
331
  private geoCache = new WeakMap<Array<{ x: number; y: number }>, Map<number, { vertices: Float32Array; vertexCount: number }>>()
@@ -390,16 +390,10 @@ export class LineWebGLSurface {
390
390
  const colorValue = parseColor(line.color)
391
391
  if (!colorValue) return false
392
392
 
393
- if (line.width === 1) {
394
- const { vertexCount, vertices } = this.getThinLineVertices(line.points)
395
- drawCmds.push({ colorValue, mode: gl.LINE_STRIP, firstVertex: totalFloats / 2, pointCount: vertexCount })
396
- totalFloats += vertices.length
397
- } else {
398
- const geometry = this.getLineGeometry(line)
399
- if (!geometry) return false
400
- drawCmds.push({ colorValue, mode: gl.TRIANGLES, firstVertex: totalFloats / 2, pointCount: geometry.vertexCount })
401
- totalFloats += geometry.vertices.length
402
- }
393
+ const geometry = this.getLineGeometry(line)
394
+ if (!geometry) return false
395
+ drawCmds.push({ colorValue, mode: gl.TRIANGLES, firstVertex: totalFloats / 2, pointCount: geometry.vertexCount })
396
+ totalFloats += geometry.vertices.length
403
397
  }
404
398
 
405
399
  if (this.lineScratch.length < totalFloats) {
@@ -407,24 +401,13 @@ export class LineWebGLSurface {
407
401
  }
408
402
  let floatOffset = 0
409
403
  for (const line of lines) {
410
- const vertices = line.width === 1
411
- ? this.getThinLineVertices(line.points).vertices
412
- : this.getLineGeometry(line)!.vertices
404
+ const vertices = this.getLineGeometry(line)!.vertices
413
405
  this.lineScratch.set(vertices, floatOffset)
414
406
  floatOffset += vertices.length
415
407
  }
416
408
 
417
- const physical = this.shared.getPhysicalRegion(region)
418
- const msaaTargets = physical ? this.ensureLineMsaaTargets(gl, physical) : null
419
- const useMsaa = msaaTargets !== null
420
-
421
- if (useMsaa) {
422
- gl.bindFramebuffer(gl.FRAMEBUFFER, msaaTargets.msaaFramebuffer)
423
- gl.viewport(0, 0, msaaTargets.widthPx, msaaTargets.heightPx)
424
- gl.disable(gl.SCISSOR_TEST)
425
- gl.clearColor(0, 0, 0, 0)
426
- gl.clear(gl.COLOR_BUFFER_BIT)
427
- } else if (!this.shared.bindRegion(region)) {
409
+ const msaaRender = this.beginMsaaRender(gl, region)
410
+ if (!msaaRender && !this.shared.bindRegion(region)) {
428
411
  return false
429
412
  }
430
413
 
@@ -448,36 +431,14 @@ export class LineWebGLSurface {
448
431
 
449
432
  gl.bindVertexArray(null)
450
433
 
451
- if (useMsaa && msaaTargets && physical) {
452
- this.resolveLineMsaaToSharedRegion(gl, msaaTargets, physical)
434
+ if (msaaRender) {
435
+ this.resolveMsaaToSharedRegion(gl, msaaRender.targets, msaaRender.physical)
453
436
  }
454
437
 
455
438
  gl.bindFramebuffer(gl.FRAMEBUFFER, null)
456
439
  return true
457
440
  }
458
441
 
459
- private getThinLineVertices(points: Array<{ x: number; y: number }>): { vertices: Float32Array; vertexCount: number } {
460
- let widthMap = this.geoCache.get(points)
461
- if (widthMap) {
462
- const cached = widthMap.get(0)
463
- if (cached) return cached
464
- } else {
465
- widthMap = new Map()
466
- this.geoCache.set(points, widthMap)
467
- }
468
-
469
- const vertexCount = points.length
470
- const vertices = new Float32Array(vertexCount * 2)
471
- let writeIndex = 0
472
- for (const point of points) {
473
- vertices[writeIndex++] = point.x
474
- vertices[writeIndex++] = point.y
475
- }
476
- const result = { vertices, vertexCount }
477
- widthMap.set(0, result)
478
- return result
479
- }
480
-
481
442
  private getLineGeometry(line: LineStrip): { vertices: Float32Array; vertexCount: number } | null {
482
443
  const halfWidth = line.width / 2
483
444
  let widthMap = this.geoCache.get(line.points)
@@ -521,7 +482,11 @@ export class LineWebGLSurface {
521
482
  }
522
483
 
523
484
  const gl = this.shared.getGL()
524
- if (!gl || !this.region || !this.shared.bindRegion(this.region)) return false
485
+ const region = this.region
486
+ if (!gl || !region) return false
487
+
488
+ const msaaRender = this.beginMsaaRender(gl, region)
489
+ if (!msaaRender && !this.shared.bindRegion(region)) return false
525
490
 
526
491
  gl.useProgram(handles.basic.program)
527
492
  gl.bindVertexArray(handles.basic.vao)
@@ -538,6 +503,12 @@ export class LineWebGLSurface {
538
503
  gl.uniform4f(handles.basic.colorLocation, colorValue[0], colorValue[1], colorValue[2], colorValue[3])
539
504
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexCount)
540
505
  gl.bindVertexArray(null)
506
+
507
+ if (msaaRender) {
508
+ this.resolveMsaaToSharedRegion(gl, msaaRender.targets, msaaRender.physical)
509
+ }
510
+
511
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null)
541
512
  return true
542
513
  }
543
514
 
@@ -545,7 +516,7 @@ export class LineWebGLSurface {
545
516
  const handles = this.handles
546
517
  const gl = this.shared.getGL()
547
518
  if (gl) {
548
- this.destroyLineMsaaTargets(gl)
519
+ this.destroyMsaaTargets(gl)
549
520
  }
550
521
  if (!handles) {
551
522
  this.vertexCapacity = 0
@@ -563,7 +534,20 @@ export class LineWebGLSurface {
563
534
  this.vertexCapacity = 0
564
535
  }
565
536
 
566
- private ensureLineMsaaTargets(gl: WebGL2RenderingContext, physical: PhysicalRegion): LineMsaaTargets | null {
537
+ private beginMsaaRender(gl: WebGL2RenderingContext, region: WebGLRegion): { targets: MsaaTargets; physical: PhysicalRegion } | null {
538
+ const physical = this.shared.getPhysicalRegion(region)
539
+ const targets = physical ? this.ensureMsaaTargets(gl, physical) : null
540
+ if (!physical || !targets) return null
541
+
542
+ gl.bindFramebuffer(gl.FRAMEBUFFER, targets.msaaFramebuffer)
543
+ gl.viewport(0, 0, targets.widthPx, targets.heightPx)
544
+ gl.disable(gl.SCISSOR_TEST)
545
+ gl.clearColor(0, 0, 0, 0)
546
+ gl.clear(gl.COLOR_BUFFER_BIT)
547
+ return { targets, physical }
548
+ }
549
+
550
+ private ensureMsaaTargets(gl: WebGL2RenderingContext, physical: PhysicalRegion): MsaaTargets | null {
567
551
  const preferredSamples = 4
568
552
  const maxSamples = Number(gl.getParameter(gl.MAX_SAMPLES)) || 0
569
553
  const samples = Math.max(1, Math.min(preferredSamples, maxSamples))
@@ -579,7 +563,7 @@ export class LineWebGLSurface {
579
563
  return existing
580
564
  }
581
565
 
582
- this.destroyLineMsaaTargets(gl)
566
+ this.destroyMsaaTargets(gl)
583
567
 
584
568
  const msaaFramebuffer = gl.createFramebuffer()
585
569
  const msaaColorRenderbuffer = gl.createRenderbuffer()
@@ -643,7 +627,7 @@ export class LineWebGLSurface {
643
627
  return targets
644
628
  }
645
629
 
646
- private destroyLineMsaaTargets(gl: WebGL2RenderingContext): void {
630
+ private destroyMsaaTargets(gl: WebGL2RenderingContext): void {
647
631
  const targets = this.msaaTargets
648
632
  if (!targets) return
649
633
  gl.deleteFramebuffer(targets.msaaFramebuffer)
@@ -653,7 +637,7 @@ export class LineWebGLSurface {
653
637
  this.msaaTargets = null
654
638
  }
655
639
 
656
- private resolveLineMsaaToSharedRegion(gl: WebGL2RenderingContext, targets: LineMsaaTargets, physical: PhysicalRegion): void {
640
+ private resolveMsaaToSharedRegion(gl: WebGL2RenderingContext, targets: MsaaTargets, physical: PhysicalRegion): void {
657
641
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER, targets.msaaFramebuffer)
658
642
  gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, targets.resolveFramebuffer)
659
643
  gl.blitFramebuffer(