Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
efb13f2
bug(obb): Fix OBB in extreme value and add test cases.
100pah Apr 22, 2025
ccc6e88
Merge branch 'v6' into feat/obb-direction
100pah Apr 22, 2025
c057c9a
test: Enhance test utilities.
100pah Apr 28, 2025
1f0758e
test: Clarify transform test case.
100pah Apr 28, 2025
456272b
chore: Clarify comments.
100pah Apr 28, 2025
0508fdc
fix&feat(BoundingRect, OBB): (1) Support the same features from OBB (…
100pah Apr 28, 2025
e9a389f
feat: Support `ignoreHostSilent` for some scenarios that textContent …
100pah Jun 2, 2025
55ba4b2
fix(overflow): lineOverflow need to update contentHeight.
100pah Jun 2, 2025
725a87c
fix(wrap): Fix that some branches cause null pointer error.
100pah Jun 5, 2025
54d354e
Merge branch 'fix/wrap-causes-npe' into feat/ignore-host-silent
100pah Jun 5, 2025
f86f96b
chore(text): (1) clarify plain text parsing: if text.style.width spec…
100pah Jun 5, 2025
3c54b35
(test): enrich test case.
100pah Jun 5, 2025
fabe0ca
perf(trancate): (1) Make text overflow truncate more precise (conside…
100pah Jun 6, 2025
5d80fdd
feat(boundingRect): Support that touchThreshold cause negative size t…
100pah Jun 7, 2025
8eeaa03
feat(boundingRect): (1) Support get intersection rect. (2) Add some s…
100pah Jun 7, 2025
ca2381c
Merge remote-tracking branch 'origin/feat/obb-direction' into feat/ig…
100pah Jun 7, 2025
58c9f72
chore(ts): Add checktype command.
100pah Jun 7, 2025
3ce7eef
test: fix dat.gui reference error introduced by the previous commit, …
100pah Jun 7, 2025
0358e3f
Merge remote-tracking branch 'origin/feat/obb-direction' into feat/ig…
100pah Jun 7, 2025
08b0fc9
feat(boundingRect): support clamp in intersect rect.
100pah Jun 8, 2025
6219dbb
perf(clipPath): Handy optimize clipPath.
100pah Jun 8, 2025
82f8bd1
feat(rectText): Support `textConfig.autoOverflowArea`, which auto cal…
100pah Jun 8, 2025
5721d2c
chore: remove unnecessary import.
100pah Jun 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"watch:bundle": "node build/build.js --watch",
"watch:lib": "npx tsc-watch -m ES2015 --outDir lib --synchronousWatchDirectory --onSuccess \"node build/processLib.js\"",
"test": "npx jest --config test/ut/jest.config.js",
"lint": "npx eslint src/**/*.ts"
"lint": "npx eslint src/**/*.ts",
"checktype": "tsc --noEmit"
},
"license": "BSD-3-Clause",
"types": "index.d.ts",
Expand Down
85 changes: 69 additions & 16 deletions src/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import Point from './core/Point';
import { LIGHT_LABEL_COLOR, DARK_LABEL_COLOR } from './config';
import { parse, stringify } from './tool/color';
import { REDRAW_BIT } from './graphic/constants';
import { invert } from './core/matrix';

export interface ElementAnimateConfig {
duration?: number
Expand Down Expand Up @@ -81,7 +82,11 @@ export interface ElementTextConfig {

/**
* Rect that text will be positioned.
* Default to be the rect of element.
* Default to be the boundingRect of the host element.
* The coords of `layoutRect` is based on the target element, but not global.
*
* [NOTICE]: boundingRect includes `lineWidth`, which is inconsistent with
* the general element placement principle, where `lineWidth` is not counted.
*/
layoutRect?: RectLike

Expand Down Expand Up @@ -109,6 +114,10 @@ export interface ElementTextConfig {

/**
* If use local user space. Which will apply host's transform
*
* [NOTICE]: If the host element may rotate to non-parallel to screen x/y,
* need to use `local:true`, otherwise the transformed layout rect may not be expected.
*
* @default false
*/
local?: boolean
Expand Down Expand Up @@ -166,6 +175,16 @@ export interface ElementTextConfig {
* In case position is not using builtin `inside` hints.
*/
inside?: boolean

/**
* Auto calculate overflow area by `textConfig.layoutRect` (if any) or `host.boundingRect`.
* It makes sense only if label is inside. It ensure the text does not overflow the host.
* Useful in `text.style.overflow` and `text.style.lineOverflow`.
*
* If `textConfig.rotation` or `text.rotation exists`, it works correctly only when the rotated text is parallel
* to its host (i.e. 0, PI/2, PI, PI*3/2, 2*PI, ...). Do not supported other cases until a real scenario arises.
*/
autoOverflowArea?: boolean
}
export interface ElementTextGuideLineConfig {
/**
Expand Down Expand Up @@ -238,6 +257,7 @@ export interface ElementProps extends Partial<ElementEventHandlerProps>, Partial
draggable?: boolean | 'horizontal' | 'vertical'

silent?: boolean
ignoreHostSilent?: boolean

ignoreClip?: boolean
globalScaleRatio?: number
Expand Down Expand Up @@ -277,8 +297,9 @@ export type ElementCalculateTextPosition = (
rect: RectLike
) => TextPositionCalculationResult;

let tmpTextPosCalcRes = {} as TextPositionCalculationResult;
let tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
const tmpTextPosCalcRes = {} as TextPositionCalculationResult;
const tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
const tmpInnerTextTrans: number[] = [];

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

/**
* When this element has `__hostTarget` (e.g., this is a `textContent`), whether
* its silent is controlled by that host silent. They may need separate silent
* settings. e.g., the host do not have `fill` but only `stroke`, or their mouse
* events serve for different features.
*/
ignoreHostSilent: boolean

/**
* 是否是 Group
*/
Expand Down Expand Up @@ -368,6 +397,8 @@ class Element<Props extends ElementProps = ElementProps> {
*/
__inHover: boolean

__clipPaths?: Path[]

/**
* path to clip the elements and its children, if it is a group.
* @see http://www.w3.org/TR/2dcontext/#clipping-region
Expand Down Expand Up @@ -511,9 +542,12 @@ class Element<Props extends ElementProps = ElementProps> {
// Reset x/y/rotation
innerTransformable.copyTransform(textEl);

// Force set attached text's position if `position` is in config.
if (textConfig.position != null) {
let layoutRect = tmpBoundingRect;
const hasPosition = textConfig.position != null;
const autoOverflowArea = textConfig.autoOverflowArea;

let layoutRect: BoundingRect;
if (autoOverflowArea || hasPosition) {
layoutRect = tmpBoundingRect;
if (textConfig.layoutRect) {
layoutRect.copy(textConfig.layoutRect);
}
Expand All @@ -523,7 +557,10 @@ class Element<Props extends ElementProps = ElementProps> {
if (!isLocal) {
layoutRect.applyTransform(this.transform);
}
}

// Force set attached text's position if `position` is in config.
if (hasPosition) {
if (this.calculateTextPosition) {
this.calculateTextPosition(tmpTextPosCalcRes, textConfig, layoutRect);
}
Expand Down Expand Up @@ -578,11 +615,27 @@ class Element<Props extends ElementProps = ElementProps> {
}
}

const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});

if (autoOverflowArea) {
const overflowRect = innerTextDefaultStyle.overflowRect =
innerTextDefaultStyle.overflowRect || new BoundingRect(0, 0, 0, 0);
innerTransformable.getLocalTransform(tmpInnerTextTrans);
invert(tmpInnerTextTrans, tmpInnerTextTrans);
BoundingRect.copy(overflowRect, layoutRect);
// If transform to a non-orthogonal state (e.g. rotate PI/3), the result of this "apply"
// is not expected. But we don't need to address it until a real scenario arises.
overflowRect.applyTransform(tmpInnerTextTrans);
}
else {
innerTextDefaultStyle.overflowRect = null;
}
// [CAUTION] Do not change `innerTransformable` below.

// Calculate text color
const isInside = textConfig.inside == null // Force to be inside or not.
? (typeof textConfig.position === 'string' && textConfig.position.indexOf('inside') >= 0)
: textConfig.inside;
const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});

let textFill;
let textStroke;
Expand Down Expand Up @@ -1021,16 +1074,16 @@ class Element<Props extends ElementProps = ElementProps> {
* Return if el.silent or any ancestor element has silent true.
*/
isSilent() {
let isSilent = this.silent;
let ancestor = this.parent;
while (!isSilent && ancestor) {
if (ancestor.silent) {
isSilent = true;
break;
// Follow the logic of `Handler.ts`#`isHover`.
let el: Element = this;
while (el) {
if (el.silent) {
return true;
}
ancestor = ancestor.parent;
const hostEl = el.__hostTarget;
el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
}
return isSilent;
return false;
}

/**
Expand Down Expand Up @@ -1637,6 +1690,7 @@ class Element<Props extends ElementProps = ElementProps> {

elProto.ignore =
elProto.silent =
elProto.ignoreHostSilent =
elProto.isGroup =
elProto.draggable =
elProto.dragging =
Expand Down Expand Up @@ -2026,5 +2080,4 @@ function animateToShallow<T>(
}
}


export default Element;
2 changes: 1 addition & 1 deletion src/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ function isHover(displayable: Displayable, x: number, y: number) {
// Consider when el is textContent, also need to be silent
// if any of its host el and its ancestors is silent.
const hostEl = el.__hostTarget;
el = hostEl ? hostEl : el.parent;
el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
}
return isSilent ? SILENT : true;
}
Expand Down
49 changes: 25 additions & 24 deletions src/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import timsort from './core/timsort';
import Displayable from './graphic/Displayable';
import Path from './graphic/Path';
import { REDRAW_BIT } from './graphic/constants';
import { NullUndefined } from './core/types';

let invalidZErrorLogged = false;
function logInvalidZError() {
Expand Down Expand Up @@ -83,7 +84,7 @@ export default class Storage {

private _updateAndAddDisplayable(
el: Element,
clipPaths: Path[],
parentClipPaths: Path[] | NullUndefined,
includeIgnore?: boolean
) {
if (el.ignore && !includeIgnore) {
Expand All @@ -95,18 +96,21 @@ export default class Storage {
el.afterUpdate();

const userSetClipPath = el.getClipPath();

if (el.ignoreClip) {
clipPaths = null;
}
else if (userSetClipPath) {

// FIXME 效率影响
if (clipPaths) {
clipPaths = clipPaths.slice();
const parentHasClipPaths = parentClipPaths && parentClipPaths.length;
let clipPathIdx = 0;
let thisClipPaths = el.__clipPaths;

if (!el.ignoreClip
&& (parentHasClipPaths || userSetClipPath)
) { // has clipPath in this pass
if (!thisClipPaths) {
thisClipPaths = el.__clipPaths = [];
}
else {
clipPaths = [];
if (parentHasClipPaths) {
// PENDING: performance?
for (let idx = 0; idx < parentClipPaths.length; idx++) {
thisClipPaths[clipPathIdx++] = parentClipPaths[idx];
}
}

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

clipPaths.push(currentClipPath);
thisClipPaths[clipPathIdx++] = currentClipPath;

parentClipPath = currentClipPath;
currentClipPath = currentClipPath.getClipPath();
}
}

if (thisClipPaths) { // Remove other old clipPath in array.
thisClipPaths.length = clipPathIdx;
}

// ZRText and Group and combining morphing Path may use children
if ((el as GroupLike).childrenRef) {
const children = (el as GroupLike).childrenRef();
Expand All @@ -137,7 +145,7 @@ export default class Storage {
child.__dirty |= REDRAW_BIT;
}

this._updateAndAddDisplayable(child, clipPaths, includeIgnore);
this._updateAndAddDisplayable(child, thisClipPaths, includeIgnore);
}

// Mark group clean here
Expand All @@ -146,13 +154,6 @@ export default class Storage {
}
else {
const disp = el as Displayable;
// Element is displayable
if (clipPaths && clipPaths.length) {
disp.__clipPaths = clipPaths;
}
else if (disp.__clipPaths && disp.__clipPaths.length > 0) {
disp.__clipPaths = [];
}

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

// Add attached text element and guide line.
const textGuide = el.getTextGuideLine();
if (textGuide) {
this._updateAndAddDisplayable(textGuide, clipPaths, includeIgnore);
this._updateAndAddDisplayable(textGuide, thisClipPaths, includeIgnore);
}

const textEl = el.getTextContent();
if (textEl) {
this._updateAndAddDisplayable(textEl, clipPaths, includeIgnore);
this._updateAndAddDisplayable(textEl, thisClipPaths, includeIgnore);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/canvas/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export function getCanvasGradient(this: void, ctx: CanvasRenderingContext2D, obj
return canvasGradient;
}

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