From 523079763fd89e5597d8ca3824f85cf80c7364de Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Sun, 29 Sep 2024 16:29:29 +0800 Subject: [PATCH 01/28] fix: fix issue with dirtyBounds incorrectly while set visible --- .../fix-visible-bounds_2024-09-29-08-30.json | 10 ++++++++++ .../src/graphic/graphic-service/graphic-service.ts | 10 +++++----- packages/vrender/__tests__/browser/src/pages/text.ts | 2 ++ 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 common/changes/@visactor/vrender-core/fix-visible-bounds_2024-09-29-08-30.json diff --git a/common/changes/@visactor/vrender-core/fix-visible-bounds_2024-09-29-08-30.json b/common/changes/@visactor/vrender-core/fix-visible-bounds_2024-09-29-08-30.json new file mode 100644 index 000000000..e590b282f --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-visible-bounds_2024-09-29-08-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: fix issue with dirtyBounds incorrectly while set visible", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts index 186d5535e..f345b256c 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts @@ -23,6 +23,7 @@ import { BoundsContext } from '../../common/bounds-context'; import { renderCommandList } from '../../common/render-command-list'; import { GraphicCreator } from '../constants'; import { identityMat4, multiplyMat4Mat4, rotateX, rotateY, rotateZ, scaleMat4, translate } from '../../common/matrix'; +import { application } from '../../application'; export function getExtraModelMatrix(dx: number, dy: number, graphic: IGraphic): mat4 | null { const { alpha, beta } = graphic.attribute; @@ -366,13 +367,12 @@ export class DefaultGraphicService implements IGraphicService { return true; } - if (!graphic.valid) { - aabbBounds.clear(); - return false; - } const { visible = theme.visible } = attribute; - if (!visible) { + + if (!(graphic.valid && visible)) { + application.graphicService.beforeUpdateAABBBounds(graphic, graphic.stage, true, aabbBounds); aabbBounds.clear(); + application.graphicService.afterUpdateAABBBounds(graphic, graphic.stage, aabbBounds, graphic, true); return false; } return true; diff --git a/packages/vrender/__tests__/browser/src/pages/text.ts b/packages/vrender/__tests__/browser/src/pages/text.ts index 3af00222d..39e6f970b 100644 --- a/packages/vrender/__tests__/browser/src/pages/text.ts +++ b/packages/vrender/__tests__/browser/src/pages/text.ts @@ -190,6 +190,8 @@ export const page = () => { // scaleY: 2 }); graphics.push(text); + text.setAttributes({ visible: false }); + console.log(text.AABBBounds); const b = text.OBBBounds; const circle = createCircle({ x: (b.x1 + b.x2) / 2, From 6a6d53cab2f55017de510763c68295eece2d45c3 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Sun, 29 Sep 2024 16:47:30 +0800 Subject: [PATCH 02/28] fix: add clearAABBBounds interface --- .../graphic/graphic-service/graphic-service.ts | 17 +++++++++++++---- .../src/interface/graphic-service.ts | 2 ++ .../builtin-plugin/dirty-bounds-plugin.ts | 15 +++++++++++++++ .../vrender/__tests__/browser/src/pages/text.ts | 8 ++++++-- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts index f345b256c..5ad5802a3 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts @@ -176,6 +176,7 @@ export class DefaultGraphicService implements IGraphicService { onClearIncremental: ISyncHook<[IGroup, IStage]>; beforeUpdateAABBBounds: ISyncHook<[IGraphic, IStage, boolean, IAABBBounds]>; afterUpdateAABBBounds: ISyncHook<[IGraphic, IStage, IAABBBounds, { globalAABBBounds: IAABBBounds }, boolean]>; + clearAABBBounds: ISyncHook<[IGraphic, IStage, IAABBBounds]>; }; // 临时bounds,用作缓存 @@ -201,7 +202,8 @@ export class DefaultGraphicService implements IGraphicService { 'aabbBounds', 'globalAABBBounds', 'selfChange' - ]) + ]), + clearAABBBounds: new SyncHook<[IGraphic, IStage, IAABBBounds]>(['graphic', 'stage', 'aabbBounds']) }; this.tempAABBBounds1 = new AABBBounds(); this.tempAABBBounds2 = new AABBBounds(); @@ -252,6 +254,11 @@ export class DefaultGraphicService implements IGraphicService { this.hooks.afterUpdateAABBBounds.call(graphic, stage, bounds, params, selfChange); } } + clearAABBBounds(graphic: IGraphic, stage: IStage, b: IAABBBounds) { + if (this.hooks.clearAABBBounds.taps.length) { + this.hooks.clearAABBBounds.call(graphic, stage, b); + } + } // TODO delete updatePathProxyAABBBounds(aabbBounds: IAABBBounds, graphic?: IGraphic): boolean { const path = typeof graphic.pathProxy === 'function' ? graphic.pathProxy(graphic.attribute) : graphic.pathProxy; @@ -370,9 +377,11 @@ export class DefaultGraphicService implements IGraphicService { const { visible = theme.visible } = attribute; if (!(graphic.valid && visible)) { - application.graphicService.beforeUpdateAABBBounds(graphic, graphic.stage, true, aabbBounds); - aabbBounds.clear(); - application.graphicService.afterUpdateAABBBounds(graphic, graphic.stage, aabbBounds, graphic, true); + // application.graphicService.beforeUpdateAABBBounds(graphic, graphic.stage, true, aabbBounds); + if (!aabbBounds.empty()) { + application.graphicService.clearAABBBounds(graphic, graphic.stage, aabbBounds); + aabbBounds.clear(); + } return false; } return true; diff --git a/packages/vrender-core/src/interface/graphic-service.ts b/packages/vrender-core/src/interface/graphic-service.ts index 19e0464f6..c5ba53f5e 100644 --- a/packages/vrender-core/src/interface/graphic-service.ts +++ b/packages/vrender-core/src/interface/graphic-service.ts @@ -51,6 +51,7 @@ export interface IGraphicService { onClearIncremental: ISyncHook<[IGroup, IStage]>; beforeUpdateAABBBounds: ISyncHook<[IGraphic, IStage, boolean, IAABBBounds]>; afterUpdateAABBBounds: ISyncHook<[IGraphic, IStage, IAABBBounds, { globalAABBBounds: IAABBBounds }, boolean]>; + clearAABBBounds: ISyncHook<[IGraphic, IStage, IAABBBounds]>; }; beforeUpdateAABBBounds: (graphic: IGraphic, stage: IStage, willUpdate: boolean, bounds: IAABBBounds) => void; afterUpdateAABBBounds: ( @@ -60,6 +61,7 @@ export interface IGraphicService { params: { globalAABBBounds: IAABBBounds }, selfChange: boolean ) => void; + clearAABBBounds: (graphic: IGraphic, stage: IStage, b: IAABBBounds) => void; creator: IGraphicCreator; validCheck: ( diff --git a/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts b/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts index 4b9115062..9e4557724 100644 --- a/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts +++ b/packages/vrender-core/src/plugins/builtin-plugin/dirty-bounds-plugin.ts @@ -59,6 +59,17 @@ export class DirtyBoundsPlugin implements IPlugin { stage.dirty(params.globalAABBBounds); } ); + application.graphicService.hooks.clearAABBBounds.tap( + this.key, + (graphic: IGraphic, stage: IStage, bounds: IAABBBounds) => { + if (!(stage && stage === this.pluginService.stage && stage.renderCount)) { + return; + } + if (stage) { + stage.dirty(bounds); + } + } + ); application.graphicService.hooks.onRemove.tap(this.key, (graphic: IGraphic) => { const stage = graphic.stage; if (!(stage && stage === this.pluginService.stage && stage.renderCount)) { @@ -78,6 +89,10 @@ export class DirtyBoundsPlugin implements IPlugin { application.graphicService.hooks.afterUpdateAABBBounds.taps.filter(item => { return item.name !== this.key; }); + application.graphicService.hooks.clearAABBBounds.taps = + application.graphicService.hooks.clearAABBBounds.taps.filter(item => { + return item.name !== this.key; + }); context.stage.hooks.afterRender.taps = context.stage.hooks.afterRender.taps.filter(item => { return item.name !== this.key; }); diff --git a/packages/vrender/__tests__/browser/src/pages/text.ts b/packages/vrender/__tests__/browser/src/pages/text.ts index 39e6f970b..2a8563858 100644 --- a/packages/vrender/__tests__/browser/src/pages/text.ts +++ b/packages/vrender/__tests__/browser/src/pages/text.ts @@ -190,8 +190,11 @@ export const page = () => { // scaleY: 2 }); graphics.push(text); - text.setAttributes({ visible: false }); - console.log(text.AABBBounds); + setTimeout(() => { + debugger; + text.setAttributes({ visible: false }); + console.log(text.AABBBounds); + }, 1000); const b = text.OBBBounds; const circle = createCircle({ x: (b.x1 + b.x2) / 2, @@ -235,6 +238,7 @@ export const page = () => { const stage = createStage({ canvas: 'main', autoRender: true, + disableDirtyBounds: false, pluginList: ['poptipForText'] }); From cd9c3752ceea53bc99e2969b5e41e81793d0b880 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 30 Sep 2024 11:16:54 +0800 Subject: [PATCH 03/28] fix: fix issue with dirtyBounds by calc globalAABBBounds --- .../vrender-core/src/graphic/graphic-service/graphic-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts index 5ad5802a3..cc5276501 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts @@ -379,6 +379,7 @@ export class DefaultGraphicService implements IGraphicService { if (!(graphic.valid && visible)) { // application.graphicService.beforeUpdateAABBBounds(graphic, graphic.stage, true, aabbBounds); if (!aabbBounds.empty()) { + aabbBounds.transformWithMatrix((graphic.parent as IGroup).globalTransMatrix); application.graphicService.clearAABBBounds(graphic, graphic.stage, aabbBounds); aabbBounds.clear(); } From c6c8c754041d8b1594df0658d82325f716a5ae91 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 30 Sep 2024 14:44:11 +0800 Subject: [PATCH 04/28] fix: fix bug where clearAABBSounds is called when graphic.parent is null --- .../vrender-core/src/graphic/graphic-service/graphic-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts index cc5276501..115c94ba6 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts @@ -379,7 +379,7 @@ export class DefaultGraphicService implements IGraphicService { if (!(graphic.valid && visible)) { // application.graphicService.beforeUpdateAABBBounds(graphic, graphic.stage, true, aabbBounds); if (!aabbBounds.empty()) { - aabbBounds.transformWithMatrix((graphic.parent as IGroup).globalTransMatrix); + graphic.parent && aabbBounds.transformWithMatrix((graphic.parent as IGroup).globalTransMatrix); application.graphicService.clearAABBBounds(graphic, graphic.stage, aabbBounds); aabbBounds.clear(); } From c38c9e3388ebcaa2793a2a120d915d82e98950b9 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Sat, 12 Oct 2024 15:58:25 +0800 Subject: [PATCH 05/28] fix: group bounds valid check ignore visible --- .../src/graphic/graphic-service/graphic-service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts index 115c94ba6..a184ff5bf 100644 --- a/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts +++ b/packages/vrender-core/src/graphic/graphic-service/graphic-service.ts @@ -370,7 +370,8 @@ export class DefaultGraphicService implements IGraphicService { return true; } - if (graphic.shadowRoot) { + // 是Group或者有影子节点的话,就直接认为是合法的 + if (graphic.shadowRoot || graphic.isContainer) { return true; } From 02286ea0b2ffb4a0c47b858003f3952823548aa4 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 9 Sep 2024 18:05:23 +0800 Subject: [PATCH 06/28] feat: sync animated attribute while call render func, closed #1416 --- ...-animate-sync-render_2024-09-09-10-04.json | 10 +++++++++ .../src/animate/Ticker/default-ticker.ts | 20 +++++++++++++---- packages/vrender-core/src/core/stage.ts | 22 +++++++++++++++++++ .../vrender-core/src/interface/animate.ts | 4 +++- packages/vrender-core/src/interface/stage.ts | 2 ++ 5 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 common/changes/@visactor/vrender-core/feat-animate-sync-render_2024-09-09-10-04.json diff --git a/common/changes/@visactor/vrender-core/feat-animate-sync-render_2024-09-09-10-04.json b/common/changes/@visactor/vrender-core/feat-animate-sync-render_2024-09-09-10-04.json new file mode 100644 index 000000000..13512d36e --- /dev/null +++ b/common/changes/@visactor/vrender-core/feat-animate-sync-render_2024-09-09-10-04.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "feat: sync animated attribute while call render func, closed #1416", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/packages/vrender-core/src/animate/Ticker/default-ticker.ts b/packages/vrender-core/src/animate/Ticker/default-ticker.ts index dad13c3d2..f485299db 100644 --- a/packages/vrender-core/src/animate/Ticker/default-ticker.ts +++ b/packages/vrender-core/src/animate/Ticker/default-ticker.ts @@ -1,4 +1,4 @@ -import { Logger } from '@visactor/vutils'; +import { EventEmitter, Logger } from '@visactor/vutils'; import type { ITickHandler, ITickerHandlerStatic, ITimeline, ITicker } from '../../interface'; import { application } from '../../application'; import type { TickerMode } from './type'; @@ -6,7 +6,7 @@ import { STATUS } from './type'; import { RAFTickHandler } from './raf-tick-handler'; import { TimeOutTickHandler } from './timeout-tick-handler'; -export class DefaultTicker implements ITicker { +export class DefaultTicker extends EventEmitter implements ITicker { protected interval: number; protected tickerHandler: ITickHandler; protected _mode: TickerMode; @@ -28,6 +28,7 @@ export class DefaultTicker implements ITicker { } constructor(timelines: ITimeline[] = []) { + super(); this.init(); this.lastFrameTime = -1; this.tickCounts = 0; @@ -195,13 +196,13 @@ export class DefaultTicker implements ITicker { this.stop(); return; } - this._handlerTick(handler); + this._handlerTick(); if (!once) { handler.tick(this.interval, this.handleTick); } }; - protected _handlerTick = (handler: ITickHandler) => { + protected _handlerTick = () => { // 具体执行函数 const tickerHandler = this.tickerHandler; const time = tickerHandler.getTime(); @@ -220,5 +221,16 @@ export class DefaultTicker implements ITicker { this.timelines.forEach(t => { t.tick(delta); }); + this.emit('afterTick'); }; + + /** + * 同步tick状态,需要手动触发tick执行,保证属性为走完动画的属性 + * 【注】grammar会设置属性到最终值,然后调用render,这时候需要VRender手动触发tick,保证属性为走完动画的属性,而不是Grammar设置上的属性 + */ + trySyncTickStatus() { + if (this.status === STATUS.RUNNING) { + this._handlerTick(); + } + } } diff --git a/packages/vrender-core/src/core/stage.ts b/packages/vrender-core/src/core/stage.ts index 1bc486af1..e16c43d50 100644 --- a/packages/vrender-core/src/core/stage.ts +++ b/packages/vrender-core/src/core/stage.ts @@ -197,6 +197,10 @@ export class Stage extends Group implements IStage { declare params: Partial; + // 是否在render之前执行了tick,如果没有执行,尝试执行tick用来应用动画属性,避免动画过程中随意赋值然后又调用同步render导致属性的突变 + // 第一次render不需要强行走动画 + protected tickedBeforeRender: boolean = true; + /** * 所有属性都具有默认值。 * Canvas为字符串或者Canvas元素,那么默认图层就会绑定到这个Canvas上 @@ -293,6 +297,7 @@ export class Stage extends Group implements IStage { if (params.background && isString(this._background) && this._background.includes('/')) { this.setAttributes({ background: this._background }); } + this.ticker.on('afterTick', this.afterTickCb); } pauseRender(sr: number = -1) { @@ -452,6 +457,18 @@ export class Stage extends Group implements IStage { this._afterRender && this._afterRender(stage); this._afterNextRenderCbs && this._afterNextRenderCbs.forEach(cb => cb(stage)); this._afterNextRenderCbs = null; + this.tickedBeforeRender = false; + }; + + protected afterTickCb = () => { + this.tickedBeforeRender = true; + // 性能模式不用立刻渲染 + if (this.params.optimize?.tickRenderMode === 'performance') { + // do nothing + } else { + // 不是rendering的时候,render + this.state !== 'rendering' && this.render(); + } }; setBeforeRender(cb: (stage: IStage) => void) { @@ -710,6 +727,10 @@ export class Stage extends Group implements IStage { this.timeline.resume(); const state = this.state; this.state = 'rendering'; + // 判断是否需要手动执行tick + if (!this.tickedBeforeRender) { + this.ticker.trySyncTickStatus(); + } this.layerService.prepareStageLayer(this); if (!this._skipRender) { this.lastRenderparams = params; @@ -968,6 +989,7 @@ export class Stage extends Group implements IStage { } this.window.release(); this.ticker.remTimeline(this.timeline); + this.ticker.removeListener('afterTick', this.afterTickCb); this.renderService.renderTreeRoots = []; } diff --git a/packages/vrender-core/src/interface/animate.ts b/packages/vrender-core/src/interface/animate.ts index ff01ba159..9fd85fcc7 100644 --- a/packages/vrender-core/src/interface/animate.ts +++ b/packages/vrender-core/src/interface/animate.ts @@ -1,3 +1,4 @@ +import type { EventEmitter } from '@visactor/vutils'; import type { AnimateMode, AnimateStatus, AnimateStepType } from '../common/enums'; import type { Releaseable } from './common'; import type { IGraphic } from './graphic'; @@ -327,7 +328,7 @@ export interface ITickerHandlerStatic { new (): ITickHandler; } -export interface ITicker { +export interface ITicker extends EventEmitter { setFPS?: (fps: number) => void; setInterval?: (interval: number) => void; getFPS?: () => number; @@ -343,4 +344,5 @@ export interface ITicker { stop: () => void; addTimeline: (timeline: ITimeline) => void; remTimeline: (timeline: ITimeline) => void; + trySyncTickStatus: () => void; } diff --git a/packages/vrender-core/src/interface/stage.ts b/packages/vrender-core/src/interface/stage.ts index ed8edf73b..c439cfe7a 100644 --- a/packages/vrender-core/src/interface/stage.ts +++ b/packages/vrender-core/src/interface/stage.ts @@ -105,6 +105,8 @@ export type IOptimizeType = { // 不存在dirtyBounds的时候,根据该配置判断是否关闭图元的超出边界判定 // 如果有dirtyBounds那么该配置不生效 disableCheckGraphicWidthOutRange?: boolean; + // tick渲染模式,effect会在tick之后立刻执行render,保证动画效果正常。performance模式中tick和render均是RAF,属性可能会被篡改 + tickRenderMode?: 'effect' | 'performance'; }; export interface IOption3D { From 85198e6e8e31af6cac645c66a89f6421967b02e0 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Tue, 27 Aug 2024 14:20:21 +0800 Subject: [PATCH 07/28] feat: support label overlap for inside arc labels --- ...-inside-label-support-overlap_2024-08-27-06-07.json | 10 ++++++++++ packages/vrender-components/src/label/arc.ts | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/feat-arc-inside-label-support-overlap_2024-08-27-06-07.json diff --git a/common/changes/@visactor/vrender-components/feat-arc-inside-label-support-overlap_2024-08-27-06-07.json b/common/changes/@visactor/vrender-components/feat-arc-inside-label-support-overlap_2024-08-27-06-07.json new file mode 100644 index 000000000..cca6e6ecb --- /dev/null +++ b/common/changes/@visactor/vrender-components/feat-arc-inside-label-support-overlap_2024-08-27-06-07.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-components", + "comment": " feat: support label overlap for inside arc labels", + "type": "none" + } + ], + "packageName": "@visactor/vrender-components" +} \ No newline at end of file diff --git a/packages/vrender-components/src/label/arc.ts b/packages/vrender-components/src/label/arc.ts index 9c9487f3a..f935fd928 100644 --- a/packages/vrender-components/src/label/arc.ts +++ b/packages/vrender-components/src/label/arc.ts @@ -148,6 +148,9 @@ export class ArcLabel extends LabelBase { } protected _overlapping(labels: (IText | IRichText)[]) { + if (['inside', 'inside-center'].includes(this.attribute.position as string)) { + return super._overlapping(labels); + } return labels; } From 008749ea1a60532cac1fe21404eeecd4eb675167 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 1 Nov 2024 17:52:22 +0800 Subject: [PATCH 08/28] fix: fix scrollbar-plugin --- .../vrender-components/src/scrollbar/index.ts | 1 + .../src/scrollbar/scrollbar-plugin.ts | 413 ++++++++++-------- 2 files changed, 240 insertions(+), 174 deletions(-) diff --git a/packages/vrender-components/src/scrollbar/index.ts b/packages/vrender-components/src/scrollbar/index.ts index e9e864d2a..3cd44fcb9 100644 --- a/packages/vrender-components/src/scrollbar/index.ts +++ b/packages/vrender-components/src/scrollbar/index.ts @@ -1,2 +1,3 @@ export * from './scrollbar'; export * from './type'; +export * from './module'; diff --git a/packages/vrender-components/src/scrollbar/scrollbar-plugin.ts b/packages/vrender-components/src/scrollbar/scrollbar-plugin.ts index e91d2aa52..3598af95c 100644 --- a/packages/vrender-components/src/scrollbar/scrollbar-plugin.ts +++ b/packages/vrender-components/src/scrollbar/scrollbar-plugin.ts @@ -2,7 +2,9 @@ import type { IGraphic, IGroup, IPlugin, IPluginService } from '@visactor/vrende import { Generator, injectable } from '@visactor/vrender-core'; import { ScrollBar } from './scrollbar'; import type { IAABBBounds } from '@visactor/vutils'; -import { AABBBounds, abs, max, min } from '@visactor/vutils'; +import { AABBBounds, abs, Bounds } from '@visactor/vutils'; +import { SCROLLBAR_EVENT } from '../constant'; +import type { ScrollBarAttributes } from './type'; // _showPoptip: 0-没有,1-添加,2-删除 @@ -19,9 +21,9 @@ export class ScrollBarPlugin implements IPlugin { pluginService: IPluginService; _uid: number = Generator.GenAutoIncrementId(); key: string = this.name + this._uid; - activeGraphic: IGraphic; - childrenBounds: IAABBBounds; + scrollContainer?: { g: IGroup; showH: boolean; showV: boolean }; scrollContainerBounds: IAABBBounds; + childrenBounds: IAABBBounds; static defaultParams: IParams = { timeout: 500 @@ -40,219 +42,282 @@ export class ScrollBarPlugin implements IPlugin { scroll = (e: { deltaX: number; deltaY: number; target: IGraphic }) => { const graphic = e.target as any; const data = this.getScrollContainer(graphic); - const { g: scrollContainer } = data; - let { showH, showV } = data; - if (!scrollContainer || scrollContainer.count === 1) { + + if (!data && !this.scrollContainer) { return; } - this.scrollContainerBounds = scrollContainer.AABBBounds.clone(); - if (abs(e.deltaX) > abs(e.deltaY)) { - showH = showH && true; - showV = showV && false; - } else { - showH = showH && false; - showV = showV && true; + + if (!data && this.scrollContainer) { + if (!this.scrollContainer.g.stage || this.scrollContainer.g.stage !== graphic.stage) { + return; + } + const newScrollContainer = this.formatScrollContainer(this.scrollContainer.g); + + if (!newScrollContainer) { + this.clearScrollbar(this.scrollContainer.g, 'all'); + // 删除老的scrollbar + return; + } + if (this.scrollContainer.showH && !newScrollContainer.showH) { + this.clearScrollbar(this.scrollContainer.g, 'horizontal'); + } + + if (this.scrollContainer.showV && !newScrollContainer.showV) { + this.clearScrollbar(this.scrollContainer.g, 'vertical'); + } + + this.scrollContainer = newScrollContainer; + } else if (data && this.scrollContainer && data.g !== this.scrollContainer.g) { + this.clearScrollbar(this.scrollContainer.g, 'all'); + } + + this.scrollContainer = data ?? this.scrollContainer; + + const scrollContainer = data.g; + const { width, height, scrollX = 0, scrollY = 0 } = scrollContainer.attribute; + let newScrollX = scrollX; + let newScrollY = scrollY; + let { showH, showV } = data; + this.scrollContainerBounds = new Bounds().set( + 0, + 0, + scrollContainer.attribute.width, + scrollContainer.attribute.height + ); + if (showH && showH) { + if (abs(e.deltaX) > abs(e.deltaY)) { + showH = showH && true; + showV = showV && false; + } else { + showH = showH && false; + showV = showV && true; + } } - scrollContainer.setAttributes({ - scrollX: showH ? (scrollContainer.attribute.scrollX || 0) + e.deltaX : scrollContainer.attribute.scrollX || 0, - scrollY: showV ? (scrollContainer.attribute.scrollY || 0) + e.deltaY : scrollContainer.attribute.scrollY || 0 - }); // 计算子元素的bounds const childrenBounds = this.childrenBounds; - const scrollContainerBounds = this.scrollContainerBounds; + childrenBounds.clear(); - scrollContainer.forEachChildren((c: IGraphic) => { - childrenBounds.union(c.AABBBounds); - }); - // 判断是否需要显示H或V,如果bounds完全在内部,那就不需要显示 - childrenBounds.transformWithMatrix(scrollContainer.transMatrix); - if (showH && scrollContainerBounds.x1 <= childrenBounds.x1 && scrollContainerBounds.x2 >= childrenBounds.x2) { - showH = false; - } + childrenBounds.set(0, 0, scrollContainer.AABBBounds.width(), scrollContainer.AABBBounds.height()); - if (showV && scrollContainerBounds.y1 <= childrenBounds.y1 && scrollContainerBounds.y2 >= childrenBounds.y2) { - showV = false; - } + const scrollWidth = childrenBounds.width(); + const scrollHeight = childrenBounds.height(); - // 转到当前坐标系下 - const m = scrollContainer.transMatrix; - scrollContainerBounds.translate(-m.e, -m.f); - childrenBounds.translate(-m.e, -m.f); - // 如果子元素的bounds小于scrollContainer,那么就扩充 if (showH) { - childrenBounds.x1 = min(childrenBounds.x1, scrollContainerBounds.x1); - childrenBounds.x2 = max(childrenBounds.x2, scrollContainerBounds.x2); + newScrollX = Math.max(Math.min((e.deltaX ?? 0) - scrollX, scrollWidth - width), 0); + } else { + newScrollX = -scrollX; } + if (showV) { - childrenBounds.y1 = min(childrenBounds.y1, scrollContainerBounds.y1); - childrenBounds.y2 = max(childrenBounds.y2, scrollContainerBounds.y2); + newScrollY = Math.max(Math.min((e.deltaY ?? 0) - scrollY, scrollHeight - height), 0); + } else { + newScrollY = -scrollY; } - childrenBounds.translate(scrollContainer.attribute.scrollX, scrollContainer.attribute.scrollY); - const shadowRoot = scrollContainer.shadowRoot ?? scrollContainer.attachShadow(); - const container = shadowRoot.createOrUpdateChild('scroll-bar', {}, 'group') as IGroup; - const { h, v, deltaH, deltaV } = this.addOrUpdateScroll(showH, showV, container, scrollContainer); + childrenBounds.translate(-newScrollX, -newScrollY); + + this.addOrUpdateScroll(showH, showV, scrollContainer.parent, scrollContainer); scrollContainer.setAttributes({ - scrollX: h ? scrollContainer.attribute.scrollX || 0 : (scrollContainer.attribute.scrollX || 0) + deltaH, - scrollY: v ? scrollContainer.attribute.scrollY || 0 : (scrollContainer.attribute.scrollY || 0) + deltaV + scrollX: -newScrollX, + scrollY: -newScrollY }); }; - addOrUpdateScroll( - showH: boolean, - showV: boolean, - container: IGroup, - scrollContainer: IGroup - ): { h: boolean; deltaH: number; v: boolean; deltaV: number } { - const scrollbars = container.children; - let h = false; - let v = false; - let deltaH = 0; - let deltaV = 0; + + handleScrollBarChange = (params: any) => { + if ( + !this.scrollContainer || + !this.scrollContainerBounds || + !this.childrenBounds || + !params || + !params.target || + !params.detail || + !params.detail.value + ) { + return; + } + const scrollbar = params.target; + const newRange = params.detail.value; + + if (scrollbar.attribute.direction === 'horizontal') { + const scrollWidth = this.childrenBounds.width(); + + this.scrollContainer.g.setAttributes({ scrollX: -newRange[0] * scrollWidth }); + } else { + const scrollHeight = this.childrenBounds.height(); + + this.scrollContainer.g.setAttributes({ scrollY: -newRange[0] * scrollHeight }); + } + }; + + initEventOfScrollbar(scrollContainer: IGroup, scrollbar: IGroup, isHorozntal?: boolean) { + scrollContainer.addEventListener('pointerover', () => { + scrollbar.setAttribute('visibleAll', true); + }); + scrollContainer.addEventListener('pointermove', () => { + scrollbar.setAttribute('visibleAll', true); + }); + scrollContainer.addEventListener('pointerout', () => { + scrollbar.setAttribute('visibleAll', false); + }); + scrollbar.addEventListener('pointerover', () => { + scrollbar.setAttribute('visibleAll', true); + }); + scrollbar.addEventListener('pointerout', () => { + scrollbar.setAttribute('visibleAll', true); + }); + + scrollbar.addEventListener('scrollUp', this.handleScrollBarChange); + scrollbar.addEventListener(SCROLLBAR_EVENT, this.handleScrollBarChange); + } + + addOrUpdateScroll(showH: boolean, showV: boolean, container: IGroup, scrollContainer: IGroup) { if (showH) { - const hScrollbar = scrollbars.filter((g: ScrollBar) => g.attribute.direction !== 'vertical')[0] as ScrollBar; - const d = this.addOrUpdateHScroll(this.scrollContainerBounds, container, hScrollbar); - h = d.valid; - deltaH = d.delta; - this.disappearScrollBar(hScrollbar, v); + const { scrollBar: hScrollbar, isUpdate } = this.addOrUpdateHScroll(scrollContainer, container, true); + + if (!isUpdate) { + this.initEventOfScrollbar(scrollContainer, hScrollbar, true); + } + } else { + this.clearScrollbar(scrollContainer, 'horizontal'); } if (showV) { - const vScrollbar = scrollbars.filter((g: ScrollBar) => g.attribute.direction === 'vertical')[0] as ScrollBar; - const d = this.addOrUpdateVScroll(this.scrollContainerBounds, container, vScrollbar); - v = d.valid; - deltaV = d.delta; - this.disappearScrollBar(vScrollbar, v); + const { scrollBar: vScrollbar, isUpdate } = this.addOrUpdateHScroll(scrollContainer, container, false); + + if (!isUpdate) { + this.initEventOfScrollbar(scrollContainer, vScrollbar, false); + } + } else { + this.clearScrollbar(scrollContainer, 'vertical'); } - return { - h, - deltaH, - v, - deltaV - }; } - addOrUpdateHScroll( - scrollContainerB: IAABBBounds, - container: IGroup, - scrollBar?: ScrollBar - ): { valid: boolean; delta: number } { + + getDirection(isHorozntal?: boolean) { + return isHorozntal ? 'horizontal' : 'vertical'; + } + + addOrUpdateHScroll(scrollContainer: IGroup, container: IGroup, isHorozntal?: boolean) { + const direction = this.getDirection(isHorozntal); + const name = `${scrollContainer.name ?? scrollContainer._uid}_${this.getDirection(isHorozntal)}_${this.name}`; + const scrollbars = container.children.filter((g: ScrollBar) => g.name === name); + let isUpdate = true; + let scrollBar = scrollbars[0] as ScrollBar; + + const { y = 0, dy = 0, x = 0, dx = 0, height, width, zIndex = 0 } = this.scrollContainer.g.attribute; + const attrs: Partial = { + x: 0, + y: 0, + direction, + zIndex: zIndex + 1, + visibleAll: true, + padding: [2, 0], + railStyle: { + fill: 'rgba(0, 0, 0, .1)' + }, + range: [0, 0.05] + }; + + if (isHorozntal) { + attrs.width = this.scrollContainerBounds.width(); + attrs.height = 12; + } else { + attrs.height = this.scrollContainerBounds.height(); + attrs.width = 12; + } + if (!scrollBar) { - scrollBar = new ScrollBar({ - direction: 'horizontal', - x: 0, - y: 0, - width: scrollContainerB.width(), - height: 12, - padding: [2, 0], - railStyle: { - fill: 'rgba(0, 0, 0, .1)' - }, - range: [0, 0.05] - }); + isUpdate = false; + + scrollBar = new ScrollBar(attrs as ScrollBarAttributes); + scrollBar.name = name; container.add(scrollBar); + (scrollBar as any).isScrollBar = true; + } else if (scrollbars.length > 1) { + scrollbars.forEach((child: IGraphic, index: number) => { + if (index) { + child.parent?.removeChild(child); + } + }); } - const b = scrollBar.AABBBounds; const childrenBounds = this.childrenBounds; - const y = scrollContainerB.y2 - b.height(); - - const ratio = Math.min(b.width() / this.childrenBounds.width(), 1); - let start = - ((scrollContainerB.x1 - childrenBounds.x1) / (childrenBounds.width() - scrollContainerB.width())) * (1 - ratio); - - let valid = true; - let delta = 0; - if (start < 0) { - start = 0; - valid = false; - delta = scrollContainerB.x1 - childrenBounds.x1; - } else if (start + ratio > 1) { - start = 1 - ratio; - valid = false; - delta = scrollContainerB.x1 - childrenBounds.x1 - (childrenBounds.width() - scrollContainerB.width()); + + if (isHorozntal) { + const ratio = Math.min(this.scrollContainerBounds.width() / childrenBounds.width(), 1); + const start = Math.max(Math.min(this.childrenBounds.x1 / this.childrenBounds.width(), 0), ratio - 1); + attrs.x = x + dx; + attrs.y = y + dy + height - this.scrollContainerBounds.height(); + attrs.range = [-start, -start + ratio]; + } else { + const ratio = Math.min(this.scrollContainerBounds.height() / childrenBounds.height(), 1); + const start = Math.max(Math.min(this.childrenBounds.y1 / this.childrenBounds.height(), 0), ratio - 1); + attrs.x = x + dx + width - this.scrollContainerBounds.width(); + attrs.y = y + dy; + attrs.range = [-start, -start + ratio]; } - scrollBar.setAttributes({ - y, - visibleAll: true, - range: [start, start + ratio] - }); + + scrollBar.setAttributes(attrs); return { - valid, - delta + scrollBar, + isUpdate }; } - addOrUpdateVScroll( - scrollContainerB: IAABBBounds, - container: IGroup, - scrollBar?: ScrollBar - ): { valid: boolean; delta: number } { - if (!scrollBar) { - scrollBar = new ScrollBar({ - direction: 'vertical', - x: 0, - y: 0, - width: 12, - height: scrollContainerB.height(), - padding: [2, 0], - railStyle: { - fill: 'rgba(0, 0, 0, .1)' - }, - range: [0, 0.05] - }); - container.add(scrollBar); - } - const b = scrollBar.AABBBounds; - const x = scrollContainerB.x2 - b.width(); - const childrenBounds = this.childrenBounds; - const ratio = Math.min(b.height() / childrenBounds.height(), 1); - let start = - ((scrollContainerB.y1 - childrenBounds.y1) / (childrenBounds.height() - scrollContainerB.height())) * (1 - ratio); - - let valid = true; - let delta = 0; - if (start < 0) { - start = 0; - valid = false; - delta = scrollContainerB.y1 - childrenBounds.y1; - } else if (start + ratio > 1) { - start = 1 - ratio; - valid = false; - delta = scrollContainerB.y1 - childrenBounds.y1 - (childrenBounds.height() - scrollContainerB.height()); + clearScrollbar(scrollContainer: IGroup, type: 'horizontal' | 'vertical' | 'all') { + if (!scrollContainer.parent) { + return; } - scrollBar.setAttributes({ - x, - visibleAll: true, - range: [start, start + ratio] + const scrollbarBars = scrollContainer.parent.children.filter((child: IGroup) => { + return (child as any).isScrollBar && (type === 'all' || (child.attribute as any).direction === type); + }); + + scrollbarBars.forEach((child: IGraphic) => { + child.parent.removeChild(child); }); - return { - valid, - delta - }; } - disappearScrollBar(scrollBar: ScrollBar, valid: boolean) { - if ((scrollBar as any)._plugin_timeout) { - clearTimeout((scrollBar as any)._plugin_timeout); + formatScrollContainer(g: IGraphic) { + if (!g || g.type !== 'group' || !g.attribute) { + return null; + } + + const { overflow, width, height } = (g as IGroup).attribute; + + if (!overflow || overflow === 'hidden') { + return null; } - (scrollBar as any)._plugin_timeout = setTimeout(() => { - scrollBar.setAttribute('visibleAll', false); - }, this.params.timeout ?? 0); + + let showH = false; + let showV = false; + + if (overflow === 'scroll') { + showH = true; + showV = true; + } else { + showH = overflow === 'scroll-x'; + showV = !showH; + } + + if (!g.AABBBounds.empty()) { + if (showH) { + showH = width < g.AABBBounds.width(); + } + + if (showV) { + showV = height < g.AABBBounds.height(); + } + } + + return showH || showV ? { g: g as IGroup, showH, showV } : null; } // 获取响应滚动的元素 getScrollContainer(graphic: IGraphic): { g: IGroup; showH: boolean; showV: boolean } | null { let g = graphic; while (g) { - if (g.attribute.overflow && g.attribute.overflow !== 'hidden') { - const overflow = g.attribute.overflow; - let showH = false; - let showV = false; - if (overflow === 'scroll') { - (showH = true), (showV = true); - } else { - showH = overflow === 'scroll-x'; - showV = !showH; - } - return { g: g as IGroup, showH, showV }; + const res = this.formatScrollContainer(g); + + if (res) { + return res; } g = g.parent; } From a475c81ea9a7001b156fae65b8264bcf11f087b7 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Tue, 27 Aug 2024 17:40:25 +0800 Subject: [PATCH 09/28] fix: optimize tagPointsUpdate clipRange effect --- .../vrender-core/src/animate/custom-animate.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/vrender-core/src/animate/custom-animate.ts b/packages/vrender-core/src/animate/custom-animate.ts index 2a12dbad8..7e063faa5 100644 --- a/packages/vrender-core/src/animate/custom-animate.ts +++ b/packages/vrender-core/src/animate/custom-animate.ts @@ -5,7 +5,6 @@ import { isArray, isNumber, isValidNumber, - max, pi, pi2, Point, @@ -19,7 +18,6 @@ import type { IArcGraphicAttribute, IArea, IAreaCacheItem, - IClipRangeByDimensionType, ICubicBezierCurve, ICurve, ICustomPath2D, @@ -715,6 +713,10 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg protected clipRange: number; protected clipRangeByDimension: 'x' | 'y'; protected segmentsCache: number[]; + protected curClipRange: number; + getCurrentClipRange() { + return this.curClipRange; + } constructor( from: any, @@ -830,6 +832,13 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg }); } + onFirstRun(): void { + const lastClipRange = this.target.attribute.clipRange; + if (isValidNumber(lastClipRange)) { + this.clipRange *= lastClipRange; + } + } + onUpdate(end: boolean, ratio: number, out: Record): void { // if not create new points, multi points animation might not work well. this.points = this.points.map((point, index) => { @@ -839,6 +848,7 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg }); if (this.clipRange) { out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; + this.curClipRange = out.clipRange; } if (this.segmentsCache && this.to.segments) { let start = 0; From 1a99107a7298abc0e1aba70accd2db99a822ed33 Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Tue, 27 Aug 2024 17:42:57 +0800 Subject: [PATCH 10/28] docs: update change log --- ...sUpdate-record-last-clipRange_2024-08-27-09-42.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@visactor/vrender-core/fix-TagPointsUpdate-record-last-clipRange_2024-08-27-09-42.json diff --git a/common/changes/@visactor/vrender-core/fix-TagPointsUpdate-record-last-clipRange_2024-08-27-09-42.json b/common/changes/@visactor/vrender-core/fix-TagPointsUpdate-record-last-clipRange_2024-08-27-09-42.json new file mode 100644 index 000000000..4a0fb5048 --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-TagPointsUpdate-record-last-clipRange_2024-08-27-09-42.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "fix: smooth out stuttering effects when multiple TagPointsUpdate instances execute concurrently", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file From 3d5bfa527255c013588080b8c61cc788d329567d Mon Sep 17 00:00:00 2001 From: xiaoluoHe Date: Tue, 27 Aug 2024 18:51:57 +0800 Subject: [PATCH 11/28] feat: optimize effect of shrinking points effect --- .../src/animate/custom-animate.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/vrender-core/src/animate/custom-animate.ts b/packages/vrender-core/src/animate/custom-animate.ts index 7e063faa5..3d3aa85a7 100644 --- a/packages/vrender-core/src/animate/custom-animate.ts +++ b/packages/vrender-core/src/animate/custom-animate.ts @@ -711,12 +711,9 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg protected interpolatePoints: [IPointLike, IPointLike][]; protected newPointAnimateType: 'grow' | 'appear' | 'clip'; protected clipRange: number; + protected shrinkClipRange: number; protected clipRangeByDimension: 'x' | 'y'; protected segmentsCache: number[]; - protected curClipRange: number; - getCurrentClipRange() { - return this.curClipRange; - } constructor( from: any, @@ -790,7 +787,11 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg this.clipRange = this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / this.toPoints[this.toPoints.length - 1][this.clipRangeByDimension]; - + if (this.clipRange === 1) { + this.shrinkClipRange = + this.toPoints[lastMatchedIndex][this.clipRangeByDimension] / + this.fromPoints[this.fromPoints.length - 1][this.clipRangeByDimension]; + } if (!isValidNumber(this.clipRange)) { this.clipRange = 0; } else { @@ -834,7 +835,7 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg onFirstRun(): void { const lastClipRange = this.target.attribute.clipRange; - if (isValidNumber(lastClipRange)) { + if (isValidNumber(lastClipRange * this.clipRange)) { this.clipRange *= lastClipRange; } } @@ -847,8 +848,18 @@ export class TagPointsUpdate extends ACustomAnimate<{ points?: IPointLike[]; seg return newPoint; }); if (this.clipRange) { + if (this.shrinkClipRange) { + // 折线变短 + if (!end) { + out.points = this.fromPoints; + out.clipRange = this.clipRange - (this.clipRange - this.shrinkClipRange) * ratio; + } else { + out.points = this.toPoints; + out.clipRange = 1; + } + return; + } out.clipRange = this.clipRange + (1 - this.clipRange) * ratio; - this.curClipRange = out.clipRange; } if (this.segmentsCache && this.to.segments) { let start = 0; From 0c47307922acf5e46f885df396964215e93e85d9 Mon Sep 17 00:00:00 2001 From: xile611 Date: Wed, 20 Nov 2024 16:10:57 +0800 Subject: [PATCH 12/28] fix: upgrade vutils to 0.19.0 --- common/config/rush/pnpm-lock.yaml | 42 +++++++++++------------ docs/demos/package.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/package.json | 2 +- packages/react-vrender/package.json | 2 +- packages/vrender-components/package.json | 6 ++-- packages/vrender-core/package.json | 2 +- packages/vrender-kits/package.json | 2 +- packages/vrender/package.json | 2 +- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 7f2f36628..723044d3b 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: '@visactor/vchart': 1.3.0 '@visactor/vgrammar': ~0.5.7 '@visactor/vrender': workspace:0.20.15 - '@visactor/vutils': ~0.18.18 + '@visactor/vutils': ~0.19.0 '@vitejs/plugin-react': 3.1.0 axios: ^1.4.0 chalk: ^3.0.0 @@ -38,7 +38,7 @@ importers: '@visactor/vchart': 1.3.0 '@visactor/vgrammar': 0.5.7 '@visactor/vrender': link:../packages/vrender - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 axios: 1.7.4 highlight.js: 11.10.0 lodash: 4.17.21 @@ -72,7 +72,7 @@ importers: '@types/react-dom': ^18.0.0 '@types/react-reconciler': ^0.28.2 '@visactor/vrender': workspace:0.20.15 - '@visactor/vutils': ~0.18.18 + '@visactor/vutils': ~0.19.0 '@vitejs/plugin-react': 3.1.0 cross-env: ^7.0.3 eslint: ~8.18.0 @@ -84,7 +84,7 @@ importers: vite: 3.2.6 dependencies: '@visactor/vrender': link:../vrender - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 react-reconciler: 0.29.2_react@18.3.1 tslib: 2.6.3 devDependencies: @@ -113,7 +113,7 @@ importers: '@types/react-dom': ^18.0.0 '@visactor/react-vrender': workspace:0.20.15 '@visactor/vrender': workspace:0.20.15 - '@visactor/vutils': ~0.18.18 + '@visactor/vutils': ~0.19.0 '@vitejs/plugin-react': 3.1.0 cross-env: ^7.0.3 eslint: ~8.18.0 @@ -126,7 +126,7 @@ importers: dependencies: '@visactor/react-vrender': link:../react-vrender '@visactor/vrender': link:../vrender - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 react-reconciler: 0.29.2_react@18.3.1 tslib: 2.6.3 devDependencies: @@ -155,7 +155,7 @@ importers: '@types/react-dom': ^18.0.0 '@visactor/vrender-core': workspace:0.20.15 '@visactor/vrender-kits': workspace:0.20.15 - '@visactor/vutils': ~0.18.18 + '@visactor/vutils': ~0.19.0 '@vitejs/plugin-react': 3.1.0 canvas: 2.11.2 cross-env: ^7.0.3 @@ -179,7 +179,7 @@ importers: '@types/jest': 26.0.24 '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 '@vitejs/plugin-react': 3.1.0_vite@3.2.6 canvas: 2.11.2 cross-env: 7.0.3 @@ -202,8 +202,8 @@ importers: '@types/jest': ^26.0.0 '@visactor/vrender-core': workspace:0.20.15 '@visactor/vrender-kits': workspace:0.20.15 - '@visactor/vscale': ~0.18.18 - '@visactor/vutils': ~0.18.18 + '@visactor/vscale': ~0.19.0 + '@visactor/vutils': ~0.19.0 cross-env: ^7.0.3 eslint: ~8.18.0 jest: ^26.0.0 @@ -215,8 +215,8 @@ importers: dependencies: '@visactor/vrender-core': link:../vrender-core '@visactor/vrender-kits': link:../vrender-kits - '@visactor/vscale': 0.18.18 - '@visactor/vutils': 0.18.18 + '@visactor/vscale': 0.19.0 + '@visactor/vutils': 0.19.0 devDependencies: '@internal/bundler': link:../../tools/bundler '@internal/eslint-config': link:../../share/eslint-config @@ -241,7 +241,7 @@ importers: '@types/jest': ^26.0.0 '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 - '@visactor/vutils': ~0.18.18 + '@visactor/vutils': ~0.19.0 '@vitejs/plugin-react': 3.1.0 color-convert: 2.0.1 cross-env: ^7.0.3 @@ -255,7 +255,7 @@ importers: typescript: 4.9.5 vite: 3.2.6 dependencies: - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 color-convert: 2.0.1 devDependencies: '@internal/bundler': link:../../tools/bundler @@ -288,7 +288,7 @@ importers: '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 '@visactor/vrender-core': workspace:0.20.15 - '@visactor/vutils': ~0.18.18 + '@visactor/vutils': ~0.19.0 '@vitejs/plugin-react': 3.1.0 canvas: 2.11.2 cross-env: ^7.0.3 @@ -302,7 +302,7 @@ importers: dependencies: '@resvg/resvg-js': 2.4.1 '@visactor/vrender-core': link:../vrender-core - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 roughjs: 4.5.2 devDependencies: '@internal/bundler': link:../../tools/bundler @@ -3422,10 +3422,10 @@ packages: '@visactor/vutils': 0.15.14 dev: false - /@visactor/vscale/0.18.18: - resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} + /@visactor/vscale/0.19.0: + resolution: {integrity: sha512-5xfxl2aego1BBPG631u2mUFKPo6EF4VqxwXhnmz/7IZoJCxXBbT3gU1aHNFa4Z8uZ08bH+6sPSbS5gP0bXhRHw==} dependencies: - '@visactor/vutils': 0.18.18 + '@visactor/vutils': 0.19.0 dev: false /@visactor/vutils/0.13.3: @@ -3444,8 +3444,8 @@ packages: eventemitter3: 4.0.7 dev: false - /@visactor/vutils/0.18.18: - resolution: {integrity: sha512-byEJefqxiCz3UWe+YedEVjsdPtnJOAtKdRYi4qT9ojgACdd6QqlWs53Eb7PlMZgWDxVxqkxJP2bZnRKw+ME0Xg==} + /@visactor/vutils/0.19.0: + resolution: {integrity: sha512-eqdMcSJAk81MOj7Kry5lx+56LcFa3NM2v1V5b7lso0WEBphQl/DXkwvpWUBJKuHKaXn6XV1R4M8tocWUXhAXdA==} dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 diff --git a/docs/demos/package.json b/docs/demos/package.json index ac7701cd5..4d2ec5d1c 100644 --- a/docs/demos/package.json +++ b/docs/demos/package.json @@ -12,7 +12,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@visactor/vrender-kits": "workspace:0.14.8", - "@visactor/vutils": "~0.18.17", + "@visactor/vutils": "~0.19.0", "d3-scale-chromatic": "^3.0.0", "lodash": "4.17.21", "dat.gui": "^0.7.9", diff --git a/docs/package.json b/docs/package.json index 1659d51a1..18891d77e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,7 @@ "dependencies": { "@arco-design/web-react": "2.46.1", "@visactor/vchart": "1.3.0", - "@visactor/vutils": "~0.18.18", + "@visactor/vutils": "~0.19.0", "@visactor/vgrammar": "~0.5.7", "@visactor/vrender": "workspace:0.20.15", "markdown-it": "^13.0.0", diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 8246d622a..6864dcd49 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -26,7 +26,7 @@ "dependencies": { "@visactor/vrender": "workspace:0.20.15", "@visactor/react-vrender": "workspace:0.20.15", - "@visactor/vutils": "~0.18.18", + "@visactor/vutils": "~0.19.0", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index f96bf02bb..d333af77f 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@visactor/vrender": "workspace:0.20.15", - "@visactor/vutils": "~0.18.18", + "@visactor/vutils": "~0.19.0", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index 7d734eb57..c9f7bf868 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -25,8 +25,8 @@ "build:spec-types": "rm -rf ./spec-types && tsc -p ./tsconfig.spec.json --declaration --emitDeclarationOnly --outDir ./spec-types" }, "dependencies": { - "@visactor/vutils": "~0.18.18", - "@visactor/vscale": "~0.18.18", + "@visactor/vutils": "~0.19.0", + "@visactor/vscale": "~0.19.0", "@visactor/vrender-core": "workspace:0.20.15", "@visactor/vrender-kits": "workspace:0.20.15" }, @@ -35,7 +35,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vscale": "~0.18.18", + "@visactor/vscale": "~0.19.0", "@types/jest": "^26.0.0", "jest": "^26.0.0", "jest-electron": "^0.1.12", diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index b18256ea2..5b88303cd 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "color-convert": "2.0.1", - "@visactor/vutils": "~0.18.18" + "@visactor/vutils": "~0.19.0" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index fcab86e77..2c7722792 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -20,7 +20,7 @@ "test": "" }, "dependencies": { - "@visactor/vutils": "~0.18.18", + "@visactor/vutils": "~0.19.0", "@visactor/vrender-core": "workspace:0.20.15", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2" diff --git a/packages/vrender/package.json b/packages/vrender/package.json index 7cf1b7e7b..fb32aed1c 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -32,7 +32,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vutils": "~0.18.18", + "@visactor/vutils": "~0.19.0", "canvas": "2.11.2", "react": "^18.0.0", "react-dom": "^18.0.0", From 03c0582e319ea92206c63e85efd3af27b44df9ea Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 20 Nov 2024 20:49:30 +0800 Subject: [PATCH 13/28] feat: use ascend and decent to make measure more accurate --- ...ccurate-measure-text_2024-11-20-12-49.json | 10 + ...ccurate-measure-text_2024-11-20-12-49.json | 10 + .../__tests__/unit/axis/line.test.ts | 4 +- .../unit/bugfix/legend-focus-layout.test.ts | 6 +- .../__tests__/unit/legend/discrete.test.ts | 2 +- .../contributions/textMeasure/AtextMeasure.ts | 190 +++++- .../core/contributions/textMeasure/layout.ts | 109 ++-- packages/vrender-core/src/graphic/config.ts | 1 + packages/vrender-core/src/graphic/text.ts | 600 ++++++------------ .../vrender-core/src/graphic/wrap-text.ts | 14 +- .../src/interface/graphic/text.ts | 12 +- packages/vrender-core/src/interface/text.ts | 9 +- .../contributions/render/text-render.ts | 257 +++----- .../__tests__/graphic/graphic-bounds.test.ts | 4 +- 14 files changed, 572 insertions(+), 656 deletions(-) create mode 100644 common/changes/@visactor/vrender-core/feat-accurate-measure-text_2024-11-20-12-49.json create mode 100644 common/changes/@visactor/vrender-kits/feat-accurate-measure-text_2024-11-20-12-49.json diff --git a/common/changes/@visactor/vrender-core/feat-accurate-measure-text_2024-11-20-12-49.json b/common/changes/@visactor/vrender-core/feat-accurate-measure-text_2024-11-20-12-49.json new file mode 100644 index 000000000..dcd273e76 --- /dev/null +++ b/common/changes/@visactor/vrender-core/feat-accurate-measure-text_2024-11-20-12-49.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-core", + "comment": "feat: use ascend and decent to make measure more accurate", + "type": "none" + } + ], + "packageName": "@visactor/vrender-core" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-kits/feat-accurate-measure-text_2024-11-20-12-49.json b/common/changes/@visactor/vrender-kits/feat-accurate-measure-text_2024-11-20-12-49.json new file mode 100644 index 000000000..567d68736 --- /dev/null +++ b/common/changes/@visactor/vrender-kits/feat-accurate-measure-text_2024-11-20-12-49.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vrender-kits", + "comment": "", + "type": "none" + } + ], + "packageName": "@visactor/vrender-kits" +} \ No newline at end of file diff --git a/packages/vrender-components/__tests__/unit/axis/line.test.ts b/packages/vrender-components/__tests__/unit/axis/line.test.ts index f0e510dff..11e61dc75 100644 --- a/packages/vrender-components/__tests__/unit/axis/line.test.ts +++ b/packages/vrender-components/__tests__/unit/axis/line.test.ts @@ -1050,14 +1050,14 @@ describe('Line Axis', () => { let firstLabel = axis.getElementsByName('axis-label')[0] as IText; let firstLayer = axis.getElementsByName('axis-label-container-layer-0')[0] as IGroup; expect(firstLabel.attribute.textBaseline).toBe('bottom'); - expect(firstLabel.attribute.y).toBe(firstLayer.AABBBounds.y2 - 2); + expect(firstLabel.attribute.y).toBe(firstLayer.AABBBounds.y2); axis.setAttribute('label', { containerAlign: 'middle' }); firstLabel = axis.getElementsByName('axis-label')[0] as IText; firstLayer = axis.getElementsByName('axis-label-container-layer-0')[0] as IGroup; expect(firstLabel.attribute.textBaseline).toBe('bottom'); expect((firstLabel.AABBBounds.y1 + firstLabel.AABBBounds.y2) / 2).toBeCloseTo( - (firstLayer.AABBBounds.y1 + firstLayer.AABBBounds.y2) * 0.5 - 2 + (firstLayer.AABBBounds.y1 + firstLayer.AABBBounds.y2) * 0.5 ); axis.setAttribute('label', { containerAlign: 'top' }); diff --git a/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts b/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts index cd95fe201..f2c64e52e 100644 --- a/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts +++ b/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts @@ -45,7 +45,7 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(422.05995178222656); + expect(legend.AABBBounds.width()).toBe(428.84796142578125); }); it('should not exceed the maximum width of the item, and the basic length exceeds, legend item without value', () => { @@ -71,7 +71,7 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(310); + expect(legend.AABBBounds.width()).toBe(320); }); it('should not exceed the maximum width of the item, and the basic length exceeds, legend item with focus', () => { @@ -133,7 +133,7 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(99.92627607073103); + expect(legend.AABBBounds.width()).toBe(100.71428571428572); }); it('should calculate when legend item just has label', () => { diff --git a/packages/vrender-components/__tests__/unit/legend/discrete.test.ts b/packages/vrender-components/__tests__/unit/legend/discrete.test.ts index 42c876851..4cad07d46 100644 --- a/packages/vrender-components/__tests__/unit/legend/discrete.test.ts +++ b/packages/vrender-components/__tests__/unit/legend/discrete.test.ts @@ -421,7 +421,7 @@ describe('DiscreteLegend', () => { expect((legend.getElementsByName('legendItem')[0] as IGroup).AABBBounds.width()).toBe(121.95); expect( (legend.getElementsByName('legendItem')[0].getElementsByName('legendItemLabel')[0] as IText)._AABBBounds.width() - ).toBeCloseTo(57.143951416015625); + ).toBeCloseTo(63.61000366210938); expect( (legend.getElementsByName('legendItem')[0].getElementsByName('legendItemValue')[0] as IText).attribute .maxLineWidth diff --git a/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts b/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts index 810e9ef04..cd5db1ba0 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts @@ -18,22 +18,43 @@ export class ATextMeasure implements ITextMeasure { service.bindTextMeasure(this); } - /** - * 获取text宽度,measureText.width - * @param text - * @param options - */ - measureTextWidth(text: string, options: TextOptionsType): number { - if (!this.context) { - return this.estimate(text, options).width; - } + protected _measureTextWithoutAlignBaseline(text: string, options: TextOptionsType, compatible?: boolean) { this.context.setTextStyleWithoutAlignBaseline(options); - const textMeasure = this.context.measureText(text); - return textMeasure.width; + const metrics = this.context.measureText(text); + + return compatible ? this.compatibleMetrics(metrics, options) : metrics; + } + + protected _measureTextWithAlignBaseline(text: string, options: TextOptionsType, compatible?: boolean) { + this.context.setTextStyle(options); + const metrics = this.context.measureText(text); + + return compatible ? this.compatibleMetrics(metrics, options) : metrics; + } + + protected compatibleMetrics(metrics: TextMetrics | { width: number }, options: TextOptionsType) { + if ( + (metrics as any).actualBoundingBoxAscent == null || + (metrics as any).actualBoundingBoxDescent == null || + (metrics as any).fontBoundingBoxAscent == null || + (metrics as any).fontBoundingBoxDescent == null + ) { + const { ascent, descent } = this.measureTextBoundADscentEstimate(options); + (metrics as any).actualBoundingBoxAscent = ascent; + (metrics as any).actualBoundingBoxDescent = descent; + (metrics as any).fontBoundingBoxAscent = ascent; + (metrics as any).fontBoundingBoxDescent = descent; + } + if ((metrics as any).actualBoundingBoxLeft == null || (metrics as any).actualBoundingBoxRight == null) { + const { left, right } = this.measureTextBoundLeftRightEstimate(options); + (metrics as any).actualBoundingBoxLeft = left; + (metrics as any).actualBoundingBoxRight = right; + } + return metrics; } // 估算文字长度 - estimate( + protected estimate( text: string, { fontSize = DefaultTextAttribute.fontSize }: TextOptionsType ): { width: number; height: number } { @@ -50,34 +71,167 @@ export class ATextMeasure implements ITextMeasure { }; } + /** + * 获取text宽度,measureText.width + * @param text + * @param options + */ + measureTextWidth(text: string, options: TextOptionsType, textMeasure?: TextMetrics | { width: number }): number { + if (!this.context) { + return this.estimate(text, options).width; + } + textMeasure = textMeasure ?? this._measureTextWithoutAlignBaseline(text, options); + return textMeasure.width; + } + /** + * 获取text宽度,measureText.width + * @param text + * @param options + */ + measureTextBoundsWidth( + text: string, + options: TextOptionsType, + textMeasure?: TextMetrics | { width: number } + ): number { + if (!this.context) { + return this.estimate(text, options).width; + } + textMeasure = textMeasure ?? this._measureTextWithoutAlignBaseline(text, options); + return textMeasure.width; + } + + measureTextBoundsLeftRight(text: string, options: TextOptionsType, textMeasure?: TextMetrics | { width: number }) { + if (!this.context) { + return this.measureTextBoundLeftRightEstimate(options); + } + textMeasure = textMeasure ?? this._measureTextWithAlignBaseline(text, options, true); + return { + left: (textMeasure as any).actualBoundingBoxLeft, + right: (textMeasure as any).actualBoundingBoxRight + }; + } + /** * 获取text像素高度,基于actualBoundingBoxAscent和actualBoundingBoxDescent * @param text * @param options */ - measureTextPixelHeight(text: string, options: TextOptionsType): number { + measureTextPixelHeight( + text: string, + options: TextOptionsType, + textMeasure?: TextMetrics | { width: number } + ): number { if (!this.context) { return options.fontSize ?? DefaultTextStyle.fontSize; } - this.context.setTextStyleWithoutAlignBaseline(options); - const textMeasure = this.context.measureText(text); + textMeasure = textMeasure ?? this._measureTextWithoutAlignBaseline(text, options, true); return Math.abs((textMeasure as any).actualBoundingBoxAscent - (textMeasure as any).actualBoundingBoxDescent); } + measureTextPixelADscent(text: string, options: TextOptionsType, textMeasure?: TextMetrics | { width: number }) { + if (!this.context) { + return this.measureTextBoundADscentEstimate(options); + } + textMeasure = textMeasure ?? this._measureTextWithAlignBaseline(text, options, true); + return { + ascent: (textMeasure as any).actualBoundingBoxAscent, + descent: (textMeasure as any).actualBoundingBoxDescent + }; + } + /** * 获取text包围盒的高度,基于fontBoundingBoxAscent和fontBoundingBoxDescent * @param text * @param options */ - measureTextBoundHieght(text: string, options: TextOptionsType): number { + measureTextBoundHieght( + text: string, + options: TextOptionsType, + textMeasure?: TextMetrics | { width: number } + ): number { if (!this.context) { return options.fontSize ?? DefaultTextStyle.fontSize; } - this.context.setTextStyleWithoutAlignBaseline(options); - const textMeasure = this.context.measureText(text); + textMeasure = textMeasure ?? this._measureTextWithoutAlignBaseline(text, options, true); return Math.abs((textMeasure as any).fontBoundingBoxAscent - (textMeasure as any).fontBoundingBoxDescent); } + measureTextBoundADscent(text: string, options: TextOptionsType, textMeasure?: TextMetrics | { width: number }) { + if (!this.context) { + return this.measureTextBoundADscentEstimate(options); + } + textMeasure = textMeasure ?? this._measureTextWithAlignBaseline(text, options, true); + return { + ascent: (textMeasure as any).fontBoundingBoxAscent, + descent: (textMeasure as any).fontBoundingBoxDescent + }; + } + + protected measureTextBoundADscentEstimate(options: TextOptionsType) { + const fontSize = options.fontSize ?? DefaultTextStyle.fontSize; + const { textBaseline } = options; + if (textBaseline === 'bottom') { + return { + ascent: fontSize, + descent: 0 + }; + } else if (textBaseline === 'middle') { + return { + ascent: fontSize / 2, + descent: fontSize / 2 + }; + } else if (textBaseline === 'alphabetic') { + return { + ascent: 0.79 * fontSize, + descent: 0.21 * fontSize + }; + } + + return { + ascent: 0, + descent: fontSize + }; + } + + protected measureTextBoundLeftRightEstimate(options: TextOptionsType) { + const fontSize = options.fontSize ?? DefaultTextStyle.fontSize; + const { textAlign } = options; + + if (textAlign === 'center') { + return { + left: fontSize / 2, + right: fontSize / 2 + }; + } else if (textAlign === 'right' || textAlign === 'end') { + return { + left: fontSize, + right: 0 + }; + } + return { + left: 0, + right: fontSize + }; + } + + measureTextPixelADscentAndWidth( + text: string, + options: TextOptionsType + ): { width: number; ascent: number; descent: number } { + if (!this.context) { + return { + ...this.measureTextBoundADscentEstimate(options), + width: this.estimate(text, options).width + }; + } + const out = this._measureTextWithoutAlignBaseline(text, options, true); + return { + ascent: (out as any).actualBoundingBoxAscent, + descent: (out as any).actualBoundingBoxDescent, + width: (out as any).width + }; + } + /** * 获取text测量对象 * @param text diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index a07963c2d..8dd8f6281 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -13,6 +13,13 @@ export class CanvasTextLayout { this.textMeasure = textMeasure; } + /** + * 布局外部的盒子,盒子的alphabetic属性模拟文字的效果 + * @param bbox + * @param textAlign + * @param textBaseline + * @returns + */ LayoutBBox(bbox: TextLayoutBBox, textAlign: TextAlignType, textBaseline: TextBaselineType): TextLayoutBBox { if (textAlign === 'left' || textAlign === 'start') { bbox.xOffset = 0; @@ -37,65 +44,6 @@ export class CanvasTextLayout { return bbox; } - GetLayout( - str: string, - width: number, - height: number, - textAlign: TextAlignType, - textBaseline: TextBaselineType, - lineHeight: number, - suffix: string, - wordBreak: boolean, - suffixPosition: 'start' | 'end' | 'middle' - ): LayoutType { - // 拆分str - const linesLayout: LayoutItemType[] = []; - // bbox高度可能大于totalHeight - const bboxWH: vec2 = [width, height]; - const bboxOffset: vec2 = [0, 0]; - - while (str.length > 0) { - const { str: clipText } = this.textMeasure.clipTextWithSuffix( - str, - this.textOptions, - width, - suffix, - wordBreak, - suffixPosition - ); - linesLayout.push({ - str: clipText, - width: this.textMeasure.measureTextWidth(clipText, this.textOptions) - }); - str = str.substring(clipText.length); - } - - if (textAlign === 'left' || textAlign === 'start') { - // origin[0] = 0; - } else if (textAlign === 'center') { - bboxOffset[0] = bboxWH[0] / -2; - } else if (textAlign === 'right' || textAlign === 'end') { - bboxOffset[0] = -bboxWH[0]; - } - - if (textBaseline === 'top') { - // origin[1] = 0; - } else if (textBaseline === 'middle') { - bboxOffset[1] = bboxWH[1] / -2; - } else if (textBaseline === 'bottom') { - bboxOffset[1] = -bboxWH[1]; - } - - const bbox: TextLayoutBBox = { - xOffset: bboxOffset[0], - yOffset: bboxOffset[1], - width: bboxWH[0], - height: bboxWH[1] - }; - - return this.layoutWithBBox(bbox, linesLayout, textAlign, textBaseline, lineHeight); - } - /** * 给定拆分好的每行字符串进行布局,如果传入lineWidth,那么后面的字符就拆分 * @param lines @@ -119,17 +67,23 @@ export class CanvasTextLayout { // 直接使用lineWidth,并拆分字符串 let width: number; for (let i = 0, len = lines.length; i < len; i++) { - width = Math.min(this.textMeasure.measureTextWidth(lines[i] as string, this.textOptions), lineWidth); + const metrics = this.textMeasure.measureTextPixelADscentAndWidth(lines[i] as string, this.textOptions); + width = Math.min(metrics.width, lineWidth); linesLayout.push({ - str: this.textMeasure.clipTextWithSuffix( - lines[i] as string, - this.textOptions, - width, - suffix, - wordBreak, - suffixPosition - ).str, - width + str: + metrics.width <= lineWidth + ? lines[i].toString() + : this.textMeasure.clipTextWithSuffix( + lines[i] as string, + this.textOptions, + width, + suffix, + wordBreak, + suffixPosition + ).str, + width, + ascent: metrics.ascent, + descent: metrics.descent }); } bboxWH[0] = lineWidth; @@ -140,9 +94,10 @@ export class CanvasTextLayout { let text: string; for (let i = 0, len = lines.length; i < len; i++) { text = lines[i] as string; - width = this.textMeasure.measureTextWidth(text, this.textOptions); + const metrics = this.textMeasure.measureTextPixelADscentAndWidth(lines[i] as string, this.textOptions); + width = metrics.width; lineWidth = Math.max(lineWidth, width); - linesLayout.push({ str: text, width }); + linesLayout.push({ str: text, width, ascent: metrics.ascent, descent: metrics.descent }); } bboxWH[0] = lineWidth; } @@ -162,6 +117,15 @@ export class CanvasTextLayout { return this.layoutWithBBox(bbox, linesLayout, textAlign, textBaseline, lineHeight); } + /** + * 给定了bbox,使用拆分好的每行字符串进行布局 + * @param bbox + * @param lines + * @param textAlign + * @param textBaseline + * @param lineHeight + * @returns + */ layoutWithBBox( bbox: TextLayoutBBox, lines: LayoutItemType[], @@ -221,8 +185,7 @@ export class CanvasTextLayout { line.leftOffset = bbox.width - line.width; } - // line.topOffset = lineHeight * 0.79 + origin[1]; // 渲染默认使用alphabetic - line.topOffset = (lineHeight - this.textOptions.fontSize) / 2 + this.textOptions.fontSize * 0.79 + origin[1]; + line.topOffset = lineHeight / 2 + (line.ascent - line.descent) / 2 + origin[1]; origin[1] += lineHeight; return line; diff --git a/packages/vrender-core/src/graphic/config.ts b/packages/vrender-core/src/graphic/config.ts index d42fee6e8..7abf481d1 100644 --- a/packages/vrender-core/src/graphic/config.ts +++ b/packages/vrender-core/src/graphic/config.ts @@ -88,6 +88,7 @@ export const DefaultStrokeStyle: IStrokeStyle = { export const DefaultTextStyle: Required = { text: '', maxLineWidth: Infinity, + maxWidth: Infinity, textAlign: 'left', textBaseline: 'alphabetic', fontSize: 16, diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index 6c7050312..f47460e42 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -13,6 +13,7 @@ import { boundStroke, TextDirection, verticalLayout } from './tools'; const TEXT_UPDATE_TAG_KEY = [ 'text', 'maxLineWidth', + 'maxWidth', // 多行文本要用到 'textAlign', 'textBaseline', @@ -49,6 +50,9 @@ export class Text extends Graphic implements IText { protected declare obbText?: Text; + /** + * 获取font字符串 + */ get font(): string { const textTheme = this.getGraphicTheme(); if (!this._font) { @@ -60,30 +64,31 @@ export class Text extends Graphic implements IText { get clipedText(): string | undefined { const attribute = this.attribute; const textTheme = this.getGraphicTheme(); - if (!this.isSimplify()) { - return undefined; - } - const { maxLineWidth = textTheme.maxLineWidth } = attribute; - if (!Number.isFinite(maxLineWidth)) { + const maxWidth = this.getMaxWidth(textTheme); + if (!Number.isFinite(maxWidth)) { return (attribute.text ?? textTheme.text).toString(); } this.tryUpdateAABBBounds(); return this.cache.clipedText; } + get clipedWidth(): number | undefined { - if (!this.isSimplify()) { - return undefined; - } this.tryUpdateAABBBounds(); return this.cache.clipedWidth; } + + /** + * 支持单行文本,横排纵排均支持 + * TODO 支持多行文本 + */ get cliped(): boolean | undefined { const textTheme = this.getGraphicTheme(); const attribute = this.attribute; - const { maxLineWidth = textTheme.maxLineWidth, text, whiteSpace = textTheme.whiteSpace } = attribute; - if (!Number.isFinite(maxLineWidth)) { + const maxWidth = this.getMaxWidth(textTheme); + if (!Number.isFinite(maxWidth)) { return false; } + const { text } = this.attribute; this.tryUpdateAABBBounds(); if (this.cache?.layoutData?.lines) { let mergedText = ''; @@ -102,19 +107,14 @@ export class Text extends Graphic implements IText { } return this.clipedText !== attribute.text.toString(); } + get multilineLayout(): LayoutType | undefined { - if (!this.isMultiLine) { - return undefined; - } this.tryUpdateAABBBounds(); return this.cache.layoutData; } - - // 是否是简单文字 - // 单行,横排 - private isSimplify(): boolean { - return !this.isMultiLine && this.attribute.direction !== 'vertical'; - } + /** + * 是否是多行文本 + */ get isMultiLine(): boolean { return Array.isArray(this.attribute.text) || this.attribute.whiteSpace === 'normal'; } @@ -125,9 +125,14 @@ export class Text extends Graphic implements IText { this.cache = {}; } + /** + * 图元属性合法,且文字不为空或者null/undefined + * @returns + */ isValid(): boolean { return super.isValid() && this._isValid(); } + protected _isValid(): boolean { const { text } = this.attribute; if (isArray(text)) { @@ -200,6 +205,105 @@ export class Text extends Graphic implements IText { return aabbBounds; } + /** + * 计算单行文字的bounds,可以缓存长度以及截取的文字 + * @param text + */ + updateSingallineAABBBounds(text: number | string): IAABBBounds { + this.updateMultilineAABBBounds([text]); + const layoutData = this.cache.layoutData; + if (layoutData) { + const line = layoutData.lines[0]; + this.cache.clipedText = line.str; + this.cache.clipedWidth = line.width; + } + return this._AABBBounds; + } + + /** + * 计算单行文字的bounds,可以缓存长度以及截取的文字 + * @param text + */ + protected updateMultilineAABBBounds(text: (number | string)[]): IAABBBounds { + const textTheme = this.getGraphicTheme(); + const { direction = textTheme.direction, underlineOffset = textTheme.underlineOffset } = this.attribute; + + const b = + direction === 'horizontal' + ? this.updateHorizontalMultilineAABBBounds(text) + : this.updateVerticalMultilineAABBBounds(text); + + if (direction === 'horizontal') { + if (underlineOffset) { + this._AABBBounds.add(this._AABBBounds.x1, this._AABBBounds.y2 + underlineOffset); + } + } + return b; + } + + /** + * 计算多行文字的bounds,缓存每行文字的布局位置 + * @param text + */ + updateHorizontalMultilineAABBBounds(text: (number | string)[]): IAABBBounds { + const textTheme = this.getGraphicTheme(); + + const attribute = this.attribute; + const { + fontFamily = textTheme.fontFamily, + textAlign = textTheme.textAlign, + textBaseline = textTheme.textBaseline, + fontSize = textTheme.fontSize, + fontWeight = textTheme.fontWeight, + ellipsis = textTheme.ellipsis, + maxLineWidth, + stroke = textTheme.stroke, + wrap = textTheme.wrap, + ignoreBuf = textTheme.ignoreBuf, + lineWidth = textTheme.lineWidth, + whiteSpace = textTheme.whiteSpace, + suffixPosition = textTheme.suffixPosition + } = attribute; + + // const buf = ignoreBuf ? 0 : 2; + const lineHeight = this.getLineHeight(attribute, textTheme); + + if (whiteSpace === 'normal' || wrap) { + return this.updateWrapAABBBounds(text); + } + if (!this.shouldUpdateShape() && this.cache?.layoutData) { + const bbox = this.cache.layoutData.bbox; + this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height); + if (stroke) { + this._AABBBounds.expand(lineWidth / 2); + } + return this._AABBBounds; + } + const textMeasure = application.graphicUtil.textMeasure; + const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily }, textMeasure); + const layoutData = layoutObj.GetLayoutByLines( + text, + textAlign, + textBaseline as any, + lineHeight, + ellipsis === true ? (textTheme.ellipsis as string) : ellipsis || undefined, + false, + maxLineWidth, + suffixPosition + ); + const { bbox } = layoutData; + this.cache.layoutData = layoutData; + this.clearUpdateShapeTag(); + + this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height); + + if (stroke) { + this._AABBBounds.expand(lineWidth / 2); + } + + return this._AABBBounds; + } + /** * 计算多行文字的bounds,缓存每行文字的布局位置 * 自动折行params.text是数组,因此只重新updateMultilineAABBBounds @@ -224,10 +328,10 @@ export class Text extends Graphic implements IText { heightLimit = 0, lineClamp } = this.attribute; - const lineHeight = - calculateLineHeight(this.attribute.lineHeight, this.attribute.fontSize || textTheme.fontSize) ?? - (this.attribute.fontSize || textTheme.fontSize); - const buf = ignoreBuf ? 0 : 2; + + // const buf = ignoreBuf ? 0 : 2; + const lineHeight = this.getLineHeight(this.attribute, textTheme); + if (!this.shouldUpdateShape() && this.cache?.layoutData) { const bbox = this.cache.layoutData.bbox; this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height); @@ -238,7 +342,8 @@ export class Text extends Graphic implements IText { } const textMeasure = application.graphicUtil.textMeasure; - const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily }, textMeasure as any) as any; + const textOptions = { fontSize, fontWeight, fontFamily }; + const layoutObj = new CanvasTextLayout(fontFamily, textOptions, textMeasure as any); // layoutObj内逻辑 const lines = isArray(text) ? (text.map(l => l.toString()) as string[]) : [text.toString()]; @@ -260,44 +365,36 @@ export class Text extends Graphic implements IText { for (let i = 0; i < lines.length; i++) { const str = lines[i] as string; let needCut = true; - // // 测量当前行宽度 - // width = Math.min( - // layoutObj.textMeasure.measureTextWidth(str, layoutObj.textOptions), - // maxLineWidth - // ); // 判断是否超过高度限制 if (i === lineCountLimit - 1) { // 当前行为最后一行,如果后面还有行,需要显示省略号 - const clip = layoutObj.textMeasure.clipTextWithSuffix( + const clip = textMeasure.clipTextWithSuffix( str, - layoutObj.textOptions, + textOptions, maxLineWidth, ellipsis, false, suffixPosition, i !== lines.length - 1 ); + const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions); linesLayout.push({ str: clip.str, - width: clip.width + width: clip.width, + ascent: matrics.ascent, + descent: matrics.descent }); break; // 不处理后续行 } // 测量截断位置 - const clip = layoutObj.textMeasure.clipText( - str, - layoutObj.textOptions, - maxLineWidth, - wordBreak !== 'break-all', - wordBreak === 'keep-all' - ); - if ((str !== '' && clip.str === '') || clip.wordBreaked) { + const clip = textMeasure.clipText(str, textOptions, maxLineWidth, wordBreak === 'break-word'); + if (str !== '' && clip.str === '') { if (ellipsis) { - const clipEllipsis = layoutObj.textMeasure.clipTextWithSuffix( + const clipEllipsis = textMeasure.clipTextWithSuffix( str, - layoutObj.textOptions, + textOptions, maxLineWidth, ellipsis, false, @@ -312,24 +409,17 @@ export class Text extends Graphic implements IText { } needCut = false; } - + const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions); linesLayout.push({ str: clip.str, - width: clip.width + width: clip.width, + ascent: matrics.ascent, + descent: matrics.descent }); - let cutLength = clip.str.length; - if (clip.wordBreaked && !(str !== '' && clip.str === '')) { - needCut = true; - cutLength = clip.wordBreaked; - } if (clip.str.length === str.length) { // 不需要截断 } else if (needCut) { - let newStr = str.substring(cutLength); - // 截断后,避免开头有空格很尬,去掉 - if (wordBreak === 'keep-all') { - newStr = newStr.replace(/^\s+/g, ''); - } + const newStr = str.substring(clip.str.length); lines.splice(i + 1, 0, newStr); } } @@ -349,30 +439,34 @@ export class Text extends Graphic implements IText { // 判断是否超过高度限制 if (i === lineCountLimit - 1) { // 当前行为最后一行 - const clip = layoutObj.textMeasure.clipTextWithSuffix( + const clip = textMeasure.clipTextWithSuffix( lines[i], - layoutObj.textOptions, + textOptions, maxLineWidth, ellipsis, false, suffixPosition ); + const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions); linesLayout.push({ str: clip.str, - width: clip.width + width: clip.width, + ascent: matrics.ascent, + descent: matrics.descent }); lineWidth = Math.max(lineWidth, clip.width); break; // 不处理后续行 } text = lines[i] as string; - width = layoutObj.textMeasure.measureTextWidth(text, layoutObj.textOptions, wordBreak === 'break-word'); + width = textMeasure.measureTextWidth(text, textOptions); lineWidth = Math.max(lineWidth, width); - linesLayout.push({ str: text, width }); + const matrics = textMeasure.measureTextPixelADscentAndWidth(text, textOptions); + linesLayout.push({ str: text, width, ascent: matrics.ascent, descent: matrics.descent }); } bboxWH[0] = lineWidth; } - bboxWH[1] = linesLayout.length * (lineHeight + buf); + bboxWH[1] = linesLayout.length * lineHeight; const bbox = { xOffset: 0, @@ -385,336 +479,8 @@ export class Text extends Graphic implements IText { const layoutData = layoutObj.layoutWithBBox(bbox, linesLayout, textAlign, textBaseline as any, lineHeight); - // const layoutData = layoutObj.GetLayoutByLines( - // text, - // textAlign, - // textBaseline as any, - // lineHeight, - // ellipsis === true ? (DefaultTextAttribute.ellipsis as string) : ellipsis || undefined, - // maxLineWidth - // ); - // const { bbox } = layoutData; - this.cache.layoutData = layoutData; - this.clearUpdateShapeTag(); - this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height); - - if (stroke) { - this._AABBBounds.expand(lineWidth / 2); - } - - return this._AABBBounds; - } - - /** - * 计算单行文字的bounds,可以缓存长度以及截取的文字 - * @param text - */ - updateSingallineAABBBounds(text: number | string): IAABBBounds { - const textTheme = this.getGraphicTheme(); - const { direction = textTheme.direction, underlineOffset = textTheme.underlineOffset } = this.attribute; - - const b = - direction === 'horizontal' - ? this.updateHorizontalSinglelineAABBBounds(text) - : this.updateVerticalSinglelineAABBBounds(text); - if (direction === 'horizontal') { - if (underlineOffset) { - this._AABBBounds.add(this._AABBBounds.x1, this._AABBBounds.y2 + underlineOffset); - } - } - return b; - } - - /** - * 计算单行文字的bounds,可以缓存长度以及截取的文字 - * @param text - */ - updateMultilineAABBBounds(text: (number | string)[]): IAABBBounds { - const textTheme = this.getGraphicTheme(); - const { direction = textTheme.direction, underlineOffset = textTheme.underlineOffset } = this.attribute; - - const b = - direction === 'horizontal' - ? this.updateHorizontalMultilineAABBBounds(text) - : this.updateVerticalMultilineAABBBounds(text); - - if (direction === 'horizontal') { - if (underlineOffset) { - this._AABBBounds.add(this._AABBBounds.x1, this._AABBBounds.y2 + underlineOffset); - } - } - return b; - } - - /** - * 计算单行文字的bounds,可以缓存长度以及截取的文字 - * @param text - */ - updateHorizontalSinglelineAABBBounds(text: number | string): IAABBBounds { - const textTheme = this.getGraphicTheme(); - const { wrap = textTheme.wrap } = this.attribute; - if (wrap) { - return this.updateWrapAABBBounds([text]); - } - - const textMeasure = application.graphicUtil.textMeasure; - let width: number; - let str: string; - const attribute = this.attribute; - const { - maxLineWidth = textTheme.maxLineWidth, - ellipsis = textTheme.ellipsis, - textAlign = textTheme.textAlign, - textBaseline = textTheme.textBaseline, - fontFamily = textTheme.fontFamily, - fontSize = textTheme.fontSize, - fontWeight = textTheme.fontWeight, - stroke = textTheme.stroke, - lineWidth = textTheme.lineWidth, - // wordBreak = textTheme.wordBreak, - ignoreBuf = textTheme.ignoreBuf, - whiteSpace = textTheme.whiteSpace, - suffixPosition = textTheme.suffixPosition - } = attribute; - if (whiteSpace === 'normal') { - return this.updateWrapAABBBounds(text); - } - const buf = ignoreBuf ? 0 : Math.max(2, fontSize * 0.075); - const textFontSize = attribute.fontSize || textTheme.fontSize; - const lineHeight = calculateLineHeight(attribute.lineHeight, textFontSize) ?? textFontSize + buf; - if (!this.shouldUpdateShape() && this.cache) { - width = this.cache.clipedWidth ?? 0; - const dx = textDrawOffsetX(textAlign, width); - const dy = textLayoutOffsetY(textBaseline, lineHeight, fontSize); - this._AABBBounds.set(dx, dy, dx + width, dy + lineHeight); - if (stroke) { - this._AABBBounds.expand(lineWidth / 2); - } - return this._AABBBounds; - } - - if (Number.isFinite(maxLineWidth)) { - if (ellipsis) { - const strEllipsis = (ellipsis === true ? textTheme.ellipsis : ellipsis) as string; - const data = textMeasure.clipTextWithSuffix( - text.toString(), - { fontSize, fontWeight, fontFamily }, - maxLineWidth, - strEllipsis, - false, - suffixPosition - ); - str = data.str; - width = data.width; - } else { - const data = textMeasure.clipText(text.toString(), { fontSize, fontWeight, fontFamily }, maxLineWidth, false); - str = data.str; - width = data.width; - } - this.cache.clipedText = str; - this.cache.clipedWidth = width; - // todo 计算原本的宽度 - } else { - width = textMeasure.measureTextWidth(text.toString(), { fontSize, fontWeight, fontFamily }); - this.cache.clipedText = text.toString(); - this.cache.clipedWidth = width; - } - this.clearUpdateShapeTag(); - - const dx = textDrawOffsetX(textAlign, width); - let lh = lineHeight; - if (application.global && application.global.isSafari()) { - // 如果是safari,那么需要额外增加高度 - lh += fontSize * 0.2; - } - const dy = textLayoutOffsetY(textBaseline, lh, fontSize, buf); - this._AABBBounds.set(dx, dy, dx + width, dy + lh); - - if (stroke) { - this._AABBBounds.expand(lineWidth / 2); - } - - return this._AABBBounds; - } - - getBaselineMapAlign(): Record { - return Text.baselineMapAlign; - } - - getAlignMapBaseline(): Record { - return Text.alignMapBaseline; - } - - static baselineMapAlign = { - top: 'left', - bottom: 'right', - middle: 'center' - }; - - static alignMapBaseline = { - left: 'top', - right: 'bottom', - center: 'middle' - }; - - /** - * 计算垂直布局的单行文字的bounds,可以缓存长度以及截取的文字 - * @param text - */ - updateVerticalSinglelineAABBBounds(text: number | string): IAABBBounds { - const textTheme = this.getGraphicTheme(); - const textMeasure = application.graphicUtil.textMeasure; - let width: number; - let str: string; - const attribute = this.attribute; - const { ignoreBuf = textTheme.ignoreBuf } = attribute; - const buf = ignoreBuf ? 0 : 2; - const { - maxLineWidth = textTheme.maxLineWidth, - ellipsis = textTheme.ellipsis, - fontSize = textTheme.fontSize, - fontWeight = textTheme.fontWeight, - fontFamily = textTheme.fontFamily, - stroke = textTheme.stroke, - lineWidth = textTheme.lineWidth, - verticalMode = textTheme.verticalMode, - suffixPosition = textTheme.suffixPosition - } = attribute; - - const lineHeight = - calculateLineHeight(attribute.lineHeight, attribute.fontSize || textTheme.fontSize) ?? - (attribute.fontSize || textTheme.fontSize) + buf; - - let { textAlign = textTheme.textAlign, textBaseline = textTheme.textBaseline } = attribute; - if (!verticalMode) { - const t = textAlign; - textAlign = (Text.baselineMapAlign as any)[textBaseline] ?? 'left'; - textBaseline = (Text.alignMapBaseline as any)[t] ?? 'top'; - } - if (!this.shouldUpdateShape() && this.cache) { - width = this.cache.clipedWidth; - const dx = textDrawOffsetX(textAlign, width); - const dy = textLayoutOffsetY(textBaseline, lineHeight, fontSize); - this._AABBBounds.set(dy, dx, dy + lineHeight, dx + width); - if (stroke) { - this._AABBBounds.expand(lineWidth / 2); - } - return this._AABBBounds; - } - - let verticalList: { text: string; width?: number; direction: TextDirection }[][] = [ - verticalLayout(text.toString()) - ]; - if (Number.isFinite(maxLineWidth)) { - if (ellipsis) { - const strEllipsis = (ellipsis === true ? textTheme.ellipsis : ellipsis) as string; - const data = textMeasure.clipTextWithSuffixVertical( - verticalList[0], - { fontSize, fontWeight, fontFamily }, - maxLineWidth, - strEllipsis, - false, - suffixPosition - ); - verticalList = [data.verticalList]; - width = data.width; - } else { - const data = textMeasure.clipTextVertical( - verticalList[0], - { fontSize, fontWeight, fontFamily }, - maxLineWidth, - false - ); - verticalList = [data.verticalList]; - width = data.width; - } - this.cache.verticalList = verticalList; - this.cache.clipedWidth = width; - // todo 计算原本的宽度 - } else { - width = 0; - verticalList[0].forEach(t => { - const w = - t.direction === TextDirection.HORIZONTAL - ? fontSize - : textMeasure.measureTextWidth(t.text, { fontSize, fontWeight, fontFamily }); - - width += w; - t.width = w; - }); - this.cache.verticalList = verticalList; - this.cache.clipedWidth = width; - } - this.clearUpdateShapeTag(); - - const dx = textDrawOffsetX(textAlign, width); - const dy = textLayoutOffsetY(textBaseline, lineHeight, fontSize); - this._AABBBounds.set(dy, dx, dy + lineHeight, dx + width); - - if (stroke) { - this._AABBBounds.expand(lineWidth / 2); - } - - return this._AABBBounds; - } - - /** - * 计算多行文字的bounds,缓存每行文字的布局位置 - * @param text - */ - updateHorizontalMultilineAABBBounds(text: (number | string)[]): IAABBBounds { - const textTheme = this.getGraphicTheme(); - const { wrap = textTheme.wrap } = this.attribute; - if (wrap) { - return this.updateWrapAABBBounds(text); - } - - const attribute = this.attribute; - const { - fontFamily = textTheme.fontFamily, - textAlign = textTheme.textAlign, - textBaseline = textTheme.textBaseline, - fontSize = textTheme.fontSize, - fontWeight = textTheme.fontWeight, - ellipsis = textTheme.ellipsis, - maxLineWidth, - stroke = textTheme.stroke, - // ignoreBuf = textTheme.ignoreBuf, - lineWidth = textTheme.lineWidth, - whiteSpace = textTheme.whiteSpace, - suffixPosition = textTheme.suffixPosition - } = attribute; - // const buf = ignoreBuf ? 0 : Math.max(2, fontSize * 0.075); - const lineHeight = - calculateLineHeight(attribute.lineHeight, attribute.fontSize || textTheme.fontSize) ?? - (attribute.fontSize || textTheme.fontSize); - if (whiteSpace === 'normal') { - return this.updateWrapAABBBounds(text); - } - if (!this.shouldUpdateShape() && this.cache?.layoutData) { - const bbox = this.cache.layoutData.bbox; - this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height); - if (stroke) { - this._AABBBounds.expand(lineWidth / 2); - } - return this._AABBBounds; - } - const textMeasure = application.graphicUtil.textMeasure; - const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily }, textMeasure); - const layoutData = layoutObj.GetLayoutByLines( - text, - textAlign, - textBaseline as any, - lineHeight, - ellipsis === true ? (textTheme.ellipsis as string) : ellipsis || undefined, - false, - maxLineWidth, - suffixPosition - ); - const { bbox } = layoutData; this.cache.layoutData = layoutData; this.clearUpdateShapeTag(); - this._AABBBounds.set(bbox.xOffset, bbox.yOffset, bbox.xOffset + bbox.width, bbox.yOffset + bbox.height); if (stroke) { @@ -733,8 +499,6 @@ export class Text extends Graphic implements IText { const textMeasure = application.graphicUtil.textMeasure; let width: number; const attribute = this.attribute; - const { ignoreBuf = textTheme.ignoreBuf } = attribute; - const buf = ignoreBuf ? 0 : 2; const { maxLineWidth = textTheme.maxLineWidth, ellipsis = textTheme.ellipsis, @@ -747,9 +511,9 @@ export class Text extends Graphic implements IText { verticalMode = textTheme.verticalMode, suffixPosition = textTheme.suffixPosition } = attribute; - const lineHeight = - calculateLineHeight(attribute.lineHeight, attribute.fontSize || textTheme.fontSize) ?? - (attribute.fontSize || textTheme.fontSize) + buf; + + const lineHeight = this.getLineHeight(attribute, textTheme); + let { textAlign = textTheme.textAlign, textBaseline = textTheme.textBaseline } = attribute; if (!verticalMode) { const t = textAlign; @@ -834,6 +598,28 @@ export class Text extends Graphic implements IText { return this._AABBBounds; } + // /** + // * 是否是简单文字 + // * 单行,横排 + // * @returns + // */ + // protected isSinglelineAndHorizontal(): boolean { + // return !this.isMultiLine && this.attribute.direction !== 'vertical'; + // } + + protected getMaxWidth(theme: ITextGraphicAttribute): number { + // 传入了maxLineWidth就优先使用,否则使用maxWidth + const attribute = this.attribute; + return attribute.maxLineWidth ?? attribute.maxWidth ?? theme.maxWidth; + } + + protected getLineHeight(attribute: ITextGraphicAttribute, textTheme: ITextGraphicAttribute) { + return ( + calculateLineHeight(attribute.lineHeight, attribute.fontSize || textTheme.fontSize) ?? + (attribute.fontSize || textTheme.fontSize) + ); + } + protected needUpdateTags(keys: string[], k = TEXT_UPDATE_TAG_KEY): boolean { return super.needUpdateTags(keys, k); } @@ -848,6 +634,34 @@ export class Text extends Graphic implements IText { getNoWorkAnimateAttr(): Record { return Text.NOWORK_ANIMATE_ATTR; } + + /** + * 用于垂直布局时align和baseline相互转换 + * @returns + */ + getBaselineMapAlign(): Record { + return Text.baselineMapAlign; + } + + /** + * 用于垂直布局时align和baseline相互转换 + * @returns + */ + getAlignMapBaseline(): Record { + return Text.alignMapBaseline; + } + + static baselineMapAlign = { + top: 'left', + bottom: 'right', + middle: 'center' + }; + + static alignMapBaseline = { + left: 'top', + right: 'bottom', + center: 'middle' + }; } export function createText(attributes: ITextGraphicAttribute): IText { diff --git a/packages/vrender-core/src/graphic/wrap-text.ts b/packages/vrender-core/src/graphic/wrap-text.ts index fc3a81313..f8b0d00e3 100644 --- a/packages/vrender-core/src/graphic/wrap-text.ts +++ b/packages/vrender-core/src/graphic/wrap-text.ts @@ -107,7 +107,9 @@ export class WrapText extends Text { ); linesLayout.push({ str: clip.str, - width: clip.width + width: clip.width, + ascent: 0, + descent: 0 }); break; // 不处理后续行 } @@ -141,7 +143,9 @@ export class WrapText extends Text { linesLayout.push({ str: clip.str, - width: clip.width + width: clip.width, + ascent: 0, + descent: 0 }); if (clip.str.length === str.length) { // 不需要截断 @@ -176,7 +180,9 @@ export class WrapText extends Text { ); linesLayout.push({ str: clip.str, - width: clip.width + width: clip.width, + ascent: 0, + descent: 0 }); lineWidth = Math.max(lineWidth, clip.width); break; // 不处理后续行 @@ -185,7 +191,7 @@ export class WrapText extends Text { text = lines[i] as string; width = layoutObj.textMeasure.measureTextWidth(text, layoutObj.textOptions, wordBreak === 'break-word'); lineWidth = Math.max(lineWidth, width); - linesLayout.push({ str: text, width }); + linesLayout.push({ str: text, width, ascent: 0, descent: 0 }); } bboxWH[0] = lineWidth; } diff --git a/packages/vrender-core/src/interface/graphic/text.ts b/packages/vrender-core/src/interface/graphic/text.ts index 62a98d5ae..dea01b5d6 100644 --- a/packages/vrender-core/src/interface/graphic/text.ts +++ b/packages/vrender-core/src/interface/graphic/text.ts @@ -1,4 +1,3 @@ -import type { IAABBBounds } from '@visactor/vutils'; import type { IGraphicAttribute, IGraphic } from '../graphic'; export interface TextLayoutBBox { @@ -13,6 +12,8 @@ export interface LayoutItemType { leftOffset?: number; // 该行距离左侧的偏移 topOffset?: number; // 该行距离右侧的偏移 width: number; + ascent: number; + descent: number; } export interface SimplifyLayoutType { @@ -33,6 +34,7 @@ export interface LayoutType { export type ITextAttribute = { text: string | number | string[] | number[]; maxLineWidth: number; + maxWidth: number; textAlign: TextAlignType; textBaseline: TextBaselineType; fontSize: number; @@ -62,11 +64,12 @@ export type ITextAttribute = { disableAutoClipedPoptip?: boolean; }; export type ITextCache = { - // 单行文本的时候缓存用 + // 单行文本的时候缓存(多行文本没有) clipedText?: string; clipedWidth?: number; - // 多行文本的布局缓存 + // 文本的布局缓存(单行文本也有) layoutData?: LayoutType; + // 垂直布局的列表 verticalList?: { text: string; width?: number; direction: number }[][]; }; @@ -89,9 +92,6 @@ export interface IText extends IGraphic { getBaselineMapAlign: () => Record; getAlignMapBaseline: () => Record; - - updateMultilineAABBBounds: (text: (number | string)[]) => IAABBBounds; - updateSingallineAABBBounds: (text: number | string) => IAABBBounds; } export type TextAlignType = 'left' | 'right' | 'center' | 'start' | 'end'; diff --git a/packages/vrender-core/src/interface/text.ts b/packages/vrender-core/src/interface/text.ts index 7d9a2030e..639e9ffa7 100644 --- a/packages/vrender-core/src/interface/text.ts +++ b/packages/vrender-core/src/interface/text.ts @@ -15,6 +15,10 @@ export interface ITextMeasure extends IContribution { measureTextWidth: (text: string, options: TextOptionsType) => number; measureTextPixelHeight: (text: string, options: TextOptionsType) => number; measureTextBoundHieght: (text: string, options: TextOptionsType) => number; + measureTextPixelADscentAndWidth: ( + text: string, + options: TextOptionsType + ) => { width: number; ascent: number; descent: number }; clipText: ( text: string, options: TextOptionsType, @@ -32,9 +36,10 @@ export interface ITextMeasure extends IContribution { text: string, options: TextOptionsType, width: number, - suffix: string, + suffix: string | boolean, wordBreak: boolean, - position: 'start' | 'end' | 'middle' + position: 'start' | 'end' | 'middle', + forceSuffix?: boolean ) => { str: string; width: number }; clipTextWithSuffixVertical: ( verticalList: { text: string; width?: number; direction: number }[], diff --git a/packages/vrender-core/src/render/contributions/render/text-render.ts b/packages/vrender-core/src/render/contributions/render/text-render.ts index 48bd80a77..24c108301 100644 --- a/packages/vrender-core/src/render/contributions/render/text-render.ts +++ b/packages/vrender-core/src/render/contributions/render/text-render.ts @@ -71,12 +71,6 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph y: originY = textAttribute.y } = text.attribute; - let { textAlign = textAttribute.textAlign, textBaseline = textAttribute.textBaseline } = text.attribute; - if (!verticalMode && direction === 'vertical') { - const t = textAlign; - textAlign = text.getBaselineMapAlign()[textBaseline] ?? ('left' as any); - textBaseline = text.getAlignMapBaseline()[t] ?? ('top' as any); - } const lineHeight = calculateLineHeight(text.attribute.lineHeight, fontSize) ?? fontSize; const data = this.valid(text, textAttribute, fillCb, strokeCb); @@ -139,7 +133,8 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph } else if (fVisible) { context.setCommonStyle(text, text.attribute, originX - x, originY - y, textAttribute); context.fillText(t, _x, _y, z); - this.drawUnderLine(underline, lineThrough, text, _x, _y, z, textAttribute, context); + // 垂直布局的情况下不支持下划线和中划线 + // this.drawUnderLine(underline, lineThrough, text, _x, _y, 0, 0, z, textAttribute, context); } } @@ -148,140 +143,94 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph context.setTransformForCurrent(); } }; - if (text.isMultiLine) { - context.setTextStyleWithoutAlignBaseline(text.attribute, textAttribute, z); - if (direction === 'horizontal') { - const { multilineLayout } = text; - if (!multilineLayout) { - context.highPerformanceRestore(); - return; - } // 如果不存在的话,需要render层自行布局 - const { xOffset, yOffset } = multilineLayout.bbox; - if (doStroke) { - if (strokeCb) { - strokeCb(context, text.attribute, textAttribute); - } else if (sVisible) { - context.setStrokeStyle(text, text.attribute, originX - x, originY - y, textAttribute); - multilineLayout.lines.forEach(line => { - context.strokeText( - line.str, - (line.leftOffset || 0) + xOffset + x, - (line.topOffset || 0) + yOffset + y, - z - ); - }); - } - } - if (doFill) { - if (fillCb) { - fillCb(context, text.attribute, textAttribute); - } else if (fVisible) { - context.setCommonStyle(text, text.attribute, originX - x, originY - y, textAttribute); - multilineLayout.lines.forEach(line => { - context.fillText(line.str, (line.leftOffset || 0) + xOffset + x, (line.topOffset || 0) + yOffset + y, z); - this.drawUnderLine( - underline, - lineThrough, - text, - (line.leftOffset || 0) + xOffset + x, - // y是基于alphabetic对齐的,这里-0.05是为了和不换行的文字保持效果一致 - (line.topOffset || 0) + yOffset + y - textDrawOffsetY('bottom', fontSize) - 0.05 * fontSize, - z, - textAttribute, - context, - { - width: line.width - } - ); - }); - } + context.setTextStyleWithoutAlignBaseline(text.attribute, textAttribute, z); + if (direction === 'horizontal') { + const { multilineLayout } = text; + if (!multilineLayout) { + context.highPerformanceRestore(); + return; + } // 如果不存在的话,需要render层自行布局 + const { xOffset, yOffset } = multilineLayout.bbox; + if (doStroke) { + if (strokeCb) { + strokeCb(context, text.attribute, textAttribute); + } else if (sVisible) { + context.setStrokeStyle(text, text.attribute, originX - x, originY - y, textAttribute); + multilineLayout.lines.forEach(line => { + context.strokeText(line.str, (line.leftOffset || 0) + xOffset + x, (line.topOffset || 0) + yOffset + y, z); + }); } - } else { - text.tryUpdateAABBBounds(); // 更新cache - const cache = text.cache; - const { verticalList } = cache; - context.textAlign = 'left'; - context.textBaseline = 'top'; - const totalHeight = lineHeight * verticalList.length; - let totalW = 0; - verticalList.forEach(verticalData => { - const _w = verticalData.reduce((a, b) => a + (b.width || 0), 0); - totalW = max(_w, totalW); - }); - let offsetY = 0; - let offsetX = 0; - if (textBaseline === 'bottom') { - offsetX = -totalHeight; - } else if (textBaseline === 'middle') { - offsetX = -totalHeight / 2; + } + if (doFill) { + if (fillCb) { + fillCb(context, text.attribute, textAttribute); + } else if (fVisible) { + context.setCommonStyle(text, text.attribute, originX - x, originY - y, textAttribute); + multilineLayout.lines.forEach(line => { + context.fillText(line.str, (line.leftOffset || 0) + xOffset + x, (line.topOffset || 0) + yOffset + y, z); + this.drawUnderLine( + underline, + lineThrough, + text, + (line.leftOffset || 0) + xOffset + x, + (line.topOffset || 0) + yOffset + y, + line.descent, + (line.descent - line.ascent) / 2, + z, + textAttribute, + context, + { + width: line.width + } + ); + }); } + } + } else { + let { textAlign = textAttribute.textAlign, textBaseline = textAttribute.textBaseline } = text.attribute; + if (!verticalMode) { + const t = textAlign; + textAlign = text.getBaselineMapAlign()[textBaseline] ?? ('left' as any); + textBaseline = text.getAlignMapBaseline()[t] ?? ('top' as any); + } + text.tryUpdateAABBBounds(); // 更新cache + const cache = text.cache; + const { verticalList } = cache; + context.textAlign = 'left'; + context.textBaseline = 'top'; + const totalHeight = lineHeight * verticalList.length; + let totalW = 0; + verticalList.forEach(verticalData => { + const _w = verticalData.reduce((a, b) => a + (b.width || 0), 0); + totalW = max(_w, totalW); + }); + let offsetY = 0; + let offsetX = 0; + if (textBaseline === 'bottom') { + offsetX = -totalHeight; + } else if (textBaseline === 'middle') { + offsetX = -totalHeight / 2; + } + if (textAlign === 'center') { + offsetY -= totalW / 2; + } else if (textAlign === 'right') { + offsetY -= totalW; + } + verticalList.forEach((verticalData, i) => { + const currentW = verticalData.reduce((a, b) => a + (b.width || 0), 0); + const dw = totalW - currentW; + let currentOffsetY = offsetY; if (textAlign === 'center') { - offsetY -= totalW / 2; + currentOffsetY += dw / 2; } else if (textAlign === 'right') { - offsetY -= totalW; + currentOffsetY += dw; } - verticalList.forEach((verticalData, i) => { - const currentW = verticalData.reduce((a, b) => a + (b.width || 0), 0); - const dw = totalW - currentW; - let currentOffsetY = offsetY; - if (textAlign === 'center') { - currentOffsetY += dw / 2; - } else if (textAlign === 'right') { - currentOffsetY += dw; - } - verticalData.forEach(item => { - const { text, width, direction } = item; - drawText(text, totalHeight - (i + 1) * lineHeight + offsetX, currentOffsetY, direction); - currentOffsetY += width; - }); + verticalData.forEach(item => { + const { text, width, direction } = item; + drawText(text, totalHeight - (i + 1) * lineHeight + offsetX, currentOffsetY, direction); + currentOffsetY += width; }); - } - } else { - if (direction === 'horizontal') { - context.setTextStyle(text.attribute, textAttribute, z); - const t = text.clipedText as string; - let dy = 0; - if (lineHeight !== fontSize) { - if (textBaseline === 'top') { - dy = (lineHeight - fontSize) / 2; - } else if (textBaseline === 'middle') { - // middle do nothing - } else if (textBaseline === 'bottom') { - dy = -(lineHeight - fontSize) / 2; - } else { - // alphabetic do nothing - // dy = (lineHeight - fontSize) / 2 - fontSize * 0.79; - } - } - drawText(t, 0, dy, 0); - } else { - text.tryUpdateAABBBounds(); // 更新cache - const cache = text.cache; - if (cache) { - context.setTextStyleWithoutAlignBaseline(text.attribute, textAttribute, z); - const { verticalList } = cache; - let offsetY = 0; - const totalW = verticalList[0].reduce((a, b) => a + (b.width || 0), 0); - let offsetX = 0; - if (textBaseline === 'bottom') { - offsetX = -lineHeight; - } else if (textBaseline === 'middle') { - offsetX = -lineHeight / 2; - } - if (textAlign === 'center') { - offsetY -= totalW / 2; - } else if (textAlign === 'right') { - offsetY -= totalW; - } - context.textAlign = 'left'; - context.textBaseline = 'top'; - verticalList[0].forEach(item => { - const { text, width, direction } = item; - drawText(text, offsetX, offsetY, direction); - offsetY += width; - }); - } - } + }); } transform3dMatrixToContextMatrix && this.restoreTransformUseContext2d(text, textAttribute, z, context); @@ -313,8 +262,10 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph underline: number, lineThrough: number, text: IText, - x: number, - y: number, + anchorX: number, + anchorY: number, + offsetUnderLineY: number, + offsetThroughLineY: number, z: number, textAttribute: Required, context: IContext2d, @@ -328,8 +279,8 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph const { textAlign = textAttribute.textAlign, - textBaseline = textAttribute.textBaseline, - fontSize = textAttribute.fontSize, + // textBaseline = textAttribute.textBaseline, + // fontSize = textAttribute.fontSize, fill = textAttribute.fill, opacity = textAttribute.opacity, underlineOffset = textAttribute.underlineOffset, @@ -339,29 +290,31 @@ export class DefaultCanvasTextRender extends BaseRender implements IGraph const isMulti = !isNil(multiOption); const w = isMulti ? multiOption!.width : text.clipedWidth; const offsetX = isMulti ? 0 : textDrawOffsetX(textAlign, w); - const offsetY = textLayoutOffsetY(isMulti ? 'alphabetic' : textBaseline, fontSize, fontSize); + // const offsetY = textLayoutOffsetY(isMulti ? 'alphabetic' : textBaseline, fontSize, fontSize); const attribute = { lineWidth: 0, stroke: fill, opacity, strokeOpacity: fillOpacity }; - let deltaY = isMulti ? -3 : 0; + // let deltaY = isMulti ? -3 : 0; if (underline) { attribute.lineWidth = underline; - context.setStrokeStyle(text, attribute, x, y, textAttribute); + context.setStrokeStyle(text, attribute, anchorX, anchorY, textAttribute); underlineDash && context.setLineDash(underlineDash); context.beginPath(); - const dy = y + offsetY + fontSize + underlineOffset + deltaY; - context.moveTo(x + offsetX, dy, z); - context.lineTo(x + offsetX + w, dy, z); + // const dy = y + offsetY + fontSize + underlineOffset + deltaY; + const dy = anchorY + offsetUnderLineY + underlineOffset; + context.moveTo(anchorX + offsetX, dy, z); + context.lineTo(anchorX + offsetX + w, dy, z); context.stroke(); } - if (isMulti) { - deltaY = -1; - } + // if (isMulti) { + // deltaY = -1; + // } if (lineThrough) { attribute.lineWidth = lineThrough; - context.setStrokeStyle(text, attribute, x, y, textAttribute); + context.setStrokeStyle(text, attribute, anchorX, anchorY, textAttribute); context.beginPath(); - const dy = y + offsetY + fontSize / 2 + deltaY; - context.moveTo(x + offsetX, dy, z); - context.lineTo(x + offsetX + w, dy, z); + // const dy = y + offsetY + fontSize / 2 + deltaY; + const dy = anchorY + offsetThroughLineY; + context.moveTo(anchorX + offsetX, dy, z); + context.lineTo(anchorX + offsetX + w, dy, z); context.stroke(); } } diff --git a/packages/vrender/__tests__/graphic/graphic-bounds.test.ts b/packages/vrender/__tests__/graphic/graphic-bounds.test.ts index 6d545f4fd..87e128fd0 100644 --- a/packages/vrender/__tests__/graphic/graphic-bounds.test.ts +++ b/packages/vrender/__tests__/graphic/graphic-bounds.test.ts @@ -314,9 +314,9 @@ describe('Graphic-Bounds', () => { }); expect(text.AABBBounds.x1).toBeCloseTo(100); - expect(text.AABBBounds.y1).toBeCloseTo(86.36); + expect(text.AABBBounds.y1).toBeCloseTo(87.36); expect(text.AABBBounds.x2).toBeCloseTo(185.390625); - expect(text.AABBBounds.y2).toBeCloseTo(104.36); + expect(text.AABBBounds.y2).toBeCloseTo(103.36); text = createText({ x: 100, From 14ee1c29b35205644584acdfa5bcda6fe44a1262 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 20 Nov 2024 22:01:18 +0800 Subject: [PATCH 14/28] fix: access keep-all strategy --- packages/vrender-core/src/graphic/text.ts | 17 ++++++++++++++--- packages/vrender-core/src/interface/text.ts | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index f47460e42..5cd6ad49c 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -389,8 +389,14 @@ export class Text extends Graphic implements IText { } // 测量截断位置 - const clip = textMeasure.clipText(str, textOptions, maxLineWidth, wordBreak === 'break-word'); - if (str !== '' && clip.str === '') { + const clip = textMeasure.clipText( + str, + textOptions, + maxLineWidth, + wordBreak !== 'break-all', + wordBreak === 'keep-all' + ); + if ((str !== '' && clip.str === '') || clip.wordBreaked) { if (ellipsis) { const clipEllipsis = textMeasure.clipTextWithSuffix( str, @@ -416,10 +422,15 @@ export class Text extends Graphic implements IText { ascent: matrics.ascent, descent: matrics.descent }); + let cutLength = clip.str.length; + if (clip.wordBreaked && !(str !== '' && clip.str === '')) { + needCut = true; + cutLength = clip.wordBreaked; + } if (clip.str.length === str.length) { // 不需要截断 } else if (needCut) { - const newStr = str.substring(clip.str.length); + const newStr = str.substring(cutLength); lines.splice(i + 1, 0, newStr); } } diff --git a/packages/vrender-core/src/interface/text.ts b/packages/vrender-core/src/interface/text.ts index 639e9ffa7..be0a56d45 100644 --- a/packages/vrender-core/src/interface/text.ts +++ b/packages/vrender-core/src/interface/text.ts @@ -25,7 +25,7 @@ export interface ITextMeasure extends IContribution { width: number, wordBreak: boolean, keepAllBreak?: boolean - ) => { str: string; width: number }; + ) => { str: string; width: number; wordBreaked?: number }; clipTextVertical: ( verticalList: { text: string; width?: number; direction: number }[], options: TextOptionsType, From fea3e4edb3a005af2be54ba54740e92ad40848ef Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 22 Nov 2024 10:16:29 +0800 Subject: [PATCH 15/28] feat: support MeasureModeEnum and set default to fontBounding --- .../contributions/textMeasure/AtextMeasure.ts | 87 +++- .../core/contributions/textMeasure/layout.ts | 27 +- packages/vrender-core/src/graphic/config.ts | 64 +-- packages/vrender-core/src/graphic/text.ts | 37 +- .../src/interface/graphic/text.ts | 8 + packages/vrender-core/src/interface/text.ts | 5 +- .../__tests__/browser/src/pages/text.ts | 455 +++++++++++------- 7 files changed, 436 insertions(+), 247 deletions(-) diff --git a/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts b/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts index cd5db1ba0..400d7799c 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts @@ -1,6 +1,7 @@ import { injectable } from '../../../common/inversify-lite'; import type { IGraphicUtil } from '../../../interface/core'; import type { ICanvas, IContext2d, EnvType } from '../../../interface'; +import { MeasureModeEnum } from '../../../interface'; import type { TextOptionsType, ITextMeasure } from '../../../interface/text'; import { DefaultTextAttribute, DefaultTextStyle } from '../../../graphic/config'; import { testLetter } from '../../../graphic/richtext/utils'; @@ -169,28 +170,32 @@ export class ATextMeasure implements ITextMeasure { protected measureTextBoundADscentEstimate(options: TextOptionsType) { const fontSize = options.fontSize ?? DefaultTextStyle.fontSize; - const { textBaseline } = options; - if (textBaseline === 'bottom') { - return { - ascent: fontSize, - descent: 0 - }; - } else if (textBaseline === 'middle') { - return { - ascent: fontSize / 2, - descent: fontSize / 2 - }; - } else if (textBaseline === 'alphabetic') { - return { - ascent: 0.79 * fontSize, - descent: 0.21 * fontSize - }; - } - return { - ascent: 0, - descent: fontSize + ascent: 0.79 * fontSize, + descent: 0.21 * fontSize }; + // const { textBaseline } = options; + // if (textBaseline === 'bottom') { + // return { + // ascent: fontSize, + // descent: 0 + // }; + // } else if (textBaseline === 'middle') { + // return { + // ascent: fontSize / 2, + // descent: fontSize / 2 + // }; + // } else if (textBaseline === 'alphabetic') { + // return { + // ascent: 0.79 * fontSize, + // descent: 0.21 * fontSize + // }; + // } + + // return { + // ascent: 0, + // descent: fontSize + // }; } protected measureTextBoundLeftRightEstimate(options: TextOptionsType) { @@ -216,7 +221,8 @@ export class ATextMeasure implements ITextMeasure { measureTextPixelADscentAndWidth( text: string, - options: TextOptionsType + options: TextOptionsType, + mode: MeasureModeEnum ): { width: number; ascent: number; descent: number } { if (!this.context) { return { @@ -225,6 +231,45 @@ export class ATextMeasure implements ITextMeasure { }; } const out = this._measureTextWithoutAlignBaseline(text, options, true); + + if (mode === MeasureModeEnum.actualBounding) { + return { + ascent: (out as any).actualBoundingBoxAscent, + descent: (out as any).actualBoundingBoxDescent, + width: (out as any).width + }; + } else if (mode === MeasureModeEnum.estimate) { + return { + ...this.measureTextBoundADscentEstimate(options), + width: (out as any).width + }; + } else if (mode === MeasureModeEnum.fontBounding) { + const { lineHeight = options.fontSize } = options; + let ratio = 1; + if (lineHeight) { + const fontBoundingHeight = (out as any).fontBoundingBoxAscent + (out as any).fontBoundingBoxDescent; + ratio = lineHeight / fontBoundingHeight; + } + // 避免二次矫正,应当保证所有字符组合的基线都一样,否则fontBounding就失去意义了 + // 但如果超出边界了,就只能进行二次矫正 + let ascent = (out as any).fontBoundingBoxAscent * ratio; + let descent = (out as any).fontBoundingBoxDescent * ratio; + // 只能一边超出,都超出的话目前无法矫正,因为行高不能超 + if ((out as any).actualBoundingBoxDescent && descent < (out as any).actualBoundingBoxDescent) { + const delta = (out as any).actualBoundingBoxDescent - descent; + descent += delta; + ascent -= delta; + } else if ((out as any).actualBoundingBoxAscent && ascent < (out as any).actualBoundingBoxAscent) { + const delta = (out as any).actualBoundingBoxAscent - ascent; + ascent += delta; + descent -= delta; + } + return { + ascent, + descent, + width: (out as any).width + }; + } return { ascent: (out as any).actualBoundingBoxAscent, descent: (out as any).actualBoundingBoxDescent, diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 8dd8f6281..dce318f8d 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -1,6 +1,7 @@ import type { vec2 } from '@visactor/vutils'; import type { ITextMeasure, TextOptionsType } from '../../../interface/text'; import type { TextLayoutBBox, LayoutItemType, LayoutType, TextAlignType, TextBaselineType } from '../../../interface'; +import { MeasureModeEnum } from '../../../interface'; export class CanvasTextLayout { private fontFamily: string; @@ -56,9 +57,13 @@ export class CanvasTextLayout { lineHeight: number, suffix: string = '', wordBreak: boolean, - lineWidth?: number, - suffixPosition: 'start' | 'end' | 'middle' = 'end' + params?: { + lineWidth?: number; + suffixPosition?: 'start' | 'end' | 'middle'; + measureMode?: MeasureModeEnum; + } ): LayoutType { + const { lineWidth, suffixPosition = 'end', measureMode = MeasureModeEnum.actualBounding } = params ?? {}; lines = lines.map(l => l.toString()) as string[]; const linesLayout: LayoutItemType[] = []; // bbox高度可能大于totalHeight @@ -67,7 +72,11 @@ export class CanvasTextLayout { // 直接使用lineWidth,并拆分字符串 let width: number; for (let i = 0, len = lines.length; i < len; i++) { - const metrics = this.textMeasure.measureTextPixelADscentAndWidth(lines[i] as string, this.textOptions); + const metrics = this.textMeasure.measureTextPixelADscentAndWidth( + lines[i] as string, + this.textOptions, + measureMode + ); width = Math.min(metrics.width, lineWidth); linesLayout.push({ str: @@ -89,17 +98,21 @@ export class CanvasTextLayout { bboxWH[0] = lineWidth; } else { // 使用所有行中最长的作为lineWidth - lineWidth = 0; + let _lineWidth = 0; let width: number; let text: string; for (let i = 0, len = lines.length; i < len; i++) { text = lines[i] as string; - const metrics = this.textMeasure.measureTextPixelADscentAndWidth(lines[i] as string, this.textOptions); + const metrics = this.textMeasure.measureTextPixelADscentAndWidth( + lines[i] as string, + this.textOptions, + measureMode + ); width = metrics.width; - lineWidth = Math.max(lineWidth, width); + _lineWidth = Math.max(_lineWidth, width); linesLayout.push({ str: text, width, ascent: metrics.ascent, descent: metrics.descent }); } - bboxWH[0] = lineWidth; + bboxWH[0] = _lineWidth; } bboxWH[1] = linesLayout.length * lineHeight; diff --git a/packages/vrender-core/src/graphic/config.ts b/packages/vrender-core/src/graphic/config.ts index 7abf481d1..41c0223e4 100644 --- a/packages/vrender-core/src/graphic/config.ts +++ b/packages/vrender-core/src/graphic/config.ts @@ -1,36 +1,37 @@ // 存放公共属性 import { Logger, Matrix, pi2 } from '@visactor/vutils'; import { CustomPath2D } from '../common/custom-path2d'; -import type { - IArcGraphicAttribute, - IAreaGraphicAttribute, - IGraphicAttribute, - ICircleGraphicAttribute, - IFillStyle, - IGlyphGraphicAttribute, - IGroupGraphicAttribute, - IImageGraphicAttribute, - ILineGraphicAttribute, - IPathGraphicAttribute, - IPolygonGraphicAttribute, - IRect3dGraphicAttribute, - IRectGraphicAttribute, - IStrokeStyle, - IGraphicStyle, - ISymbolGraphicAttribute, - ITextAttribute, - ITextGraphicAttribute, - IRichTextGraphicAttribute, - ITransform, - RichTextWordBreak, - RichTextVerticalDirection, - RichTextGlobalAlignType, - RichTextGlobalBaselineType, - IRichTextIconGraphicAttribute, - IConnectedStyle, - ILayout, - IDebugType, - IPickStyle +import { + type IArcGraphicAttribute, + type IAreaGraphicAttribute, + type IGraphicAttribute, + type ICircleGraphicAttribute, + type IFillStyle, + type IGlyphGraphicAttribute, + type IGroupGraphicAttribute, + type IImageGraphicAttribute, + type ILineGraphicAttribute, + type IPathGraphicAttribute, + type IPolygonGraphicAttribute, + type IRect3dGraphicAttribute, + type IRectGraphicAttribute, + type IStrokeStyle, + type IGraphicStyle, + type ISymbolGraphicAttribute, + type ITextAttribute, + type ITextGraphicAttribute, + type IRichTextGraphicAttribute, + type ITransform, + type RichTextWordBreak, + type RichTextVerticalDirection, + type RichTextGlobalAlignType, + type RichTextGlobalBaselineType, + type IRichTextIconGraphicAttribute, + type IConnectedStyle, + type ILayout, + type IDebugType, + type IPickStyle, + MeasureModeEnum } from '../interface'; export const DefaultLayout: ILayout = { @@ -114,7 +115,8 @@ export const DefaultTextStyle: Required = { suffixPosition: 'end', underlineDash: [], underlineOffset: 0, - disableAutoClipedPoptip: undefined + disableAutoClipedPoptip: undefined, + measureMode: MeasureModeEnum.fontBounding }; export const DefaultPickStyle: IPickStyle = { diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index 5cd6ad49c..ef25b5d60 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -241,6 +241,10 @@ export class Text extends Graphic implements IText { return b; } + guessLineHeightBuf(fontSize: number) { + return fontSize ? fontSize * 0.1 : 0; + } + /** * 计算多行文字的bounds,缓存每行文字的布局位置 * @param text @@ -259,14 +263,15 @@ export class Text extends Graphic implements IText { maxLineWidth, stroke = textTheme.stroke, wrap = textTheme.wrap, - ignoreBuf = textTheme.ignoreBuf, + measureMode = textTheme.measureMode, lineWidth = textTheme.lineWidth, whiteSpace = textTheme.whiteSpace, - suffixPosition = textTheme.suffixPosition + suffixPosition = textTheme.suffixPosition, + ignoreBuf = textTheme.ignoreBuf } = attribute; - // const buf = ignoreBuf ? 0 : 2; - const lineHeight = this.getLineHeight(attribute, textTheme); + const buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize); + const lineHeight = this.getLineHeight(attribute, textTheme) + buf; if (whiteSpace === 'normal' || wrap) { return this.updateWrapAABBBounds(text); @@ -280,7 +285,7 @@ export class Text extends Graphic implements IText { return this._AABBBounds; } const textMeasure = application.graphicUtil.textMeasure; - const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily }, textMeasure); + const layoutObj = new CanvasTextLayout(fontFamily, { fontSize, fontWeight, fontFamily, lineHeight }, textMeasure); const layoutData = layoutObj.GetLayoutByLines( text, textAlign, @@ -288,8 +293,11 @@ export class Text extends Graphic implements IText { lineHeight, ellipsis === true ? (textTheme.ellipsis as string) : ellipsis || undefined, false, - maxLineWidth, - suffixPosition + { + lineWidth: maxLineWidth, + suffixPosition, + measureMode + } ); const { bbox } = layoutData; this.cache.layoutData = layoutData; @@ -324,13 +332,14 @@ export class Text extends Graphic implements IText { fontWeight = textTheme.fontWeight, // widthLimit, ignoreBuf = textTheme.ignoreBuf, + measureMode = textTheme.measureMode, suffixPosition = textTheme.suffixPosition, heightLimit = 0, lineClamp } = this.attribute; - // const buf = ignoreBuf ? 0 : 2; - const lineHeight = this.getLineHeight(this.attribute, textTheme); + const buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize); + const lineHeight = this.getLineHeight(this.attribute, textTheme) + buf; if (!this.shouldUpdateShape() && this.cache?.layoutData) { const bbox = this.cache.layoutData.bbox; @@ -342,7 +351,7 @@ export class Text extends Graphic implements IText { } const textMeasure = application.graphicUtil.textMeasure; - const textOptions = { fontSize, fontWeight, fontFamily }; + const textOptions = { fontSize, fontWeight, fontFamily, lineHeight }; const layoutObj = new CanvasTextLayout(fontFamily, textOptions, textMeasure as any); // layoutObj内逻辑 @@ -378,7 +387,7 @@ export class Text extends Graphic implements IText { suffixPosition, i !== lines.length - 1 ); - const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions); + const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions, measureMode); linesLayout.push({ str: clip.str, width: clip.width, @@ -415,7 +424,7 @@ export class Text extends Graphic implements IText { } needCut = false; } - const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions); + const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions, measureMode); linesLayout.push({ str: clip.str, width: clip.width, @@ -458,7 +467,7 @@ export class Text extends Graphic implements IText { false, suffixPosition ); - const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions); + const matrics = textMeasure.measureTextPixelADscentAndWidth(clip.str, textOptions, measureMode); linesLayout.push({ str: clip.str, width: clip.width, @@ -472,7 +481,7 @@ export class Text extends Graphic implements IText { text = lines[i] as string; width = textMeasure.measureTextWidth(text, textOptions); lineWidth = Math.max(lineWidth, width); - const matrics = textMeasure.measureTextPixelADscentAndWidth(text, textOptions); + const matrics = textMeasure.measureTextPixelADscentAndWidth(text, textOptions, measureMode); linesLayout.push({ str: text, width, ascent: matrics.ascent, descent: matrics.descent }); } bboxWH[0] = lineWidth; diff --git a/packages/vrender-core/src/interface/graphic/text.ts b/packages/vrender-core/src/interface/graphic/text.ts index dea01b5d6..75f00e65b 100644 --- a/packages/vrender-core/src/interface/graphic/text.ts +++ b/packages/vrender-core/src/interface/graphic/text.ts @@ -31,6 +31,12 @@ export interface LayoutType { textBaseline: TextBaselineType; } +export enum MeasureModeEnum { + estimate = 0, + actualBounding = 1, + fontBounding = 2 +} + export type ITextAttribute = { text: string | number | string[] | number[]; maxLineWidth: number; @@ -62,6 +68,8 @@ export type ITextAttribute = { // textDecorationWidth: number; // padding?: number | number[]; disableAutoClipedPoptip?: boolean; + // 测量模式,默认使用actualBounding + measureMode?: MeasureModeEnum; }; export type ITextCache = { // 单行文本的时候缓存(多行文本没有) diff --git a/packages/vrender-core/src/interface/text.ts b/packages/vrender-core/src/interface/text.ts index be0a56d45..c563aed87 100644 --- a/packages/vrender-core/src/interface/text.ts +++ b/packages/vrender-core/src/interface/text.ts @@ -1,5 +1,6 @@ import type { IContribution } from './contribution'; import type { IGraphicUtil } from './core'; +import type { MeasureModeEnum } from './graphic/text'; export interface TextOptionsType { fontSize?: number; @@ -7,6 +8,7 @@ export interface TextOptionsType { fontFamily?: string; fontStyle?: string; fontVariant?: string; + lineHeight?: number; textBaseline?: 'alphabetic' | 'top' | 'middle' | 'bottom'; textAlign?: 'left' | 'center' | 'right' | 'start' | 'end'; } @@ -17,7 +19,8 @@ export interface ITextMeasure extends IContribution { measureTextBoundHieght: (text: string, options: TextOptionsType) => number; measureTextPixelADscentAndWidth: ( text: string, - options: TextOptionsType + options: TextOptionsType, + mode: MeasureModeEnum ) => { width: number; ascent: number; descent: number }; clipText: ( text: string, diff --git a/packages/vrender/__tests__/browser/src/pages/text.ts b/packages/vrender/__tests__/browser/src/pages/text.ts index 2a8563858..89a48be96 100644 --- a/packages/vrender/__tests__/browser/src/pages/text.ts +++ b/packages/vrender/__tests__/browser/src/pages/text.ts @@ -50,190 +50,299 @@ function performance(stage: any) { export const page = () => { const graphics: IGraphic[] = []; - const t = createText({ - text: ['2022年世界国家和地区GDP总量 🚀'], - ellipsis: '...', - fill: 'linear-gradient(90deg, #215F97 0%, #FF948F 100%)', - fontSize: 24, - fontWeight: 'bold', - textAlign: 'center', - textBaseline: 'top', - width: 308, - lineHeight: '150%', - fontStyle: 'normal', - underline: 1, - stroke: 'transparent', - fontFamily: '', - wrap: true, - whiteSpace: 'no-wrap', - maxLineWidth: 308, - x: 154, - y: 0 - }); - console.log(t, t.cliped); - graphics.push(t); - // t.animate().to({ maxLineWidth: 0 }, 3000, 'linear'); + // const t = createText({ + // text: ['2022年世界国家和地区GDP总量 🚀'], + // ellipsis: '...', + // fill: 'linear-gradient(90deg, #215F97 0%, #FF948F 100%)', + // fontSize: 24, + // fontWeight: 'bold', + // textAlign: 'center', + // textBaseline: 'top', + // width: 308, + // lineHeight: '150%', + // fontStyle: 'normal', + // underline: 1, + // stroke: 'transparent', + // fontFamily: '', + // wrap: true, + // whiteSpace: 'no-wrap', + // maxLineWidth: 308, + // x: 154, + // y: 0 + // }); + // console.log(t, t.cliped); + // graphics.push(t); + // // t.animate().to({ maxLineWidth: 0 }, 3000, 'linear'); - const tt = createText({ - x: 971.9754981994629, - y: -213.8625716268361, - textAlign: 'center', - _debug_bounds: true, - textBaseline: 'middle', - text: ['细分'], - underline: 1, - underlineOffset: 0, - underlineDash: [2, 2], - fontSize: 16, - whiteSpace: 'normal', - graphicAlign: 'center', - graphicBaseline: 'middle', - fill: '#000', - ignoreBuf: true, - fontFamily: 'D-Din', - maxLineWidth: 120, - heightLimit: 999999, - // angle: 0.6, - // anchor: [971.9754981994629, -213.8625716268361], - visible: true, - background: '#F54A45' - }); - console.log(tt); - const g = createGroup({ - // angle: 0.6 - x: -600, - y: 600 - }); - g.add(tt); + // const tt = createText({ + // x: 971.9754981994629, + // y: -213.8625716268361, + // textAlign: 'center', + // _debug_bounds: true, + // textBaseline: 'middle', + // text: ['细分'], + // underline: 1, + // underlineOffset: 0, + // underlineDash: [2, 2], + // fontSize: 16, + // whiteSpace: 'normal', + // graphicAlign: 'center', + // graphicBaseline: 'middle', + // fill: '#000', + // ignoreBuf: true, + // fontFamily: 'D-Din', + // maxLineWidth: 120, + // heightLimit: 999999, + // // angle: 0.6, + // // anchor: [971.9754981994629, -213.8625716268361], + // visible: true, + // background: '#F54A45' + // }); + // console.log(tt); + // const g = createGroup({ + // // angle: 0.6 + // x: -600, + // y: 600 + // }); + // g.add(tt); + + // graphics.push(g); + + // graphics.push( + // createText({ + // x: 300, + // y: 200, + // // fill: { + // // gradient: 'linear', + // // x0: 0, + // // y0: 0, + // // x1: 1, + // // y1: 1, + // // stops: [ + // // { offset: 0, color: 'green' }, + // // { offset: 0.5, color: 'orange' }, + // // { offset: 1, color: 'red' } + // // ] + // // }, + // // background: 'red', + // // backgroundCornerRadius: 10, + // text: ['这是一行文字', '这是第二哈那个'], + // fill: 'red', + // maxLineWidth: 100, + // whiteSpace: 'normal', + // fontSize: 36, + // textBaseline: 'top' + // }) + // ); + // console.log('aaa', graphics[graphics.length - 1]); + // graphics.push( + // createLine({ + // x: 300, + // y: 200, + // // fill: { + // // gradient: 'linear', + // // x0: 0, + // // y0: 0, + // // x1: 1, + // // y1: 1, + // // stops: [ + // // { offset: 0, color: 'green' }, + // // { offset: 0.5, color: 'orange' }, + // // { offset: 1, color: 'red' } + // // ] + // // }, + // // background: 'red', + // // backgroundCornerRadius: 10, + // stroke: 'green', + // points: [ + // { x: -100, y: 0 }, + // { x: 300, y: 0 } + // ] + // }) + // ); - graphics.push(g); + // const text = createText({ + // x: 500, + // y: 200, + // fill: colorPools[5], + // // text: ['Tffg'], + // text: 'fkdalfffffffffffffffffjkllllll', + // wordBreak: 'break-word', + // maxLineWidth: 200, + // // ellipsis: '', + // direction: 'horizontal', + // angle: 0.6, + // stroke: 'green', + // // wordBreak: 'break-word', + // // maxLineWidth: 200, + // // ellipsis: '', + // // direction: 'vertical', + // // fontSize: 120, + // // stroke: 'green', + // // lineWidth: 100, + // // lineHeight: 30, + // // lineThrough: 1, + // // underline: 1, + // textAlign: 'left', + // textBaseline: 'middle' + // // textBaseline: 'bottom' + // // scaleX: 2, + // // scaleY: 2 + // }); + // graphics.push(text); + // setTimeout(() => { + // debugger; + // text.setAttributes({ visible: false }); + // console.log(text.AABBBounds); + // }, 1000); + // const b = text.OBBBounds; + // const circle = createCircle({ + // x: (b.x1 + b.x2) / 2, + // y: (b.y1 + b.y2) / 2, + // fill: 'black', + // radius: 2 + // }); + // graphics.push(circle); + // const rect = createRect({ + // x: b.x1, + // y: b.y1, + // width: b.width(), + // height: b.height(), + // stroke: 'red', + // anchor: [(b.x1 + b.x2) / 2, (b.y1 + b.y2) / 2], + // angle: 0.6, + // lineWidth: 1 + // }); + // graphics.push(rect); + + // const textLimit = createText({ + // x: 500, + // y: 300, + // fill: colorPools[5], + // // text: ['Tffg'], + // text: 'this is textaaaaaaaaaaaaaaaaa aaa this isisisisisis abc', + // // text: '这是textabc这aaaaa是什么这是阿萨姆abcaaaaabcdef这是textabc这aaaaa是什么这是阿萨姆abcaaaaa', + // // heightLimit: 40, + // wordBreak: 'keep-all', + // maxLineWidth: 100, + // stroke: 'green', + // textAlign: 'left', + // textBaseline: 'middle', + // whiteSpace: 'normal' + // // wrap: true + // }); + // console.log('textLimit', textLimit); + // graphics.push(textLimit); + + const list = [ + { + text: 'Mar', + textBaseline: 'bottom' + }, + { + text: 'May', + textBaseline: 'bottom' + }, + { + text: 'Mar', + textBaseline: 'middle' + }, + { + text: 'May', + textBaseline: 'middle' + }, + { + text: 'Sale', + textBaseline: 'middle' + }, + { + text: 'Sale_aa', + textBaseline: 'middle' + }, + { + text: 'ggg', + textBaseline: 'middle', + x: 500 + } + ]; + + graphics.push( + createLine({ + x: 0, + y: 300, + points: [ + { x: 0, y: 0 }, + { x: 1000, y: 0 } + ], + stroke: 'pink' + }) + ); graphics.push( - createText({ - x: 300, - y: 200, - // fill: { - // gradient: 'linear', - // x0: 0, - // y0: 0, - // x1: 1, - // y1: 1, - // stops: [ - // { offset: 0, color: 'green' }, - // { offset: 0.5, color: 'orange' }, - // { offset: 1, color: 'red' } - // ] - // }, - // background: 'red', - // backgroundCornerRadius: 10, - text: ['这是一行文字', '这是第二哈那个'], - fill: 'red', - maxLineWidth: 100, - whiteSpace: 'normal', - fontSize: 36, - textBaseline: 'top' + createLine({ + x: 0, + y: 400, + points: [ + { x: 0, y: 0 }, + { x: 1000, y: 0 } + ], + stroke: 'pink' }) ); - console.log('aaa', graphics[graphics.length - 1]); graphics.push( createLine({ - x: 300, - y: 200, - // fill: { - // gradient: 'linear', - // x0: 0, - // y0: 0, - // x1: 1, - // y1: 1, - // stops: [ - // { offset: 0, color: 'green' }, - // { offset: 0.5, color: 'orange' }, - // { offset: 1, color: 'red' } - // ] - // }, - // background: 'red', - // backgroundCornerRadius: 10, - stroke: 'green', + x: 0, + y: 500, points: [ - { x: -100, y: 0 }, - { x: 300, y: 0 } - ] + { x: 0, y: 0 }, + { x: 1000, y: 0 } + ], + stroke: 'pink' }) ); - const text = createText({ - x: 500, - y: 200, - fill: colorPools[5], - // text: ['Tffg'], - text: 'fkdalfffffffffffffffffjkllllll', - wordBreak: 'break-word', - maxLineWidth: 200, - // ellipsis: '', - direction: 'horizontal', - angle: 0.6, - stroke: 'green', - // wordBreak: 'break-word', - // maxLineWidth: 200, - // ellipsis: '', - // direction: 'vertical', - // fontSize: 120, - // stroke: 'green', - // lineWidth: 100, - // lineHeight: 30, - // lineThrough: 1, - // underline: 1, - textAlign: 'left', - textBaseline: 'middle' - // textBaseline: 'bottom' - // scaleX: 2, - // scaleY: 2 - }); - graphics.push(text); - setTimeout(() => { - debugger; - text.setAttributes({ visible: false }); - console.log(text.AABBBounds); - }, 1000); - const b = text.OBBBounds; - const circle = createCircle({ - x: (b.x1 + b.x2) / 2, - y: (b.y1 + b.y2) / 2, - fill: 'black', - radius: 2 + list.forEach((item, index) => { + graphics.push( + createText({ + x: 50 + index * 50, + y: 300, + fill: 'red', + measureMode: 0, + ignoreBuf: true, + fontSize: 22, + _debug_bounds: true, + ...item + }) + ); }); - graphics.push(circle); - - const rect = createRect({ - x: b.x1, - y: b.y1, - width: b.width(), - height: b.height(), - stroke: 'red', - anchor: [(b.x1 + b.x2) / 2, (b.y1 + b.y2) / 2], - angle: 0.6, - lineWidth: 1 + list.forEach((item, index) => { + graphics.push( + createText({ + x: 50 + index * 50, + y: 400, + fill: 'red', + measureMode: 2, + ignoreBuf: true, + fontSize: 22, + _debug_bounds: true, + ...item + }) + ); }); - graphics.push(rect); - - const textLimit = createText({ - x: 500, - y: 300, - fill: colorPools[5], - // text: ['Tffg'], - text: 'this is textaaaaaaaaaaaaaaaaa aaa this isisisisisis abc', - // text: '这是textabc这aaaaa是什么这是阿萨姆abcaaaaabcdef这是textabc这aaaaa是什么这是阿萨姆abcaaaaa', - // heightLimit: 40, - wordBreak: 'keep-all', - maxLineWidth: 100, - stroke: 'green', - textAlign: 'left', - textBaseline: 'middle', - whiteSpace: 'normal' - // wrap: true + list.forEach((item, index) => { + graphics.push( + createText({ + x: 50 + index * 50, + y: 500, + ...item, + fill: 'red', + ignoreBuf: true, + fontSize: 22, + _debug_bounds: true, + ...item + }) + ); }); - console.log('textLimit', textLimit); - graphics.push(textLimit); const stage = createStage({ canvas: 'main', @@ -258,8 +367,8 @@ export const page = () => { performance(stage); }); - setTimeout(() => { - console.log(stage.getLayer('_builtin_interactive').children); - stage.release(); - }, 2000); + // setTimeout(() => { + // console.log(stage.getLayer('_builtin_interactive').children); + // stage.release(); + // }, 2000); }; From d9c8995791033f2f0f60d52772c1a63dd3785404 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 22 Nov 2024 11:01:32 +0800 Subject: [PATCH 16/28] feat: add secondary correction for text layout --- .../vrender-components/__tests__/unit/pager.test.ts | 2 +- .../vrender-components/__tests__/unit/slider.test.ts | 2 +- .../src/core/contributions/textMeasure/layout.ts | 10 ++++++++++ packages/vrender/__tests__/browser/src/pages/text.ts | 6 ++++++ .../vrender/__tests__/graphic/graphic-bounds.test.ts | 8 ++++---- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/vrender-components/__tests__/unit/pager.test.ts b/packages/vrender-components/__tests__/unit/pager.test.ts index 0bf4009bf..17f424161 100644 --- a/packages/vrender-components/__tests__/unit/pager.test.ts +++ b/packages/vrender-components/__tests__/unit/pager.test.ts @@ -58,6 +58,6 @@ describe('Pager', () => { expect((pager.preHandler as ISymbol).hasState('disable')).toBeFalsy(); expect((pager.nextHandler as ISymbol).hasState('disable')).toBeFalsy(); expect(pager.AABBBounds.width()).toBeCloseTo(20.399993896484375); - expect(pager.AABBBounds.height()).toBeCloseTo(58); + expect(pager.AABBBounds.height()).toBeCloseTo(59.2); }); }); diff --git a/packages/vrender-components/__tests__/unit/slider.test.ts b/packages/vrender-components/__tests__/unit/slider.test.ts index a74723a39..18ad44cea 100644 --- a/packages/vrender-components/__tests__/unit/slider.test.ts +++ b/packages/vrender-components/__tests__/unit/slider.test.ts @@ -132,7 +132,7 @@ describe('Slider', () => { expect(startText.attribute.textBaseline).toBe('top'); const endText = slider.getElementsByName(SLIDER_ELEMENT_NAME.endText)[0] as IText; - expect(endText.attribute.y).toBe(228); + expect(endText.attribute.y).toBe(229.2); expect(endText.attribute.x).toBeCloseTo(5); expect(endText.attribute.textAlign).toBe('center'); expect(endText.attribute.textBaseline).toBe('top'); diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index dce318f8d..2f543b6c4 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -199,6 +199,16 @@ export class CanvasTextLayout { } line.topOffset = lineHeight / 2 + (line.ascent - line.descent) / 2 + origin[1]; + // const actualHeight = line.ascent + line.descent; + // const buf = 2; + // const actualHeightWithBuf = actualHeight + buf; + // if (actualHeightWithBuf < lineHeight - buf) { + // if (textBaseline === 'bottom') { + // line.topOffset += (lineHeight - (actualHeightWithBuf)) / 2; + // } else if (textBaseline === 'top') { + // line.topOffset -= (lineHeight - (actualHeightWithBuf)) / 2; + // } + // } origin[1] += lineHeight; return line; diff --git a/packages/vrender/__tests__/browser/src/pages/text.ts b/packages/vrender/__tests__/browser/src/pages/text.ts index 89a48be96..e9ad5c7de 100644 --- a/packages/vrender/__tests__/browser/src/pages/text.ts +++ b/packages/vrender/__tests__/browser/src/pages/text.ts @@ -265,6 +265,11 @@ export const page = () => { textBaseline: 'middle', x: 500 } + // { + // text: '...', + // textBaseline: 'bottom', + // x: 550 + // } ]; graphics.push( @@ -336,6 +341,7 @@ export const page = () => { y: 500, ...item, fill: 'red', + measureMode: 1, ignoreBuf: true, fontSize: 22, _debug_bounds: true, diff --git a/packages/vrender/__tests__/graphic/graphic-bounds.test.ts b/packages/vrender/__tests__/graphic/graphic-bounds.test.ts index 87e128fd0..6554067e7 100644 --- a/packages/vrender/__tests__/graphic/graphic-bounds.test.ts +++ b/packages/vrender/__tests__/graphic/graphic-bounds.test.ts @@ -314,9 +314,9 @@ describe('Graphic-Bounds', () => { }); expect(text.AABBBounds.x1).toBeCloseTo(100); - expect(text.AABBBounds.y1).toBeCloseTo(87.36); + expect(text.AABBBounds.y1).toBeCloseTo(86.096); expect(text.AABBBounds.x2).toBeCloseTo(185.390625); - expect(text.AABBBounds.y2).toBeCloseTo(103.36); + expect(text.AABBBounds.y2).toBeCloseTo(103.696); text = createText({ x: 100, @@ -328,9 +328,9 @@ describe('Graphic-Bounds', () => { }); expect(text.AABBBounds.x1).toBeCloseTo(100); - expect(text.AABBBounds.y1).toBeCloseTo(74.72); + expect(text.AABBBounds.y1).toBeCloseTo(72.192); expect(text.AABBBounds.x2).toBeCloseTo(185.390625); - expect(text.AABBBounds.y2).toBeCloseTo(106.72); + expect(text.AABBBounds.y2).toBeCloseTo(107.392); }); it('arc bounds', () => { From c3dce38ff71b68933eda929fc8f73b408b4209fd Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 22 Nov 2024 16:10:51 +0800 Subject: [PATCH 17/28] chore: update vutil version to 0.19.1 --- common/config/rush/pnpm-lock.yaml | 42 +++++++++++------------ docs/demos/package.json | 2 +- docs/package.json | 2 +- packages/react-vrender-utils/package.json | 2 +- packages/react-vrender/package.json | 2 +- packages/vrender-components/package.json | 6 ++-- packages/vrender-core/package.json | 2 +- packages/vrender-kits/package.json | 2 +- packages/vrender/package.json | 2 +- 9 files changed, 31 insertions(+), 31 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 723044d3b..aaea2c07e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: '@visactor/vchart': 1.3.0 '@visactor/vgrammar': ~0.5.7 '@visactor/vrender': workspace:0.20.15 - '@visactor/vutils': ~0.19.0 + '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 axios: ^1.4.0 chalk: ^3.0.0 @@ -38,7 +38,7 @@ importers: '@visactor/vchart': 1.3.0 '@visactor/vgrammar': 0.5.7 '@visactor/vrender': link:../packages/vrender - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 axios: 1.7.4 highlight.js: 11.10.0 lodash: 4.17.21 @@ -72,7 +72,7 @@ importers: '@types/react-dom': ^18.0.0 '@types/react-reconciler': ^0.28.2 '@visactor/vrender': workspace:0.20.15 - '@visactor/vutils': ~0.19.0 + '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 cross-env: ^7.0.3 eslint: ~8.18.0 @@ -84,7 +84,7 @@ importers: vite: 3.2.6 dependencies: '@visactor/vrender': link:../vrender - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 react-reconciler: 0.29.2_react@18.3.1 tslib: 2.6.3 devDependencies: @@ -113,7 +113,7 @@ importers: '@types/react-dom': ^18.0.0 '@visactor/react-vrender': workspace:0.20.15 '@visactor/vrender': workspace:0.20.15 - '@visactor/vutils': ~0.19.0 + '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 cross-env: ^7.0.3 eslint: ~8.18.0 @@ -126,7 +126,7 @@ importers: dependencies: '@visactor/react-vrender': link:../react-vrender '@visactor/vrender': link:../vrender - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 react-reconciler: 0.29.2_react@18.3.1 tslib: 2.6.3 devDependencies: @@ -155,7 +155,7 @@ importers: '@types/react-dom': ^18.0.0 '@visactor/vrender-core': workspace:0.20.15 '@visactor/vrender-kits': workspace:0.20.15 - '@visactor/vutils': ~0.19.0 + '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 canvas: 2.11.2 cross-env: ^7.0.3 @@ -179,7 +179,7 @@ importers: '@types/jest': 26.0.24 '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 '@vitejs/plugin-react': 3.1.0_vite@3.2.6 canvas: 2.11.2 cross-env: 7.0.3 @@ -202,8 +202,8 @@ importers: '@types/jest': ^26.0.0 '@visactor/vrender-core': workspace:0.20.15 '@visactor/vrender-kits': workspace:0.20.15 - '@visactor/vscale': ~0.19.0 - '@visactor/vutils': ~0.19.0 + '@visactor/vscale': ~0.19.1 + '@visactor/vutils': ~0.19.1 cross-env: ^7.0.3 eslint: ~8.18.0 jest: ^26.0.0 @@ -215,8 +215,8 @@ importers: dependencies: '@visactor/vrender-core': link:../vrender-core '@visactor/vrender-kits': link:../vrender-kits - '@visactor/vscale': 0.19.0 - '@visactor/vutils': 0.19.0 + '@visactor/vscale': 0.19.1 + '@visactor/vutils': 0.19.1 devDependencies: '@internal/bundler': link:../../tools/bundler '@internal/eslint-config': link:../../share/eslint-config @@ -241,7 +241,7 @@ importers: '@types/jest': ^26.0.0 '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 - '@visactor/vutils': ~0.19.0 + '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 color-convert: 2.0.1 cross-env: ^7.0.3 @@ -255,7 +255,7 @@ importers: typescript: 4.9.5 vite: 3.2.6 dependencies: - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 color-convert: 2.0.1 devDependencies: '@internal/bundler': link:../../tools/bundler @@ -288,7 +288,7 @@ importers: '@types/react': ^18.0.0 '@types/react-dom': ^18.0.0 '@visactor/vrender-core': workspace:0.20.15 - '@visactor/vutils': ~0.19.0 + '@visactor/vutils': ~0.19.1 '@vitejs/plugin-react': 3.1.0 canvas: 2.11.2 cross-env: ^7.0.3 @@ -302,7 +302,7 @@ importers: dependencies: '@resvg/resvg-js': 2.4.1 '@visactor/vrender-core': link:../vrender-core - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 roughjs: 4.5.2 devDependencies: '@internal/bundler': link:../../tools/bundler @@ -3422,10 +3422,10 @@ packages: '@visactor/vutils': 0.15.14 dev: false - /@visactor/vscale/0.19.0: - resolution: {integrity: sha512-5xfxl2aego1BBPG631u2mUFKPo6EF4VqxwXhnmz/7IZoJCxXBbT3gU1aHNFa4Z8uZ08bH+6sPSbS5gP0bXhRHw==} + /@visactor/vscale/0.19.1: + resolution: {integrity: sha512-hPNBP33sTzB/7xxot1FTSnuDmween06iM+JW3j7u/AmdGeMON4cphisEN5iJ5DR5Z9Br5PolvP4vnhemoqlTZw==} dependencies: - '@visactor/vutils': 0.19.0 + '@visactor/vutils': 0.19.1 dev: false /@visactor/vutils/0.13.3: @@ -3444,8 +3444,8 @@ packages: eventemitter3: 4.0.7 dev: false - /@visactor/vutils/0.19.0: - resolution: {integrity: sha512-eqdMcSJAk81MOj7Kry5lx+56LcFa3NM2v1V5b7lso0WEBphQl/DXkwvpWUBJKuHKaXn6XV1R4M8tocWUXhAXdA==} + /@visactor/vutils/0.19.1: + resolution: {integrity: sha512-ydvC2RvRISCsL86tkhyStJpsTX61UCCGN+BzPeMfmDv4cOc5IWJn3MnL3Twgj0lm6irCabEPQyetPSZ4NEmoOw==} dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 diff --git a/docs/demos/package.json b/docs/demos/package.json index 4d2ec5d1c..825c7b6e0 100644 --- a/docs/demos/package.json +++ b/docs/demos/package.json @@ -12,7 +12,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@visactor/vrender-kits": "workspace:0.14.8", - "@visactor/vutils": "~0.19.0", + "@visactor/vutils": "~0.19.1", "d3-scale-chromatic": "^3.0.0", "lodash": "4.17.21", "dat.gui": "^0.7.9", diff --git a/docs/package.json b/docs/package.json index 18891d77e..7c252ae58 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,7 @@ "dependencies": { "@arco-design/web-react": "2.46.1", "@visactor/vchart": "1.3.0", - "@visactor/vutils": "~0.19.0", + "@visactor/vutils": "~0.19.1", "@visactor/vgrammar": "~0.5.7", "@visactor/vrender": "workspace:0.20.15", "markdown-it": "^13.0.0", diff --git a/packages/react-vrender-utils/package.json b/packages/react-vrender-utils/package.json index 6864dcd49..e500f2009 100644 --- a/packages/react-vrender-utils/package.json +++ b/packages/react-vrender-utils/package.json @@ -26,7 +26,7 @@ "dependencies": { "@visactor/vrender": "workspace:0.20.15", "@visactor/react-vrender": "workspace:0.20.15", - "@visactor/vutils": "~0.19.0", + "@visactor/vutils": "~0.19.1", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/react-vrender/package.json b/packages/react-vrender/package.json index d333af77f..163162d75 100644 --- a/packages/react-vrender/package.json +++ b/packages/react-vrender/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@visactor/vrender": "workspace:0.20.15", - "@visactor/vutils": "~0.19.0", + "@visactor/vutils": "~0.19.1", "react-reconciler": "^0.29.0", "tslib": "^2.3.1" }, diff --git a/packages/vrender-components/package.json b/packages/vrender-components/package.json index c9f7bf868..385e7d638 100644 --- a/packages/vrender-components/package.json +++ b/packages/vrender-components/package.json @@ -25,8 +25,8 @@ "build:spec-types": "rm -rf ./spec-types && tsc -p ./tsconfig.spec.json --declaration --emitDeclarationOnly --outDir ./spec-types" }, "dependencies": { - "@visactor/vutils": "~0.19.0", - "@visactor/vscale": "~0.19.0", + "@visactor/vutils": "~0.19.1", + "@visactor/vscale": "~0.19.1", "@visactor/vrender-core": "workspace:0.20.15", "@visactor/vrender-kits": "workspace:0.20.15" }, @@ -35,7 +35,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vscale": "~0.19.0", + "@visactor/vscale": "~0.19.1", "@types/jest": "^26.0.0", "jest": "^26.0.0", "jest-electron": "^0.1.12", diff --git a/packages/vrender-core/package.json b/packages/vrender-core/package.json index 5b88303cd..b8aaf0a86 100644 --- a/packages/vrender-core/package.json +++ b/packages/vrender-core/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "color-convert": "2.0.1", - "@visactor/vutils": "~0.19.0" + "@visactor/vutils": "~0.19.1" }, "devDependencies": { "@internal/bundler": "workspace:*", diff --git a/packages/vrender-kits/package.json b/packages/vrender-kits/package.json index 2c7722792..eec1f7062 100644 --- a/packages/vrender-kits/package.json +++ b/packages/vrender-kits/package.json @@ -20,7 +20,7 @@ "test": "" }, "dependencies": { - "@visactor/vutils": "~0.19.0", + "@visactor/vutils": "~0.19.1", "@visactor/vrender-core": "workspace:0.20.15", "@resvg/resvg-js": "2.4.1", "roughjs": "4.5.2" diff --git a/packages/vrender/package.json b/packages/vrender/package.json index fb32aed1c..defc825b6 100644 --- a/packages/vrender/package.json +++ b/packages/vrender/package.json @@ -32,7 +32,7 @@ "@internal/eslint-config": "workspace:*", "@internal/ts-config": "workspace:*", "@rushstack/eslint-patch": "~1.1.4", - "@visactor/vutils": "~0.19.0", + "@visactor/vutils": "~0.19.1", "canvas": "2.11.2", "react": "^18.0.0", "react-dom": "^18.0.0", From 7cd236921b8643e90495d12399ebb594233a2be5 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Fri, 22 Nov 2024 17:55:37 +0800 Subject: [PATCH 18/28] fix: fix issue with singleline text in alphabetic layout --- .../core/contributions/textMeasure/layout.ts | 38 ++++++++++++------ packages/vrender-core/src/graphic/text.ts | 2 +- .../__tests__/browser/src/pages/text.ts | 40 +++++++++++++++++++ .../__tests__/graphic/graphic-bounds.test.ts | 4 +- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 2f543b6c4..9c1cde015 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -21,7 +21,12 @@ export class CanvasTextLayout { * @param textBaseline * @returns */ - LayoutBBox(bbox: TextLayoutBBox, textAlign: TextAlignType, textBaseline: TextBaselineType): TextLayoutBBox { + LayoutBBox( + bbox: TextLayoutBBox, + textAlign: TextAlignType, + textBaseline: TextBaselineType, + linesLayout: LayoutItemType[] + ): TextLayoutBBox { if (textAlign === 'left' || textAlign === 'start') { bbox.xOffset = 0; } else if (textAlign === 'center') { @@ -37,7 +42,13 @@ export class CanvasTextLayout { } else if (textBaseline === 'middle') { bbox.yOffset = bbox.height / -2; } else if (textBaseline === 'alphabetic') { - bbox.yOffset = bbox.height * -0.79; + // 如果仅有一行,要保证和直接使用canvas绘制的textBaseline一致 + let percent = 0.79; + if (linesLayout.length === 1) { + const lineInfo = linesLayout[0]; + percent = lineInfo.ascent / (lineInfo.ascent + lineInfo.descent); + } + bbox.yOffset = bbox.height * -percent; } else { bbox.yOffset = -bbox.height; } @@ -125,7 +136,7 @@ export class CanvasTextLayout { height: bboxWH[1] }; - this.LayoutBBox(bbox, textAlign, textBaseline); + this.LayoutBBox(bbox, textAlign, textBaseline, linesLayout); return this.layoutWithBBox(bbox, linesLayout, textAlign, textBaseline, lineHeight); } @@ -199,16 +210,17 @@ export class CanvasTextLayout { } line.topOffset = lineHeight / 2 + (line.ascent - line.descent) / 2 + origin[1]; - // const actualHeight = line.ascent + line.descent; - // const buf = 2; - // const actualHeightWithBuf = actualHeight + buf; - // if (actualHeightWithBuf < lineHeight - buf) { - // if (textBaseline === 'bottom') { - // line.topOffset += (lineHeight - (actualHeightWithBuf)) / 2; - // } else if (textBaseline === 'top') { - // line.topOffset -= (lineHeight - (actualHeightWithBuf)) / 2; - // } - // } + + const actualHeight = line.ascent + line.descent; + const buf = 0; + const actualHeightWithBuf = actualHeight + buf; + if (actualHeightWithBuf < lineHeight - buf) { + if (textBaseline === 'bottom') { + line.topOffset += (lineHeight - actualHeightWithBuf) / 2; + } else if (textBaseline === 'top') { + line.topOffset -= (lineHeight - actualHeightWithBuf) / 2; + } + } origin[1] += lineHeight; return line; diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index ef25b5d60..38e5ce2a4 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -495,7 +495,7 @@ export class Text extends Graphic implements IText { height: bboxWH[1] }; - layoutObj.LayoutBBox(bbox, textAlign, textBaseline as any); + layoutObj.LayoutBBox(bbox, textAlign, textBaseline as any, linesLayout); const layoutData = layoutObj.layoutWithBBox(bbox, linesLayout, textAlign, textBaseline as any, lineHeight); diff --git a/packages/vrender/__tests__/browser/src/pages/text.ts b/packages/vrender/__tests__/browser/src/pages/text.ts index e9ad5c7de..fdeee4cc5 100644 --- a/packages/vrender/__tests__/browser/src/pages/text.ts +++ b/packages/vrender/__tests__/browser/src/pages/text.ts @@ -350,6 +350,46 @@ export const page = () => { ); }); + graphics.push( + createLine({ + x: 229, + y: 207, + stroke: 'blue', + points: [ + { x: -100, y: 0 }, + { x: 300, y: 0 } + ] + }) + ); + graphics.push( + createText({ + textAlign: 'center', + lineWidth: 0, + textConfig: [], + lineHeight: '150%', + fontWeight: 'bold', + fillOpacity: 1, + textBaseline: 'alphabetic', + fill: 'red', + fontFamily: + 'LarkHackSafariFont,LarkEmojiFont,LarkChineseQuote,-apple-system,BlinkMacSystemFont,"Helvetica Neue",Tahoma,"PingFang SC","Microsoft Yahei",Arial,"Hiragino Sans GB",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"', + fontStyle: '', + underline: 0, + stroke: '#00295C', + visible: true, + x: 229, + y: 207, + angle: 0, + limitAttrs: { + text: '节日福利' + }, + text: '节日福利', + fontSize: 69, + _debug_bounds: true, + pickable: true + }) + ); + const stage = createStage({ canvas: 'main', autoRender: true, diff --git a/packages/vrender/__tests__/graphic/graphic-bounds.test.ts b/packages/vrender/__tests__/graphic/graphic-bounds.test.ts index 6554067e7..701968a2b 100644 --- a/packages/vrender/__tests__/graphic/graphic-bounds.test.ts +++ b/packages/vrender/__tests__/graphic/graphic-bounds.test.ts @@ -314,9 +314,9 @@ describe('Graphic-Bounds', () => { }); expect(text.AABBBounds.x1).toBeCloseTo(100); - expect(text.AABBBounds.y1).toBeCloseTo(86.096); + expect(text.AABBBounds.y1).toBeCloseTo(85.50588235294117); expect(text.AABBBounds.x2).toBeCloseTo(185.390625); - expect(text.AABBBounds.y2).toBeCloseTo(103.696); + expect(text.AABBBounds.y2).toBeCloseTo(103.10588235294118); text = createText({ x: 100, From 9e2ef4ef4b08218a2b21ef154e07587a02995524 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 25 Nov 2024 15:56:46 +0800 Subject: [PATCH 19/28] fix: fix issue with top and bottom baseline --- .../contributions/textMeasure/AtextMeasure.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts b/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts index 400d7799c..5771ac04e 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/AtextMeasure.ts @@ -244,16 +244,16 @@ export class ATextMeasure implements ITextMeasure { width: (out as any).width }; } else if (mode === MeasureModeEnum.fontBounding) { - const { lineHeight = options.fontSize } = options; - let ratio = 1; - if (lineHeight) { - const fontBoundingHeight = (out as any).fontBoundingBoxAscent + (out as any).fontBoundingBoxDescent; - ratio = lineHeight / fontBoundingHeight; - } + // const { lineHeight = options.fontSize } = options; + // let ratio = 1; + // if (lineHeight) { + // const fontBoundingHeight = (out as any).fontBoundingBoxAscent + (out as any).fontBoundingBoxDescent; + // ratio = lineHeight / fontBoundingHeight; + // } // 避免二次矫正,应当保证所有字符组合的基线都一样,否则fontBounding就失去意义了 // 但如果超出边界了,就只能进行二次矫正 - let ascent = (out as any).fontBoundingBoxAscent * ratio; - let descent = (out as any).fontBoundingBoxDescent * ratio; + let ascent = (out as any).fontBoundingBoxAscent; + let descent = (out as any).fontBoundingBoxDescent; // 只能一边超出,都超出的话目前无法矫正,因为行高不能超 if ((out as any).actualBoundingBoxDescent && descent < (out as any).actualBoundingBoxDescent) { const delta = (out as any).actualBoundingBoxDescent - descent; From 1dc58b912223306c44e3a602d535d91d3a27209e Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Mon, 25 Nov 2024 16:57:22 +0800 Subject: [PATCH 20/28] feat: text support keepCenterInLine --- .../core/contributions/textMeasure/layout.ts | 32 ++++++++++++------- packages/vrender-core/src/graphic/config.ts | 3 +- packages/vrender-core/src/graphic/text.ts | 20 ++++++++---- .../vrender-core/src/graphic/wrap-text.ts | 11 ++++--- .../src/interface/graphic/text.ts | 5 +++ 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 9c1cde015..208f29e6b 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -72,9 +72,15 @@ export class CanvasTextLayout { lineWidth?: number; suffixPosition?: 'start' | 'end' | 'middle'; measureMode?: MeasureModeEnum; + keepCenterInLine?: boolean; } ): LayoutType { - const { lineWidth, suffixPosition = 'end', measureMode = MeasureModeEnum.actualBounding } = params ?? {}; + const { + lineWidth, + suffixPosition = 'end', + measureMode = MeasureModeEnum.actualBounding, + keepCenterInLine = false + } = params ?? {}; lines = lines.map(l => l.toString()) as string[]; const linesLayout: LayoutItemType[] = []; // bbox高度可能大于totalHeight @@ -103,7 +109,8 @@ export class CanvasTextLayout { ).str, width, ascent: metrics.ascent, - descent: metrics.descent + descent: metrics.descent, + keepCenterInLine }); } bboxWH[0] = lineWidth; @@ -121,7 +128,7 @@ export class CanvasTextLayout { ); width = metrics.width; _lineWidth = Math.max(_lineWidth, width); - linesLayout.push({ str: text, width, ascent: metrics.ascent, descent: metrics.descent }); + linesLayout.push({ str: text, width, ascent: metrics.ascent, descent: metrics.descent, keepCenterInLine }); } bboxWH[0] = _lineWidth; } @@ -211,16 +218,19 @@ export class CanvasTextLayout { line.topOffset = lineHeight / 2 + (line.ascent - line.descent) / 2 + origin[1]; - const actualHeight = line.ascent + line.descent; - const buf = 0; - const actualHeightWithBuf = actualHeight + buf; - if (actualHeightWithBuf < lineHeight - buf) { - if (textBaseline === 'bottom') { - line.topOffset += (lineHeight - actualHeightWithBuf) / 2; - } else if (textBaseline === 'top') { - line.topOffset -= (lineHeight - actualHeightWithBuf) / 2; + if (!line.keepCenterInLine) { + const actualHeight = line.ascent + line.descent; + const buf = 0; + const actualHeightWithBuf = actualHeight + buf; + if (actualHeightWithBuf < lineHeight - buf) { + if (textBaseline === 'bottom') { + line.topOffset += (lineHeight - actualHeightWithBuf) / 2; + } else if (textBaseline === 'top') { + line.topOffset -= (lineHeight - actualHeightWithBuf) / 2; + } } } + origin[1] += lineHeight; return line; diff --git a/packages/vrender-core/src/graphic/config.ts b/packages/vrender-core/src/graphic/config.ts index 41c0223e4..576dd6122 100644 --- a/packages/vrender-core/src/graphic/config.ts +++ b/packages/vrender-core/src/graphic/config.ts @@ -116,7 +116,8 @@ export const DefaultTextStyle: Required = { underlineDash: [], underlineOffset: 0, disableAutoClipedPoptip: undefined, - measureMode: MeasureModeEnum.fontBounding + measureMode: MeasureModeEnum.fontBounding, + keepCenterInLine: false }; export const DefaultPickStyle: IPickStyle = { diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index 38e5ce2a4..8059e252b 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -267,7 +267,8 @@ export class Text extends Graphic implements IText { lineWidth = textTheme.lineWidth, whiteSpace = textTheme.whiteSpace, suffixPosition = textTheme.suffixPosition, - ignoreBuf = textTheme.ignoreBuf + ignoreBuf = textTheme.ignoreBuf, + keepCenterInLine = textTheme.keepCenterInLine } = attribute; const buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize); @@ -296,7 +297,8 @@ export class Text extends Graphic implements IText { { lineWidth: maxLineWidth, suffixPosition, - measureMode + measureMode, + keepCenterInLine } ); const { bbox } = layoutData; @@ -335,7 +337,8 @@ export class Text extends Graphic implements IText { measureMode = textTheme.measureMode, suffixPosition = textTheme.suffixPosition, heightLimit = 0, - lineClamp + lineClamp, + keepCenterInLine = textTheme.keepCenterInLine } = this.attribute; const buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize); @@ -392,7 +395,8 @@ export class Text extends Graphic implements IText { str: clip.str, width: clip.width, ascent: matrics.ascent, - descent: matrics.descent + descent: matrics.descent, + keepCenterInLine }); break; // 不处理后续行 } @@ -429,7 +433,8 @@ export class Text extends Graphic implements IText { str: clip.str, width: clip.width, ascent: matrics.ascent, - descent: matrics.descent + descent: matrics.descent, + keepCenterInLine }); let cutLength = clip.str.length; if (clip.wordBreaked && !(str !== '' && clip.str === '')) { @@ -472,7 +477,8 @@ export class Text extends Graphic implements IText { str: clip.str, width: clip.width, ascent: matrics.ascent, - descent: matrics.descent + descent: matrics.descent, + keepCenterInLine }); lineWidth = Math.max(lineWidth, clip.width); break; // 不处理后续行 @@ -482,7 +488,7 @@ export class Text extends Graphic implements IText { width = textMeasure.measureTextWidth(text, textOptions); lineWidth = Math.max(lineWidth, width); const matrics = textMeasure.measureTextPixelADscentAndWidth(text, textOptions, measureMode); - linesLayout.push({ str: text, width, ascent: matrics.ascent, descent: matrics.descent }); + linesLayout.push({ str: text, width, ascent: matrics.ascent, descent: matrics.descent, keepCenterInLine }); } bboxWH[0] = lineWidth; } diff --git a/packages/vrender-core/src/graphic/wrap-text.ts b/packages/vrender-core/src/graphic/wrap-text.ts index f8b0d00e3..1fce194ae 100644 --- a/packages/vrender-core/src/graphic/wrap-text.ts +++ b/packages/vrender-core/src/graphic/wrap-text.ts @@ -109,7 +109,8 @@ export class WrapText extends Text { str: clip.str, width: clip.width, ascent: 0, - descent: 0 + descent: 0, + keepCenterInLine: false }); break; // 不处理后续行 } @@ -145,7 +146,8 @@ export class WrapText extends Text { str: clip.str, width: clip.width, ascent: 0, - descent: 0 + descent: 0, + keepCenterInLine: false }); if (clip.str.length === str.length) { // 不需要截断 @@ -182,7 +184,8 @@ export class WrapText extends Text { str: clip.str, width: clip.width, ascent: 0, - descent: 0 + descent: 0, + keepCenterInLine: false }); lineWidth = Math.max(lineWidth, clip.width); break; // 不处理后续行 @@ -191,7 +194,7 @@ export class WrapText extends Text { text = lines[i] as string; width = layoutObj.textMeasure.measureTextWidth(text, layoutObj.textOptions, wordBreak === 'break-word'); lineWidth = Math.max(lineWidth, width); - linesLayout.push({ str: text, width, ascent: 0, descent: 0 }); + linesLayout.push({ str: text, width, ascent: 0, descent: 0, keepCenterInLine: false }); } bboxWH[0] = lineWidth; } diff --git a/packages/vrender-core/src/interface/graphic/text.ts b/packages/vrender-core/src/interface/graphic/text.ts index 75f00e65b..61bae93c5 100644 --- a/packages/vrender-core/src/interface/graphic/text.ts +++ b/packages/vrender-core/src/interface/graphic/text.ts @@ -14,6 +14,7 @@ export interface LayoutItemType { width: number; ascent: number; descent: number; + keepCenterInLine: boolean; } export interface SimplifyLayoutType { @@ -68,8 +69,12 @@ export type ITextAttribute = { // textDecorationWidth: number; // padding?: number | number[]; disableAutoClipedPoptip?: boolean; + // @since 0.21.0 // 测量模式,默认使用actualBounding measureMode?: MeasureModeEnum; + // @since 0.21.0 + // 保持在行中间的位置 + keepCenterInLine?: boolean; }; export type ITextCache = { // 单行文本的时候缓存(多行文本没有) From 3e64a8783aa28f089ae698b54b1f7ee3545f1fea Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 26 Nov 2024 11:26:24 +0800 Subject: [PATCH 21/28] fix: fix issue with empty lines array --- packages/vrender-core/src/graphic/text.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index 8059e252b..4747ec51b 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -212,7 +212,7 @@ export class Text extends Graphic implements IText { updateSingallineAABBBounds(text: number | string): IAABBBounds { this.updateMultilineAABBBounds([text]); const layoutData = this.cache.layoutData; - if (layoutData) { + if (layoutData && layoutData.lines && layoutData.lines.length) { const line = layoutData.lines[0]; this.cache.clipedText = line.str; this.cache.clipedWidth = line.width; From 6c785fa5a7e9319839d95a8338db7025ceb1d690 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 26 Nov 2024 11:47:38 +0800 Subject: [PATCH 22/28] fix: fix issue with illegal textAlign --- .../vrender-core/src/core/contributions/textMeasure/layout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 208f29e6b..799ee7526 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -34,7 +34,7 @@ export class CanvasTextLayout { } else if (textAlign === 'right' || textAlign === 'end') { bbox.xOffset = -bbox.width; } else { - throw new Error('非法的textAlign'); + bbox.xOffset = 0; } if (textBaseline === 'top') { From a89a5351b1a6b4faa7034699c8731c010e62b37b Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 26 Nov 2024 14:37:42 +0800 Subject: [PATCH 23/28] fix: fix issue with text alphabetic layout --- packages/vrender-core/src/common/utils.ts | 7 ++++++- .../src/core/contributions/textMeasure/layout.ts | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/vrender-core/src/common/utils.ts b/packages/vrender-core/src/common/utils.ts index 2b14a809d..7a425a4af 100644 --- a/packages/vrender-core/src/common/utils.ts +++ b/packages/vrender-core/src/common/utils.ts @@ -362,10 +362,15 @@ export class RafBasedSTO { export const rafBasedSto = new RafBasedSTO(); -export const calculateLineHeight = (lineHeight: string | number, fontSize: number): number => { +export const _calculateLineHeight = (lineHeight: string | number, fontSize: number): number => { if (isString(lineHeight) && lineHeight[lineHeight.length - 1] === '%') { const scale = Number.parseFloat(lineHeight.substring(0, lineHeight.length - 1)) / 100; return fontSize * scale; } return lineHeight as number; }; + +export const calculateLineHeight = (lineHeight: string | number, fontSize: number): number => { + const _lh = _calculateLineHeight(lineHeight, fontSize); + return isNaN(_lh) ? _lh : Math.max(fontSize, _lh); +}; diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 799ee7526..61a3528e0 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -218,6 +218,7 @@ export class CanvasTextLayout { line.topOffset = lineHeight / 2 + (line.ascent - line.descent) / 2 + origin[1]; + // 在行内进行偏移 if (!line.keepCenterInLine) { const actualHeight = line.ascent + line.descent; const buf = 0; @@ -229,6 +230,11 @@ export class CanvasTextLayout { line.topOffset -= (lineHeight - actualHeightWithBuf) / 2; } } + if (textBaseline === 'alphabetic') { + const fontBoundingHeight = line.ascent + line.descent; + const ratio = lineHeight / fontBoundingHeight; + line.topOffset = lineHeight / 2 + ((line.ascent - line.descent) / 2) * ratio + origin[1]; + } } origin[1] += lineHeight; From 46a2d8b243f451172ac7af9d1045e5183b7ee46d Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Tue, 26 Nov 2024 14:59:57 +0800 Subject: [PATCH 24/28] feat: lineHeight not add buf --- .../vrender-components/__tests__/unit/pager.test.ts | 2 +- .../vrender-components/__tests__/unit/slider.test.ts | 2 +- packages/vrender-core/src/graphic/text.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/vrender-components/__tests__/unit/pager.test.ts b/packages/vrender-components/__tests__/unit/pager.test.ts index 17f424161..0bf4009bf 100644 --- a/packages/vrender-components/__tests__/unit/pager.test.ts +++ b/packages/vrender-components/__tests__/unit/pager.test.ts @@ -58,6 +58,6 @@ describe('Pager', () => { expect((pager.preHandler as ISymbol).hasState('disable')).toBeFalsy(); expect((pager.nextHandler as ISymbol).hasState('disable')).toBeFalsy(); expect(pager.AABBBounds.width()).toBeCloseTo(20.399993896484375); - expect(pager.AABBBounds.height()).toBeCloseTo(59.2); + expect(pager.AABBBounds.height()).toBeCloseTo(58); }); }); diff --git a/packages/vrender-components/__tests__/unit/slider.test.ts b/packages/vrender-components/__tests__/unit/slider.test.ts index 18ad44cea..a74723a39 100644 --- a/packages/vrender-components/__tests__/unit/slider.test.ts +++ b/packages/vrender-components/__tests__/unit/slider.test.ts @@ -132,7 +132,7 @@ describe('Slider', () => { expect(startText.attribute.textBaseline).toBe('top'); const endText = slider.getElementsByName(SLIDER_ELEMENT_NAME.endText)[0] as IText; - expect(endText.attribute.y).toBe(229.2); + expect(endText.attribute.y).toBe(228); expect(endText.attribute.x).toBeCloseTo(5); expect(endText.attribute.textAlign).toBe('center'); expect(endText.attribute.textBaseline).toBe('top'); diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index 4747ec51b..df2393326 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -272,7 +272,7 @@ export class Text extends Graphic implements IText { } = attribute; const buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize); - const lineHeight = this.getLineHeight(attribute, textTheme) + buf; + const lineHeight = this.getLineHeight(attribute, textTheme, buf); if (whiteSpace === 'normal' || wrap) { return this.updateWrapAABBBounds(text); @@ -342,7 +342,7 @@ export class Text extends Graphic implements IText { } = this.attribute; const buf = ignoreBuf ? 0 : this.guessLineHeightBuf(fontSize); - const lineHeight = this.getLineHeight(this.attribute, textTheme) + buf; + const lineHeight = this.getLineHeight(this.attribute, textTheme, buf); if (!this.shouldUpdateShape() && this.cache?.layoutData) { const bbox = this.cache.layoutData.bbox; @@ -538,7 +538,7 @@ export class Text extends Graphic implements IText { suffixPosition = textTheme.suffixPosition } = attribute; - const lineHeight = this.getLineHeight(attribute, textTheme); + const lineHeight = this.getLineHeight(attribute, textTheme, 0); let { textAlign = textTheme.textAlign, textBaseline = textTheme.textBaseline } = attribute; if (!verticalMode) { @@ -639,10 +639,10 @@ export class Text extends Graphic implements IText { return attribute.maxLineWidth ?? attribute.maxWidth ?? theme.maxWidth; } - protected getLineHeight(attribute: ITextGraphicAttribute, textTheme: ITextGraphicAttribute) { + protected getLineHeight(attribute: ITextGraphicAttribute, textTheme: ITextGraphicAttribute, buf: number) { return ( calculateLineHeight(attribute.lineHeight, attribute.fontSize || textTheme.fontSize) ?? - (attribute.fontSize || textTheme.fontSize) + (attribute.fontSize || textTheme.fontSize) + buf ); } From 615c95549a847319f4ec6e6970e98df89e1a66a2 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 27 Nov 2024 15:00:51 +0800 Subject: [PATCH 25/28] fix: fix issue where clip text width not accurate --- .../core/contributions/textMeasure/layout.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 61a3528e0..1687b94bb 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -94,19 +94,25 @@ export class CanvasTextLayout { this.textOptions, measureMode ); - width = Math.min(metrics.width, lineWidth); + let str: string = lines[i].toString(); + // 大于最大宽度,需要裁剪 + if (metrics.width > lineWidth) { + const data = this.textMeasure.clipTextWithSuffix( + lines[i] as string, + this.textOptions, + width, + suffix, + wordBreak, + suffixPosition + ); + str = data.str; + width = data.width; + } else { + // 小于最大宽度,不需要裁剪,直接取文字总宽度即可 + width = metrics.width; + } linesLayout.push({ - str: - metrics.width <= lineWidth - ? lines[i].toString() - : this.textMeasure.clipTextWithSuffix( - lines[i] as string, - this.textOptions, - width, - suffix, - wordBreak, - suffixPosition - ).str, + str, width, ascent: metrics.ascent, descent: metrics.descent, From 485c50fe4fafc409487252bee64a0cf94cf244e5 Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 27 Nov 2024 15:07:01 +0800 Subject: [PATCH 26/28] fix: trim space only in keep-all mode --- packages/vrender-core/src/graphic/text.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vrender-core/src/graphic/text.ts b/packages/vrender-core/src/graphic/text.ts index df2393326..ff46f2653 100644 --- a/packages/vrender-core/src/graphic/text.ts +++ b/packages/vrender-core/src/graphic/text.ts @@ -444,7 +444,11 @@ export class Text extends Graphic implements IText { if (clip.str.length === str.length) { // 不需要截断 } else if (needCut) { - const newStr = str.substring(cutLength); + let newStr = str.substring(cutLength); + // 截断后,避免开头有空格很尬,去掉 + if (wordBreak === 'keep-all') { + newStr = newStr.replace(/^\s+/g, ''); + } lines.splice(i + 1, 0, newStr); } } From 7b0070cd793634b669c709a637032b11ce2ab4fb Mon Sep 17 00:00:00 2001 From: zhouxinyu Date: Wed, 27 Nov 2024 16:00:40 +0800 Subject: [PATCH 27/28] fix: fix issue with last clip text fixed incorrectly --- .../__tests__/unit/bugfix/legend-focus-layout.test.ts | 6 +++--- .../__tests__/unit/legend/discrete.test.ts | 2 +- .../src/core/contributions/textMeasure/layout.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts b/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts index f2c64e52e..cd95fe201 100644 --- a/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts +++ b/packages/vrender-components/__tests__/unit/bugfix/legend-focus-layout.test.ts @@ -45,7 +45,7 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(428.84796142578125); + expect(legend.AABBBounds.width()).toBe(422.05995178222656); }); it('should not exceed the maximum width of the item, and the basic length exceeds, legend item without value', () => { @@ -71,7 +71,7 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(320); + expect(legend.AABBBounds.width()).toBe(310); }); it('should not exceed the maximum width of the item, and the basic length exceeds, legend item with focus', () => { @@ -133,7 +133,7 @@ describe('Legend focus layout', () => { stage.defaultLayer.add(legend as unknown as IGraphic); stage.render(); - expect(legend.AABBBounds.width()).toBe(100.71428571428572); + expect(legend.AABBBounds.width()).toBe(99.92627607073103); }); it('should calculate when legend item just has label', () => { diff --git a/packages/vrender-components/__tests__/unit/legend/discrete.test.ts b/packages/vrender-components/__tests__/unit/legend/discrete.test.ts index 4cad07d46..42c876851 100644 --- a/packages/vrender-components/__tests__/unit/legend/discrete.test.ts +++ b/packages/vrender-components/__tests__/unit/legend/discrete.test.ts @@ -421,7 +421,7 @@ describe('DiscreteLegend', () => { expect((legend.getElementsByName('legendItem')[0] as IGroup).AABBBounds.width()).toBe(121.95); expect( (legend.getElementsByName('legendItem')[0].getElementsByName('legendItemLabel')[0] as IText)._AABBBounds.width() - ).toBeCloseTo(63.61000366210938); + ).toBeCloseTo(57.143951416015625); expect( (legend.getElementsByName('legendItem')[0].getElementsByName('legendItemValue')[0] as IText).attribute .maxLineWidth diff --git a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts index 1687b94bb..020a26fa6 100644 --- a/packages/vrender-core/src/core/contributions/textMeasure/layout.ts +++ b/packages/vrender-core/src/core/contributions/textMeasure/layout.ts @@ -100,7 +100,7 @@ export class CanvasTextLayout { const data = this.textMeasure.clipTextWithSuffix( lines[i] as string, this.textOptions, - width, + lineWidth, suffix, wordBreak, suffixPosition From 8675724f65d893ffd500a725c3d574bf3d973e67 Mon Sep 17 00:00:00 2001 From: Rui-Sun Date: Thu, 28 Nov 2024 11:00:03 +0800 Subject: [PATCH 28/28] fix: change WrapText into Text in Radio component --- packages/vrender-components/src/radio/radio.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vrender-components/src/radio/radio.ts b/packages/vrender-components/src/radio/radio.ts index 2c4f1e563..945df2893 100644 --- a/packages/vrender-components/src/radio/radio.ts +++ b/packages/vrender-components/src/radio/radio.ts @@ -1,7 +1,7 @@ import { merge } from '@visactor/vutils'; import { AbstractComponent } from '../core/base'; import type { RadioAttributes } from './type'; -import { Arc, WrapText } from '@visactor/vrender-core'; +import { Arc, Text } from '@visactor/vrender-core'; import type { ComponentOptions } from '../interface'; import { loadRadioComponent } from './register'; @@ -39,7 +39,7 @@ export class Radio extends AbstractComponent> { } }; _circle: Arc; - _text: WrapText; + _text: Text; name: 'radio'; @@ -91,7 +91,7 @@ export class Radio extends AbstractComponent> { } renderText() { - this._text = new WrapText(merge({}, this.attribute.text)); + this._text = new Text(merge({}, this.attribute.text)); if (this.attribute.disabled) { this._text.setAttribute('fill', this.attribute.text.disableFill); }