zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
511 lines (438 loc) • 16.3 kB
text/typescript
import * as matrix from './matrix';
import Point, { PointLike } from './Point';
import { NullUndefined } from './types';
const mathMin = Math.min;
const mathMax = Math.max;
const mathAbs = Math.abs;
const XY = ['x', 'y'] as const;
const WH = ['width', 'height'] as const;
const lt = new Point();
const rb = new Point();
const lb = new Point();
const rt = new Point();
const _intersectCtx = createIntersectContext();
const _minTv = _intersectCtx.minTv;
const _maxTv = _intersectCtx.maxTv;
// [min, max]
const _lenMinMax = [0, 0];
class BoundingRect {
x: number
y: number
width: number
height: number
constructor(x: number, y: number, width: number, height: number) {
BoundingRect.set(this, x, y, width, height);
}
static set<TTarget extends RectLike>(
target: TTarget, x: number, y: number, width: number, height: number
): TTarget {
if (width < 0) {
x = x + width;
width = -width;
}
if (height < 0) {
y = y + height;
height = -height;
}
target.x = x;
target.y = y;
target.width = width;
target.height = height;
return target;
}
union(other: BoundingRect) {
const x = mathMin(other.x, this.x);
const y = mathMin(other.y, this.y);
// If x is -Infinity and width is Infinity (like in the case of
// IncrementalDisplayable), x + width would be NaN
if (isFinite(this.x) && isFinite(this.width)) {
this.width = mathMax(
other.x + other.width,
this.x + this.width
) - x;
}
else {
this.width = other.width;
}
if (isFinite(this.y) && isFinite(this.height)) {
this.height = mathMax(
other.y + other.height,
this.y + this.height
) - y;
}
else {
this.height = other.height;
}
this.x = x;
this.y = y;
}
applyTransform(m: matrix.MatrixArray) {
BoundingRect.applyTransform(this, this, m);
}
calculateTransform(b: RectLike): matrix.MatrixArray {
const a = this;
const sx = b.width / a.width;
const sy = b.height / a.height;
const m = matrix.create();
matrix.translate(m, m, [-a.x, -a.y]);
matrix.scale(m, m, [sx, sy]);
matrix.translate(m, m, [b.x, b.y]);
return m;
}
/**
* @see `static intersect`
*/
intersect(
b: RectLike,
mtv?: PointLike,
opt?: BoundingRectIntersectOpt
): boolean {
return BoundingRect.intersect(this, b, mtv, opt);
}
/**
* [NOTICE]
* Touching the edge is considered an intersection.
* zero-width/height can still cause intersection if `touchThreshold` is 0.
* See more in `BoundingRectIntersectOpt['touchThreshold']`
*
* @param mtv
* If it's not overlapped. it means needs to move `b` rect with Maximum Translation Vector to be overlapped.
* Else it means needs to move `b` rect with Minimum Translation Vector to be not overlapped.
*/
static intersect(
a: RectLike,
b: RectLike,
mtv?: PointLike,
opt?: BoundingRectIntersectOpt
): boolean {
if (mtv) {
Point.set(mtv, 0, 0);
}
const outIntersectRect = opt && opt.outIntersectRect || null;
const clamp = opt && opt.clamp;
if (outIntersectRect) {
outIntersectRect.x = outIntersectRect.y = outIntersectRect.width = outIntersectRect.height = NaN;
}
if (!a || !b) {
return false;
}
// Normalize negative width/height.
if (!(a instanceof BoundingRect)) {
a = BoundingRect.set(_tmpIntersectA, a.x, a.y, a.width, a.height);
}
if (!(b instanceof BoundingRect)) {
b = BoundingRect.set(_tmpIntersectB, b.x, b.y, b.width, b.height);
}
const useMTV = !!mtv;
_intersectCtx.reset(opt, useMTV);
const touchThreshold = _intersectCtx.touchThreshold;
const ax0 = a.x + touchThreshold;
const ax1 = a.x + a.width - touchThreshold;
const ay0 = a.y + touchThreshold;
const ay1 = a.y + a.height - touchThreshold;
const bx0 = b.x + touchThreshold;
const bx1 = b.x + b.width - touchThreshold;
const by0 = b.y + touchThreshold;
const by1 = b.y + b.height - touchThreshold;
if (ax0 > ax1 || ay0 > ay1 || bx0 > bx1 || by0 > by1) {
return false;
}
const overlap = !(ax1 < bx0 || bx1 < ax0 || ay1 < by0 || by1 < ay0);
if (useMTV || outIntersectRect) {
_lenMinMax[0] = Infinity;
_lenMinMax[1] = 0;
intersectOneDim(ax0, ax1, bx0, bx1, 0, useMTV, outIntersectRect, clamp);
intersectOneDim(ay0, ay1, by0, by1, 1, useMTV, outIntersectRect, clamp);
if (useMTV) {
Point.copy(
mtv,
overlap
? (_intersectCtx.useDir ? _intersectCtx.dirMinTv : _minTv)
: _maxTv
);
}
}
return overlap;
}
static contain(rect: RectLike, x: number, y: number): boolean {
return x >= rect.x
&& x <= (rect.x + rect.width)
&& y >= rect.y
&& y <= (rect.y + rect.height);
}
contain(x: number, y: number): boolean {
return BoundingRect.contain(this, x, y);
}
clone() {
return new BoundingRect(this.x, this.y, this.width, this.height);
}
/**
* Copy from another rect
*/
copy(other: RectLike) {
BoundingRect.copy(this, other);
}
plain(): RectLike {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
};
}
/**
* If not having NaN or Infinity with attributes
*/
isFinite(): boolean {
return isFinite(this.x)
&& isFinite(this.y)
&& isFinite(this.width)
&& isFinite(this.height);
}
isZero(): boolean {
return this.width === 0 || this.height === 0;
}
static create(rect: RectLike): BoundingRect {
return new BoundingRect(rect.x, rect.y, rect.width, rect.height);
}
static copy<TTarget extends RectLike>(target: TTarget, source: RectLike): TTarget {
target.x = source.x;
target.y = source.y;
target.width = source.width;
target.height = source.height;
return target;
}
static applyTransform(target: RectLike, source: RectLike, m: matrix.MatrixArray) {
// In case usage like this
// el.getBoundingRect().applyTransform(el.transform)
// And element has no transform
if (!m) {
if (target !== source) {
BoundingRect.copy(target, source);
}
return;
}
// Fast path when there is no rotation in matrix.
if (m[1] < 1e-5 && m[1] > -1e-5 && m[2] < 1e-5 && m[2] > -1e-5) {
const sx = m[0];
const sy = m[3];
const tx = m[4];
const ty = m[5];
target.x = source.x * sx + tx;
target.y = source.y * sy + ty;
target.width = source.width * sx;
target.height = source.height * sy;
if (target.width < 0) {
target.x += target.width;
target.width = -target.width;
}
if (target.height < 0) {
target.y += target.height;
target.height = -target.height;
}
return;
}
// source and target can be same instance.
lt.x = lb.x = source.x;
lt.y = rt.y = source.y;
rb.x = rt.x = source.x + source.width;
rb.y = lb.y = source.y + source.height;
lt.transform(m);
rt.transform(m);
rb.transform(m);
lb.transform(m);
target.x = mathMin(lt.x, rb.x, lb.x, rt.x);
target.y = mathMin(lt.y, rb.y, lb.y, rt.y);
const maxX = mathMax(lt.x, rb.x, lb.x, rt.x);
const maxY = mathMax(lt.y, rb.y, lb.y, rt.y);
target.width = maxX - target.x;
target.height = maxY - target.y;
}
}
const _tmpIntersectA = new BoundingRect(0, 0, 0, 0);
const _tmpIntersectB = new BoundingRect(0, 0, 0, 0);
function intersectOneDim(
a0: number, a1: number, b0: number, b1: number,
updateDimIdx: number,
useMTV: boolean,
outIntersectRect: BoundingRectIntersectOpt['outIntersectRect'],
clamp: BoundingRectIntersectOpt['clamp']
): void {
const d0 = mathAbs(a1 - b0);
const d1 = mathAbs(b1 - a0);
const d01min = mathMin(d0, d1);
const updateDim = XY[updateDimIdx];
const zeroDim = XY[1 - updateDimIdx];
const wh = WH[updateDimIdx];
if (a1 < b0 || b1 < a0) { // No intersection on this dimension.
if (d0 < d1) {
if (useMTV) {
_maxTv[updateDim] = -d0; // b is on the right/bottom(larger x/y)
}
if (clamp) {
outIntersectRect[updateDim] = a1;
outIntersectRect[wh] = 0;
}
}
else {
if (useMTV) {
_maxTv[updateDim] = d1; // b is on the left/top(smaller x/y)
}
if (clamp) {
outIntersectRect[updateDim] = a0;
outIntersectRect[wh] = 0;
}
}
}
else { // Has intersection
if (outIntersectRect) {
outIntersectRect[updateDim] = mathMax(a0, b0);
outIntersectRect[wh] = mathMin(a1, b1) - outIntersectRect[updateDim];
}
if (useMTV) {
if (d01min < _lenMinMax[0] || _intersectCtx.useDir) {
// If bidirectional, both dist0 dist1 need to check,
// otherwise only check the smaller one.
_lenMinMax[0] = mathMin(d01min, _lenMinMax[0]);
if (d0 < d1 || !_intersectCtx.bidirectional) {
_minTv[updateDim] = d0; // b is on the right/bottom(larger x/y)
_minTv[zeroDim] = 0;
if (_intersectCtx.useDir) {
_intersectCtx.calcDirMTV();
}
}
if (d0 >= d1 || !_intersectCtx.bidirectional) {
_minTv[updateDim] = -d1; // b is on the left/top(smaller x/y)
_minTv[zeroDim] = 0;
if (_intersectCtx.useDir) {
_intersectCtx.calcDirMTV();
}
}
}
}
}
}
export type RectLike = {
x: number
y: number
width: number
height: number
}
export interface BoundingRectIntersectOpt {
/**
* If specified, when overlapping, the output `mtv` is still a minimal vector that can resolve the overlap.
* However it is not Minimum Translation Vector, but a vector follow the direction.
* Be a radian, representing a vector direction.
* `direction=atan2(y, x)`, i.e., `direction=0` is vector(1,0), `direction=PI/4` is vector(1,1).
*/
direction?: number
/**
* By default `true`. It means whether `BoundingRectIntersectOpt['direction']` is bidirectional. If `true`,
* the returned mtv is the minimal among both `opt.direction` and `opt.direction + Math.PI`.
*/
bidirectional?: boolean
/**
* Two rects that touch but are within the threshold do not be considered an intersection.
* Scenarios:
* - Without a `touchThreshold`, zero-width/height can still cause intersection.
* In some scenarios, a rect with border styles still needs to display even if width/height is zero;
* but in some other scenarios, zero-width/height represents "nothing", such as in HTML
* BoundingClientRect, or when zrender.Group has all children `ignored: true`. In this case, we can use
* a non-negative `touchThreshold` to form a "minus width/height" and force it to never cause an
* intersection. And in this case, mtv will not be calculated.
* - Without a `touchThreshold`, touching the edge is considered an intersection.
* - Having a `touchThreshold`, elements can use the same rect instance to achieve compact layout while
* still passing through the overlap-hiding handler.
* - a positive near-zero number is commonly used in `touchThreshold` for aggressive overlap handling,
* such as:
* - Hide one element if overlapping.
* - Two elements are vertically touching at top/bottom edges, but are restricted to move along
* the horizontal direction to resolve overlap.
*/
touchThreshold?: number
/**
* - If an intersection occur, set the intersection rect to it.
* - Otherwise,
* - If `clamp: true`, `outIntersectRect` is set with a clamped rect that is on the edge or corner
* of the first rect input to `intersect` method.
* - Otherwise, set to all NaN (it will not pass `contain` and `intersect`).
*/
outIntersectRect?: RectLike
clamp?: boolean;
}
/**
* [CAVEAT] Do not use it other than in `BoundingRect` and `OrientedBoundingRect`.
*/
export function createIntersectContext() {
let _direction: BoundingRectIntersectOpt['direction'] = 0;
const _dirCheckVec = new Point();
const _dirTmp = new Point();
const _ctx = {
minTv: new Point(),
maxTv: new Point(),
useDir: false as boolean,
dirMinTv: new Point(),
touchThreshold: 0 as BoundingRectIntersectOpt['touchThreshold'],
bidirectional: true as BoundingRectIntersectOpt['bidirectional'],
negativeSize: false as boolean,
reset(opt: BoundingRectIntersectOpt | NullUndefined, useMTV: boolean): void {
_ctx.touchThreshold = 0;
if (opt && opt.touchThreshold != null) {
_ctx.touchThreshold = mathMax(0, opt.touchThreshold);
}
_ctx.negativeSize = false;
if (!useMTV) {
return;
}
_ctx.minTv.set(Infinity, Infinity);
_ctx.maxTv.set(0, 0);
_ctx.useDir = false;
if (opt && opt.direction != null) {
_ctx.useDir = true;
_ctx.dirMinTv.copy(_ctx.minTv);
_dirTmp.copy(_ctx.minTv);
_direction = opt.direction;
_ctx.bidirectional = opt.bidirectional == null || !!opt.bidirectional;
if (!_ctx.bidirectional) {
_dirCheckVec.set(Math.cos(_direction), Math.sin(_direction));
}
}
},
calcDirMTV(): void {
const minTv = _ctx.minTv;
const dirMinTv = _ctx.dirMinTv;
const squareMag = minTv.y * minTv.y + minTv.x * minTv.x;
const dirSin = Math.sin(_direction);
const dirCos = Math.cos(_direction);
const dotProd = dirSin * minTv.y + dirCos * minTv.x;
if (nearZero(dotProd)) {
if (nearZero(minTv.x) && nearZero(minTv.y)) {
// The two OBBs touch at the edges.
dirMinTv.set(0, 0);
}
// Otherwise `minTv` is perpendicular to `this.direction`.
return;
}
_dirTmp.x = squareMag * dirCos / dotProd;
_dirTmp.y = squareMag * dirSin / dotProd;
if (nearZero(_dirTmp.x) && nearZero(_dirTmp.y)) {
// The result includes near-(0,0) regardless of `bidirectional`.
dirMinTv.set(0, 0);
return;
}
if ((
_ctx.bidirectional
|| _dirCheckVec.dot(_dirTmp) > 0
)
&& _dirTmp.len() < dirMinTv.len()
) {
dirMinTv.copy(_dirTmp);
}
}
};
function nearZero(val: number): boolean {
return mathAbs(val) < 1e-10; // Empirically OK for pixel-scale values.
}
return _ctx;
}
export default BoundingRect;