diff --git a/common/changes/@visactor/vrender-core/feat-line-render-contribution_2026-09-15-08-00.json b/common/changes/@visactor/vrender-core/feat-line-render-contribution_2026-09-15-08-00.json new file mode 100644 index 000000000..e3eef34bf --- /dev/null +++ b/common/changes/@visactor/vrender-core/feat-line-render-contribution_2026-09-15-08-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "feat: support render contributions for line graphic", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/packages/vrender-core/__tests__/perf/line-render-contribution-performance.test.ts b/packages/vrender-core/__tests__/perf/line-render-contribution-performance.test.ts new file mode 100644 index 000000000..d34b06ded --- /dev/null +++ b/packages/vrender-core/__tests__/perf/line-render-contribution-performance.test.ts @@ -0,0 +1,112 @@ +import { createLine } from '../../src/graphic/line'; +import { DefaultCanvasLineRender } from '../../src/render/contributions/render/line-render'; + +declare const process: { + env: Record; + stdout: { + write: (message: string) => void; + }; +}; + +// 默认跳过,和 attribute-model-performance 一样按需开:VRENDER_LINE_RENDER_PERF=1 +const runPerf = process.env.VRENDER_LINE_RENDER_PERF === '1' ? describe : describe.skip; + +const POINT_COUNT = 200; +const ITERATIONS = 2000; +const ROUNDS = 9; + +function createNoopContext() { + const noop = (): void => undefined; + return { + beginPath: noop, + closePath: noop, + moveTo: noop, + lineTo: noop, + stroke: noop, + fill: noop, + setCommonStyle: noop, + setStrokeStyle: noop, + setShadowBlendStyle: noop + }; +} + +function createBenchmarkLine() { + const points: { x: number; y: number }[] = []; + for (let i = 0; i < POINT_COUNT; i++) { + points.push({ x: i, y: (i * 7) % 100 }); + } + return createLine({ points, stroke: '#1664ff', lineWidth: 1, curveType: 'linear' }); +} + +/** 把两个钩子改成空实现,用来隔离「多出来的那点活」到底值多少 */ +class NoHookLineRender extends DefaultCanvasLineRender { + beforeRenderStep(): void { + return undefined; + } + afterRenderStep(): void { + return undefined; + } +} + +function warmUp(render: DefaultCanvasLineRender): void { + const line = createBenchmarkLine(); + const context = createNoopContext(); + for (let i = 0; i < 500; i++) { + render.drawShape(line as any, context as any, 0, 0, {} as any); + } +} + +function measure(render: DefaultCanvasLineRender): number { + const line = createBenchmarkLine(); + const context = createNoopContext(); + const start = performance.now(); + for (let i = 0; i < ITERATIONS; i++) { + render.drawShape(line as any, context as any, 0, 0, {} as any); + } + return performance.now() - start; +} + +/** + * 两个变体交替测、各取最小值。 + * 顺序测会把 JIT 预热成本算到先测的那个头上,实测能让开销算出负数。 + */ +function compare(a: DefaultCanvasLineRender, b: DefaultCanvasLineRender): { a: number; b: number } { + warmUp(a); + warmUp(b); + let bestA = Infinity; + let bestB = Infinity; + for (let round = 0; round < ROUNDS; round++) { + bestA = Math.min(bestA, measure(a)); + bestB = Math.min(bestB, measure(b)); + } + return { a: bestA, b: bestB }; +} + +runPerf('line render contribution overhead on the high-performance path', () => { + it('measures the cost added by the render contribution hooks', () => { + const emptyProvider = { getContributions: (): any[] => [] } as any; + // init() 无条件塞两个内置裁剪贡献,所以「没有自定义贡献」并不是空列表 + const withHooks = new DefaultCanvasLineRender(emptyProvider); + const withoutHooks = new NoHookLineRender(emptyProvider); + + expect((withHooks as any)._beforeRenderContribitions.length).toBe(1); + expect((withHooks as any)._afterRenderContribitions.length).toBe(1); + + const { a: baseline, b: actual } = compare(withoutHooks, withHooks); + + process.stdout.write( + JSON.stringify({ + benchmark: 'line-render-contribution-overhead', + pointCount: POINT_COUNT, + iterations: ITERATIONS, + rounds: ROUNDS, + hooksStubbedMinMs: Number(baseline.toFixed(3)), + hooksActiveMinMs: Number(actual.toFixed(3)), + overheadPerDrawUs: Number((((actual - baseline) / ITERATIONS) * 1000).toFixed(4)), + overheadPct: Number((((actual - baseline) / baseline) * 100).toFixed(2)) + }) + '\n' + ); + + expect(actual).toBeGreaterThan(0); + }); +}); diff --git a/packages/vrender-core/__tests__/unit/entries/runtime-renderer-contributions.test.ts b/packages/vrender-core/__tests__/unit/entries/runtime-renderer-contributions.test.ts index 82e9f03bf..39487e879 100644 --- a/packages/vrender-core/__tests__/unit/entries/runtime-renderer-contributions.test.ts +++ b/packages/vrender-core/__tests__/unit/entries/runtime-renderer-contributions.test.ts @@ -39,6 +39,11 @@ describe('runtime graphic renderer contributions', () => { rendererName: 'DefaultCanvasAreaRender', contributionExport: 'AreaRenderContribution' }, + { + moduleExport: 'lineModule', + rendererName: 'DefaultCanvasLineRender', + contributionExport: 'LineRenderContribution' + }, { moduleExport: 'pathModule', rendererName: 'DefaultCanvasPathRender', diff --git a/packages/vrender-core/__tests__/unit/render/line-render-contribution.test.ts b/packages/vrender-core/__tests__/unit/render/line-render-contribution.test.ts new file mode 100644 index 000000000..4a3a31529 --- /dev/null +++ b/packages/vrender-core/__tests__/unit/render/line-render-contribution.test.ts @@ -0,0 +1,199 @@ +import { createLine } from '../../../src/graphic/line'; +import { DefaultCanvasLineRender } from '../../../src/render/contributions/render/line-render'; +import { DefaultIncrementalCanvasLineRender } from '../../../src/render/contributions/render/incremental-line-render'; +import { BaseRenderContributionTime } from '../../../src/common/enums'; + +type RecordingContext = ReturnType; + +/** 记录画布调用序列,用来判「最后 stroke 的到底是折线还是裁剪形状」 */ +function createRecordingContext() { + const calls: string[] = []; + const record = (name: string) => (): void => { + calls.push(name); + }; + return { + calls, + beginPath: record('beginPath'), + closePath: record('closePath'), + moveTo: record('moveTo'), + lineTo: record('lineTo'), + rect: record('rect'), + clip: record('clip'), + save: record('save'), + restore: record('restore'), + fill: record('fill'), + stroke: record('stroke'), + setCommonStyle: jest.fn(), + setStrokeStyle: jest.fn(), + setShadowBlendStyle: jest.fn(), + nativeContext: { + moveTo: record('moveTo'), + lineTo: record('lineTo'), + closePath: record('closePath') + } + }; +} + +function createRender(contributions: any[] = []) { + return new DefaultCanvasLineRender({ getContributions: () => contributions } as any); +} + +/** 画布调用序列里,最后一次 stroke 之前那一段路径指令 */ +function pathBeforeLastStroke(context: RecordingContext) { + const strokeAt = context.calls.lastIndexOf('stroke'); + const beginAt = context.calls.lastIndexOf('beginPath', strokeAt); + return context.calls.slice(beginAt, strokeAt); +} + +describe('line render contributions', () => { + it('keeps the base drawing sequence when no contribution is registered', () => { + const line = createLine({ + points: [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 0 } + ], + stroke: '#000', + lineWidth: 1 + }); + const context = createRecordingContext(); + + createRender().drawShape(line as any, context as any, 0, 0, {} as any); + + expect(context.calls).toEqual(['beginPath', 'moveTo', 'lineTo', 'lineTo', 'stroke']); + expect(context.calls).not.toContain('clip'); + }); + + it('strokes the line itself, not the clip shape, when clipConfig is set', () => { + const line = createLine({ + points: [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + { x: 20, y: 0 } + ], + stroke: '#000', + lineWidth: 1, + clipConfig: { shape: 'rect' } + } as any); + // 只关心内置裁剪贡献会 beginPath 冲掉图元路径这件事,裁剪形状本身用最小桩 + Object.defineProperty(line, 'AABBBounds', { + get: () => ({ width: () => 20, height: () => 20 }) + }); + (line as any).getClipPath = () => ({ + draw: (ctx: any) => { + ctx.rect(0, 0, 20, 20); + return true; + } + }); + const context = createRecordingContext(); + + createRender().drawShape(line as any, context as any, 0, 0, {} as any); + + expect(context.calls).toContain('clip'); + // 裁剪生效之后重建了折线路径,stroke 画的是折线而不是裁剪矩形 + expect(pathBeforeLastStroke(context)).toEqual(['beginPath', 'moveTo', 'lineTo', 'lineTo']); + expect(pathBeforeLastStroke(context)).not.toContain('rect'); + }); + + it('passes computed visibility booleans rather than opacity numbers', () => { + const drawShape = jest.fn(); + const line = createLine({ + points: [ + { x: 0, y: 0 }, + { x: 10, y: 10 } + ], + fill: false, + stroke: '#000', + strokeOpacity: 0.5, + lineWidth: 1 + }); + const context = createRecordingContext(); + + createRender([ + { time: BaseRenderContributionTime.beforeFillStroke, useStyle: false, order: 1, drawShape } + ]).drawShape(line as any, context as any, 0, 0, {} as any); + + expect(drawShape).toHaveBeenCalledTimes(1); + const [, , , , doFill, doStroke, fVisible, sVisible] = drawShape.mock.calls[0]; + expect([doFill, doStroke, fVisible, sVisible]).toEqual([false, true, false, true]); + }); + + it('exposes the active segment attributes to per-segment contributions', () => { + const drawShape = jest.fn(); + const line = createLine({ + stroke: '#000', + lineWidth: 1, + segments: [ + { + points: [ + { x: 0, y: 0 }, + { x: 10, y: 10 } + ], + stroke: 'red' + }, + { + points: [ + { x: 10, y: 10 }, + { x: 20, y: 0 } + ], + stroke: 'blue' + } + ] + } as any); + const context = createRecordingContext(); + + createRender([ + { time: BaseRenderContributionTime.beforeFillStroke, useStyle: false, order: 1, drawShape } + ]).drawShape(line as any, context as any, 0, 0, {} as any); + + const strokes = drawShape.mock.calls.map(args => args[args.length - 1]?.attribute?.stroke); + expect(strokes).toEqual(['red', 'blue']); + }); + + it('wires contributions through the incremental renderer', () => { + const drawShape = jest.fn(); + const afterDrawShape = jest.fn(); + const render = new DefaultIncrementalCanvasLineRender({ + getContributions: () => [ + { time: BaseRenderContributionTime.beforeFillStroke, useStyle: false, order: 1, drawShape }, + { + time: BaseRenderContributionTime.afterFillStroke, + useStyle: false, + order: 1, + drawShape: afterDrawShape + } + ] + } as any); + const line = createLine({ + stroke: '#000', + lineWidth: 1, + segments: [ + { + points: [ + { x: 0, y: 0 }, + { x: 10, y: 10 } + ] + }, + { + points: [ + { x: 10, y: 10 }, + { x: 20, y: 0 } + ] + } + ] + } as any); + (line as any).incremental = 1; + const context = createRecordingContext(); + + render.drawShape(line as any, context as any, 0, 0, { + context, + multiGraphicOptions: { startAtIdx: 1, length: 1 } + } as any); + + expect(context.calls).toContain('stroke'); + expect(drawShape).toHaveBeenCalledTimes(1); + expect(afterDrawShape).toHaveBeenCalledTimes(1); + const [, , , , doFill, doStroke, fVisible, sVisible] = drawShape.mock.calls[0]; + expect([doFill, doStroke, fVisible, sVisible]).toEqual([false, true, false, true]); + }); +}); diff --git a/packages/vrender-core/src/entries/runtime-installer.ts b/packages/vrender-core/src/entries/runtime-installer.ts index f6da4f946..38be95851 100644 --- a/packages/vrender-core/src/entries/runtime-installer.ts +++ b/packages/vrender-core/src/entries/runtime-installer.ts @@ -1,6 +1,7 @@ import type { IContributionProvider, IAreaRenderContribution, + ILineRenderContribution, IDrawItemInterceptorContribution, IEnvContribution, IGlobal, @@ -21,7 +22,10 @@ import graphicModule from '../graphic/graphic-service/graphic-module'; import type { ILegacyBindingContext } from '../legacy/binding-context'; import { getLegacyBindingContext, preLoadAllModule } from '../legacy/bootstrap'; import pickModule from '../picker/pick-modules'; -import { AreaRenderContribution } from '../render/contributions/render/contributions/constants'; +import { + AreaRenderContribution, + LineRenderContribution +} from '../render/contributions/render/contributions/constants'; import { DrawItemInterceptor } from '../render/contributions/render/draw-interceptor'; import { DefaultIncrementalCanvasAreaRender } from '../render/contributions/render/incremental-area-render'; import { DefaultIncrementalDrawContribution } from '../render/contributions/render/incremental-draw-contribution'; @@ -169,7 +173,9 @@ export function configureRuntimeApplicationForApp(app: IApp): void { application.incrementalDrawContributionFactory = () => new DefaultIncrementalDrawContribution( [], - new DefaultIncrementalCanvasLineRender(), + new DefaultIncrementalCanvasLineRender( + createContributionProvider(LineRenderContribution, bindingContext) + ), new DefaultIncrementalCanvasAreaRender( createContributionProvider(AreaRenderContribution, bindingContext) ), diff --git a/packages/vrender-core/src/interface/contribution.ts b/packages/vrender-core/src/interface/contribution.ts index 3a71c6d0d..66730f3d1 100644 --- a/packages/vrender-core/src/interface/contribution.ts +++ b/packages/vrender-core/src/interface/contribution.ts @@ -6,6 +6,7 @@ import type { ISymbol, ISymbolGraphicAttribute } from './graphic/symbol'; import type { BaseRenderContributionTime } from '../common/enums'; import type { IArc, IArcGraphicAttribute } from './graphic/arc'; import type { IArea, IAreaGraphicAttribute } from './graphic/area'; +import type { ILine, ILineGraphicAttribute } from './graphic/line'; import type { IText, ITextGraphicAttribute } from './graphic/text'; import type { ICircle, ICircleGraphicAttribute } from './graphic/circle'; import type { IGroup, IGroupGraphicAttribute } from './graphic/group'; @@ -75,6 +76,8 @@ export type IPolygonRenderContribution = IBaseRenderContribution; +export type ILineRenderContribution = IBaseRenderContribution; + export interface IContributionProvider { getContributions: () => T[]; } diff --git a/packages/vrender-core/src/render/contributions/render/contributions/constants.ts b/packages/vrender-core/src/render/contributions/render/contributions/constants.ts index ec8c14b4d..e78fc82b4 100644 --- a/packages/vrender-core/src/render/contributions/render/contributions/constants.ts +++ b/packages/vrender-core/src/render/contributions/render/contributions/constants.ts @@ -6,6 +6,7 @@ export const ImageRenderContribution = Symbol.for('ImageRenderContribution'); export const PathRenderContribution = Symbol.for('PathRenderContribution'); export const PolygonRenderContribution = Symbol.for('PolygonRenderContribution'); export const RectRenderContribution = Symbol.for('RectRenderContribution'); +export const LineRenderContribution = Symbol.for('LineRenderContribution'); export const SymbolRenderContribution = Symbol.for('SymbolRenderContribution'); export const TextRenderContribution = Symbol.for('TextRenderContribution'); export const StarRenderContribution = Symbol.for('StarRenderContribution'); diff --git a/packages/vrender-core/src/render/contributions/render/incremental-line-render.ts b/packages/vrender-core/src/render/contributions/render/incremental-line-render.ts index b8c146eb8..8e302bbab 100644 --- a/packages/vrender-core/src/render/contributions/render/incremental-line-render.ts +++ b/packages/vrender-core/src/render/contributions/render/incremental-line-render.ts @@ -13,6 +13,7 @@ import type { import { getTheme } from '../../../graphic/theme'; import { LINE_NUMBER_TYPE } from '../../../graphic/constants'; import { fillVisible, runFill, runStroke, strokeVisible } from './utils'; +import type { ILineRenderVisibility } from './line-render'; import { DefaultCanvasLineRender } from './line-render'; import { drawIncrementalSegments } from '../../../common/render-curve'; @@ -82,6 +83,7 @@ export class DefaultIncrementalCanvasLineRender extends DefaultCanvasLineRender } const { context } = drawContext; + const visibility: ILineRenderVisibility = { doFill, doStroke, fVisible, sVisible }; // 不支持clipRange,不支持pick,仅支持最基础的线段绘制 for (let i = startAtIdx; i < startAtIdx + length; i++) { this.drawIncreaseSegment( @@ -92,7 +94,9 @@ export class DefaultIncrementalCanvasLineRender extends DefaultCanvasLineRender line.attribute.segments[i], [lineAttribute, line.attribute], x, - y + y, + drawContext, + visibility ); } } else { @@ -108,18 +112,65 @@ export class DefaultIncrementalCanvasLineRender extends DefaultCanvasLineRender attribute: Partial, defaultAttribute: Required | Partial[], offsetX: number, - offsetY: number + offsetY: number, + drawContext: IDrawContext, + visibility: ILineRenderVisibility ) { if (!seg) { return; } - context.beginPath(); - drawIncrementalSegments(context.nativeContext, lastSeg, seg, { offsetX, offsetY }); + // 内置裁剪贡献在 beforeFillStroke 里 beginPath 建裁剪路径,会冲掉这里建好的折线路径,故要能重建 + const buildPath = () => { + context.beginPath(); + drawIncrementalSegments(context.nativeContext, lastSeg, seg, { offsetX, offsetY }); + }; + + buildPath(); // shadow context.setShadowBlendStyle && context.setShadowBlendStyle(line, attribute, defaultAttribute); + + const { doFill, doStroke, fVisible, sVisible } = visibility; + + this.beforeRenderStep( + line, + context, + offsetX, + offsetY, + doFill, + doStroke, + fVisible, + sVisible, + defaultAttribute as Required, + drawContext, + null, + null, + { attribute } + ); + + // 有裁剪配置时上面的贡献点已经 beginPath 建了裁剪路径,重建折线路径,否则 stroke 画的是裁剪形状 + if (line.attribute.clipConfig) { + buildPath(); + } + context.setStrokeStyle(line, attribute, offsetX, offsetY, defaultAttribute); context.stroke(); + + this.afterRenderStep( + line, + context, + offsetX, + offsetY, + doFill, + doStroke, + fVisible, + sVisible, + defaultAttribute as Required, + drawContext, + null, + null, + { attribute } + ); } } diff --git a/packages/vrender-core/src/render/contributions/render/line-module.ts b/packages/vrender-core/src/render/contributions/render/line-module.ts index e74ead036..60b444d61 100644 --- a/packages/vrender-core/src/render/contributions/render/line-module.ts +++ b/packages/vrender-core/src/render/contributions/render/line-module.ts @@ -1,4 +1,6 @@ +import { bindContributionProvider, createContributionProvider } from '../../../common/contribution-provider'; import { isBindingContextLoaded } from '../../../common/module-guard'; +import { LineRenderContribution } from './contributions/constants'; import { DefaultCanvasLineRender } from './line-render'; import { GraphicRender, LineRender } from './symbol'; @@ -9,10 +11,15 @@ export function bindLineRenderModule({ bind }: { bind: any }) { } // line渲染器 bind(DefaultCanvasLineRender) - .toDynamicValue(() => new DefaultCanvasLineRender()) + .toDynamicValue( + ({ container }: { container: any }) => + new DefaultCanvasLineRender(createContributionProvider(LineRenderContribution, container)) + ) .inSingletonScope(); bind(LineRender).toService(DefaultCanvasLineRender); bind(GraphicRender).toService(LineRender); + // line渲染器注入contributions + bindContributionProvider(bind, LineRenderContribution); } export const lineModule = bindLineRenderModule; diff --git a/packages/vrender-core/src/render/contributions/render/line-render.ts b/packages/vrender-core/src/render/contributions/render/line-render.ts index ba08b091e..b429edd1f 100644 --- a/packages/vrender-core/src/render/contributions/render/line-render.ts +++ b/packages/vrender-core/src/render/contributions/render/line-render.ts @@ -12,7 +12,9 @@ import type { IDrawContext, IRenderService, IGraphicRender, - IGraphicRenderDrawParams + IGraphicRenderDrawParams, + IContributionProvider, + ILineRenderContribution } from '../../../interface'; import { getTheme } from '../../../graphic/theme'; import { LINE_NUMBER_TYPE } from '../../../graphic/constants'; @@ -20,6 +22,14 @@ import { BaseRender } from './base-render'; import { drawSegments } from '../../../common/render-curve'; import { calcLineCache } from '../../../common/segment'; +/** BaseRender.valid() 算出的填充/描边资格与可见性,用于喂给渲染贡献点 */ +export interface ILineRenderVisibility { + doFill: boolean; + doStroke: boolean; + fVisible: boolean; + sVisible: boolean; +} + /** * 默认的基于canvas的line渲染器 * 单例 @@ -29,6 +39,12 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph numberType: number = LINE_NUMBER_TYPE; declare z: number; + constructor(protected readonly graphicRenderContributions: IContributionProvider) { + super(); + this.builtinContributions = []; + this.init(graphicRenderContributions); + } + draw(line: ILine, renderService: IRenderService, drawContext: IDrawContext, params?: IGraphicRenderDrawParams) { const lineAttribute = getTheme(line, params?.theme).line; this._draw(line, lineAttribute, false, drawContext, params); @@ -63,6 +79,8 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph offsetX: number, offsetY: number, line: ILine, + drawContext: IDrawContext, + visibility: ILineRenderVisibility, fillCb?: ( ctx: IContext2d, lineAttribute: Partial, @@ -77,32 +95,61 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph if (!cache) { return; } - context.beginPath(); const z = this.z ?? 0; - drawSegments(context, cache, clipRange, clipRangeByDimension, { - offsetX, - offsetY, - offsetZ: z - }); - - // 如果是一根线,且是Closed,并且没有defined为false的点,需要close - if ( - line.cache && - !isArray(line.cache) && - line.cache.curves.every(c => c.defined) && - line.attribute.curveType && - line.attribute.curveType.includes('Closed') - ) { - context.closePath(); - } + // 内置裁剪贡献在 beforeFillStroke 里 beginPath 建裁剪路径,会冲掉这里建好的折线路径,故要能重建 + const buildPath = () => { + context.beginPath(); + + drawSegments(context, cache, clipRange, clipRangeByDimension, { + offsetX, + offsetY, + offsetZ: z + }); + + // 如果是一根线,且是Closed,并且没有defined为false的点,需要close + if ( + line.cache && + !isArray(line.cache) && + line.cache.curves.every(c => c.defined) && + line.attribute.curveType && + line.attribute.curveType.includes('Closed') + ) { + context.closePath(); + } + }; + + buildPath(); // shadow context.setShadowBlendStyle && context.setShadowBlendStyle(line, attribute, defaultAttribute); const { x: originX = 0, x: originY = 0 } = attribute; const ret: boolean = false; + const { doFill, doStroke, fVisible, sVisible } = visibility; + + this.beforeRenderStep( + line, + context, + offsetX, + offsetY, + doFill, + doStroke, + fVisible, + sVisible, + defaultAttribute as Required, + drawContext, + fillCb, + strokeCb, + { attribute } + ); + + // 有裁剪配置时上面的贡献点已经 beginPath 建了裁剪路径,重建折线路径,否则 fill/stroke 画的是裁剪形状 + if (line.attribute.clipConfig) { + buildPath(); + } + if (fill !== false) { if (fillCb) { fillCb(context, attribute, defaultAttribute); @@ -119,6 +166,22 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph context.stroke(); } } + + this.afterRenderStep( + line, + context, + offsetX, + offsetY, + doFill, + doStroke, + fVisible, + sVisible, + defaultAttribute as Required, + drawContext, + fillCb, + strokeCb, + { attribute } + ); return !!ret; } @@ -134,6 +197,7 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph offsetY: number, lineAttribute: Required, drawContext: IDrawContext, + visibility: ILineRenderVisibility, params?: IGraphicRenderDrawParams, fillCb?: ( ctx: IContext2d, @@ -146,22 +210,49 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph themeAttribute: IThemeAttribute ) => boolean ) { - context.beginPath(); - const z = this.z ?? 0; const { points } = line.attribute; const startP = points[0]; - context.moveTo(startP.x + offsetX, startP.y + offsetY, z); - for (let i = 1; i < points.length; i++) { - const p = points[i]; - context.lineTo(p.x + offsetX, p.y + offsetY, z); - } + // 内置裁剪贡献在 beforeFillStroke 里 beginPath 建裁剪路径,会冲掉这里建好的折线路径,故要能重建 + const buildPath = () => { + context.beginPath(); + context.moveTo(startP.x + offsetX, startP.y + offsetY, z); + for (let i = 1; i < points.length; i++) { + const p = points[i]; + context.lineTo(p.x + offsetX, p.y + offsetY, z); + } + }; + + buildPath(); // shadow context.setShadowBlendStyle && context.setShadowBlendStyle(line, line.attribute, lineAttribute); const { x: originX = 0, x: originY = 0 } = line.attribute; + const { doFill, doStroke, fVisible, sVisible } = visibility; + + this.beforeRenderStep( + line, + context, + offsetX, + offsetY, + doFill, + doStroke, + fVisible, + sVisible, + lineAttribute, + drawContext, + fillCb, + strokeCb, + { attribute: line.attribute } + ); + + // 有裁剪配置时上面的贡献点已经 beginPath 建了裁剪路径,重建折线路径,否则 fill/stroke 画的是裁剪形状 + if (line.attribute.clipConfig) { + buildPath(); + } + if (fill !== false) { if (fillCb) { fillCb(context, line.attribute, lineAttribute); @@ -178,6 +269,22 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph context.stroke(); } } + + this.afterRenderStep( + line, + context, + offsetX, + offsetY, + doFill, + doStroke, + fVisible, + sVisible, + lineAttribute, + drawContext, + fillCb, + strokeCb, + { attribute: line.attribute } + ); } drawShape( @@ -238,12 +345,12 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph y, lineAttribute, drawContext, + data, params, fillCb, strokeCb ); } - // const { fVisible, sVisible, doFill, doStroke } = data; function parsePoint(points: IPointLike[], connectedType: 'none' | 'connect') { if (connectedType === 'none') { @@ -349,6 +456,8 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph x, y, line, + drawContext, + data, fillCb, strokeCb ); @@ -384,6 +493,8 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph x, y, line, + drawContext, + data, fillCb, strokeCb ); @@ -405,6 +516,8 @@ export class DefaultCanvasLineRender extends BaseRender implements IGraph x, y, line, + drawContext, + data, fillCb, strokeCb );