Skip to content

Commit bc626df

Browse files
authored
Merge pull request #1127 from ecomfe/next
chore: next merge to master
2 parents 61c4c25 + 490a7b3 commit bc626df

26 files changed

+5058
-429
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"watch:bundle": "node build/build.js --watch",
2626
"watch:lib": "npx tsc-watch -m ES2015 --outDir lib --synchronousWatchDirectory --onSuccess \"node build/processLib.js\"",
2727
"test": "npx jest --config test/ut/jest.config.js",
28-
"lint": "npx eslint src/**/*.ts"
28+
"lint": "npx eslint src/**/*.ts",
29+
"checktype": "tsc --noEmit"
2930
},
3031
"license": "BSD-3-Clause",
3132
"types": "index.d.ts",

src/Element.ts

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import Point from './core/Point';
3131
import { LIGHT_LABEL_COLOR, DARK_LABEL_COLOR } from './config';
3232
import { parse, stringify } from './tool/color';
3333
import { REDRAW_BIT } from './graphic/constants';
34+
import { invert } from './core/matrix';
3435

3536
export interface ElementAnimateConfig {
3637
duration?: number
@@ -81,7 +82,11 @@ export interface ElementTextConfig {
8182

8283
/**
8384
* Rect that text will be positioned.
84-
* Default to be the rect of element.
85+
* Default to be the boundingRect of the host element.
86+
* The coords of `layoutRect` is based on the target element, but not global.
87+
*
88+
* [NOTICE]: boundingRect includes `lineWidth`, which is inconsistent with
89+
* the general element placement principle, where `lineWidth` is not counted.
8590
*/
8691
layoutRect?: RectLike
8792

@@ -109,6 +114,10 @@ export interface ElementTextConfig {
109114

110115
/**
111116
* If use local user space. Which will apply host's transform
117+
*
118+
* [NOTICE]: If the host element may rotate to non-parallel to screen x/y,
119+
* need to use `local:true`, otherwise the transformed layout rect may not be expected.
120+
*
112121
* @default false
113122
*/
114123
local?: boolean
@@ -166,6 +175,16 @@ export interface ElementTextConfig {
166175
* In case position is not using builtin `inside` hints.
167176
*/
168177
inside?: boolean
178+
179+
/**
180+
* Auto calculate overflow area by `textConfig.layoutRect` (if any) or `host.boundingRect`.
181+
* It makes sense only if label is inside. It ensure the text does not overflow the host.
182+
* Useful in `text.style.overflow` and `text.style.lineOverflow`.
183+
*
184+
* If `textConfig.rotation` or `text.rotation exists`, it works correctly only when the rotated text is parallel
185+
* to its host (i.e. 0, PI/2, PI, PI*3/2, 2*PI, ...). Do not supported other cases until a real scenario arises.
186+
*/
187+
autoOverflowArea?: boolean
169188
}
170189
export interface ElementTextGuideLineConfig {
171190
/**
@@ -238,6 +257,7 @@ export interface ElementProps extends Partial<ElementEventHandlerProps>, Partial
238257
draggable?: boolean | 'horizontal' | 'vertical'
239258

240259
silent?: boolean
260+
ignoreHostSilent?: boolean
241261

242262
ignoreClip?: boolean
243263
globalScaleRatio?: number
@@ -277,8 +297,9 @@ export type ElementCalculateTextPosition = (
277297
rect: RectLike
278298
) => TextPositionCalculationResult;
279299

280-
let tmpTextPosCalcRes = {} as TextPositionCalculationResult;
281-
let tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
300+
const tmpTextPosCalcRes = {} as TextPositionCalculationResult;
301+
const tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
302+
const tmpInnerTextTrans: number[] = [];
282303

283304
// eslint-disable-next-line @typescript-eslint/no-unused-vars
284305
interface Element<Props extends ElementProps = ElementProps> extends Transformable,
@@ -313,6 +334,14 @@ class Element<Props extends ElementProps = ElementProps> {
313334
*/
314335
silent: boolean
315336

337+
/**
338+
* When this element has `__hostTarget` (e.g., this is a `textContent`), whether
339+
* its silent is controlled by that host silent. They may need separate silent
340+
* settings. e.g., the host do not have `fill` but only `stroke`, or their mouse
341+
* events serve for different features.
342+
*/
343+
ignoreHostSilent: boolean
344+
316345
/**
317346
* 是否是 Group
318347
*/
@@ -368,6 +397,8 @@ class Element<Props extends ElementProps = ElementProps> {
368397
*/
369398
__inHover: boolean
370399

400+
__clipPaths?: Path[]
401+
371402
/**
372403
* path to clip the elements and its children, if it is a group.
373404
* @see http://www.w3.org/TR/2dcontext/#clipping-region
@@ -511,9 +542,12 @@ class Element<Props extends ElementProps = ElementProps> {
511542
// Reset x/y/rotation
512543
innerTransformable.copyTransform(textEl);
513544

514-
// Force set attached text's position if `position` is in config.
515-
if (textConfig.position != null) {
516-
let layoutRect = tmpBoundingRect;
545+
const hasPosition = textConfig.position != null;
546+
const autoOverflowArea = textConfig.autoOverflowArea;
547+
548+
let layoutRect: BoundingRect;
549+
if (autoOverflowArea || hasPosition) {
550+
layoutRect = tmpBoundingRect;
517551
if (textConfig.layoutRect) {
518552
layoutRect.copy(textConfig.layoutRect);
519553
}
@@ -523,7 +557,10 @@ class Element<Props extends ElementProps = ElementProps> {
523557
if (!isLocal) {
524558
layoutRect.applyTransform(this.transform);
525559
}
560+
}
526561

562+
// Force set attached text's position if `position` is in config.
563+
if (hasPosition) {
527564
if (this.calculateTextPosition) {
528565
this.calculateTextPosition(tmpTextPosCalcRes, textConfig, layoutRect);
529566
}
@@ -578,11 +615,27 @@ class Element<Props extends ElementProps = ElementProps> {
578615
}
579616
}
580617

618+
const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});
619+
620+
if (autoOverflowArea) {
621+
const overflowRect = innerTextDefaultStyle.overflowRect =
622+
innerTextDefaultStyle.overflowRect || new BoundingRect(0, 0, 0, 0);
623+
innerTransformable.getLocalTransform(tmpInnerTextTrans);
624+
invert(tmpInnerTextTrans, tmpInnerTextTrans);
625+
BoundingRect.copy(overflowRect, layoutRect);
626+
// If transform to a non-orthogonal state (e.g. rotate PI/3), the result of this "apply"
627+
// is not expected. But we don't need to address it until a real scenario arises.
628+
overflowRect.applyTransform(tmpInnerTextTrans);
629+
}
630+
else {
631+
innerTextDefaultStyle.overflowRect = null;
632+
}
633+
// [CAUTION] Do not change `innerTransformable` below.
634+
581635
// Calculate text color
582636
const isInside = textConfig.inside == null // Force to be inside or not.
583637
? (typeof textConfig.position === 'string' && textConfig.position.indexOf('inside') >= 0)
584638
: textConfig.inside;
585-
const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});
586639

587640
let textFill;
588641
let textStroke;
@@ -1021,16 +1074,16 @@ class Element<Props extends ElementProps = ElementProps> {
10211074
* Return if el.silent or any ancestor element has silent true.
10221075
*/
10231076
isSilent() {
1024-
let isSilent = this.silent;
1025-
let ancestor = this.parent;
1026-
while (!isSilent && ancestor) {
1027-
if (ancestor.silent) {
1028-
isSilent = true;
1029-
break;
1077+
// Follow the logic of `Handler.ts`#`isHover`.
1078+
let el: Element = this;
1079+
while (el) {
1080+
if (el.silent) {
1081+
return true;
10301082
}
1031-
ancestor = ancestor.parent;
1083+
const hostEl = el.__hostTarget;
1084+
el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
10321085
}
1033-
return isSilent;
1086+
return false;
10341087
}
10351088

10361089
/**
@@ -1637,6 +1690,7 @@ class Element<Props extends ElementProps = ElementProps> {
16371690

16381691
elProto.ignore =
16391692
elProto.silent =
1693+
elProto.ignoreHostSilent =
16401694
elProto.isGroup =
16411695
elProto.draggable =
16421696
elProto.dragging =
@@ -2026,5 +2080,4 @@ function animateToShallow<T>(
20262080
}
20272081
}
20282082

2029-
20302083
export default Element;

src/Handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ function isHover(displayable: Displayable, x: number, y: number) {
504504
// Consider when el is textContent, also need to be silent
505505
// if any of its host el and its ancestors is silent.
506506
const hostEl = el.__hostTarget;
507-
el = hostEl ? hostEl : el.parent;
507+
el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
508508
}
509509
return isSilent ? SILENT : true;
510510
}

src/Storage.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import timsort from './core/timsort';
88
import Displayable from './graphic/Displayable';
99
import Path from './graphic/Path';
1010
import { REDRAW_BIT } from './graphic/constants';
11+
import { NullUndefined } from './core/types';
1112

1213
let invalidZErrorLogged = false;
1314
function logInvalidZError() {
@@ -83,7 +84,7 @@ export default class Storage {
8384

8485
private _updateAndAddDisplayable(
8586
el: Element,
86-
clipPaths: Path[],
87+
parentClipPaths: Path[] | NullUndefined,
8788
includeIgnore?: boolean
8889
) {
8990
if (el.ignore && !includeIgnore) {
@@ -95,18 +96,21 @@ export default class Storage {
9596
el.afterUpdate();
9697

9798
const userSetClipPath = el.getClipPath();
98-
99-
if (el.ignoreClip) {
100-
clipPaths = null;
101-
}
102-
else if (userSetClipPath) {
103-
104-
// FIXME 效率影响
105-
if (clipPaths) {
106-
clipPaths = clipPaths.slice();
99+
const parentHasClipPaths = parentClipPaths && parentClipPaths.length;
100+
let clipPathIdx = 0;
101+
let thisClipPaths = el.__clipPaths;
102+
103+
if (!el.ignoreClip
104+
&& (parentHasClipPaths || userSetClipPath)
105+
) { // has clipPath in this pass
106+
if (!thisClipPaths) {
107+
thisClipPaths = el.__clipPaths = [];
107108
}
108-
else {
109-
clipPaths = [];
109+
if (parentHasClipPaths) {
110+
// PENDING: performance?
111+
for (let idx = 0; idx < parentClipPaths.length; idx++) {
112+
thisClipPaths[clipPathIdx++] = parentClipPaths[idx];
113+
}
110114
}
111115

112116
let currentClipPath = userSetClipPath;
@@ -118,13 +122,17 @@ export default class Storage {
118122
currentClipPath.parent = parentClipPath as Group;
119123
currentClipPath.updateTransform();
120124

121-
clipPaths.push(currentClipPath);
125+
thisClipPaths[clipPathIdx++] = currentClipPath;
122126

123127
parentClipPath = currentClipPath;
124128
currentClipPath = currentClipPath.getClipPath();
125129
}
126130
}
127131

132+
if (thisClipPaths) { // Remove other old clipPath in array.
133+
thisClipPaths.length = clipPathIdx;
134+
}
135+
128136
// ZRText and Group and combining morphing Path may use children
129137
if ((el as GroupLike).childrenRef) {
130138
const children = (el as GroupLike).childrenRef();
@@ -137,7 +145,7 @@ export default class Storage {
137145
child.__dirty |= REDRAW_BIT;
138146
}
139147

140-
this._updateAndAddDisplayable(child, clipPaths, includeIgnore);
148+
this._updateAndAddDisplayable(child, thisClipPaths, includeIgnore);
141149
}
142150

143151
// Mark group clean here
@@ -146,13 +154,6 @@ export default class Storage {
146154
}
147155
else {
148156
const disp = el as Displayable;
149-
// Element is displayable
150-
if (clipPaths && clipPaths.length) {
151-
disp.__clipPaths = clipPaths;
152-
}
153-
else if (disp.__clipPaths && disp.__clipPaths.length > 0) {
154-
disp.__clipPaths = [];
155-
}
156157

157158
// Avoid invalid z, z2, zlevel cause sorting error.
158159
if (isNaN(disp.z)) {
@@ -174,18 +175,18 @@ export default class Storage {
174175
// Add decal
175176
const decalEl = (el as Path).getDecalElement && (el as Path).getDecalElement();
176177
if (decalEl) {
177-
this._updateAndAddDisplayable(decalEl, clipPaths, includeIgnore);
178+
this._updateAndAddDisplayable(decalEl, thisClipPaths, includeIgnore);
178179
}
179180

180181
// Add attached text element and guide line.
181182
const textGuide = el.getTextGuideLine();
182183
if (textGuide) {
183-
this._updateAndAddDisplayable(textGuide, clipPaths, includeIgnore);
184+
this._updateAndAddDisplayable(textGuide, thisClipPaths, includeIgnore);
184185
}
185186

186187
const textEl = el.getTextContent();
187188
if (textEl) {
188-
this._updateAndAddDisplayable(textEl, clipPaths, includeIgnore);
189+
this._updateAndAddDisplayable(textEl, thisClipPaths, includeIgnore);
189190
}
190191
}
191192

src/canvas/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export function getCanvasGradient(this: void, ctx: CanvasRenderingContext2D, obj
8282
return canvasGradient;
8383
}
8484

85+
// [CAVEAT] Assume the clipPaths array is never modified during a batch of `isClipPathChanged` calling.
8586
export function isClipPathChanged(clipPaths: Path[], prevClipPaths: Path[]): boolean {
86-
// displayable.__clipPaths can only be `null`/`undefined` or an non-empty array.
8787
if (clipPaths === prevClipPaths || (!clipPaths && !prevClipPaths)) {
8888
return false;
8989
}

0 commit comments

Comments
 (0)