zrender-nightly 5.7.0-dev.20250620 → 5.7.0-dev.20250621
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/build/prepublish.js +20 -0
- package/dist/zrender.js +563 -277
- package/dist/zrender.js.map +1 -1
- package/dist/zrender.min.js +1 -1
- package/lib/Element.d.ts +4 -0
- package/lib/Element.js +34 -16
- package/lib/Handler.js +1 -1
- package/lib/Storage.js +20 -20
- package/lib/canvas/graphic.js +1 -1
- package/lib/contain/text.d.ts +14 -2
- package/lib/contain/text.js +65 -15
- package/lib/core/BoundingRect.d.ts +25 -3
- package/lib/core/BoundingRect.js +182 -76
- package/lib/core/OrientedBoundingRect.d.ts +2 -2
- package/lib/core/OrientedBoundingRect.js +50 -34
- package/lib/core/PathProxy.d.ts +1 -0
- package/lib/core/PathProxy.js +16 -1
- package/lib/core/dom.d.ts +1 -0
- package/lib/core/dom.js +17 -0
- package/lib/core/env.js +15 -10
- package/lib/core/types.d.ts +1 -0
- package/lib/core/util.d.ts +1 -0
- package/lib/core/util.js +2 -1
- package/lib/graphic/Displayable.js +1 -1
- package/lib/graphic/Text.d.ts +4 -2
- package/lib/graphic/Text.js +23 -14
- package/lib/graphic/helper/parseText.d.ts +13 -4
- package/lib/graphic/helper/parseText.js +110 -54
- package/lib/svg-legacy/helper/ClippathManager.js +6 -6
- package/lib/tool/color.d.ts +3 -1
- package/lib/tool/color.js +6 -6
- package/lib/tool/parseSVG.js +11 -0
- package/lib/tool/path.js +7 -4
- package/lib/zrender.d.ts +1 -1
- package/lib/zrender.js +1 -1
- package/package.json +3 -2
- package/src/Element.ts +69 -16
- package/src/Handler.ts +1 -1
- package/src/Storage.ts +25 -24
- package/src/canvas/graphic.ts +1 -1
- package/src/canvas/helper.ts +1 -1
- package/src/contain/text.ts +103 -19
- package/src/core/BoundingRect.ts +308 -87
- package/src/core/OrientedBoundingRect.ts +86 -46
- package/src/core/PathProxy.ts +17 -1
- package/src/core/Transformable.ts +2 -0
- package/src/core/dom.ts +24 -0
- package/src/core/env.ts +31 -24
- package/src/core/matrix.ts +2 -1
- package/src/core/types.ts +2 -0
- package/src/core/util.ts +4 -2
- package/src/graphic/Displayable.ts +1 -3
- package/src/graphic/Group.ts +2 -0
- package/src/graphic/Text.ts +68 -21
- package/src/graphic/helper/parseText.ts +211 -83
- package/src/svg-legacy/helper/ClippathManager.ts +5 -5
- package/src/tool/color.ts +15 -11
- package/src/tool/parseSVG.ts +12 -1
- package/src/tool/path.ts +9 -4
- package/src/zrender.ts +1 -1
package/src/core/BoundingRect.ts
CHANGED
@@ -1,20 +1,24 @@
|
|
1
|
-
/**
|
2
|
-
* @module echarts/core/BoundingRect
|
3
|
-
*/
|
4
|
-
|
5
1
|
import * as matrix from './matrix';
|
6
2
|
import Point, { PointLike } from './Point';
|
3
|
+
import { NullUndefined } from './types';
|
7
4
|
|
8
5
|
const mathMin = Math.min;
|
9
6
|
const mathMax = Math.max;
|
7
|
+
const mathAbs = Math.abs;
|
8
|
+
|
9
|
+
const XY = ['x', 'y'] as const;
|
10
|
+
const WH = ['width', 'height'] as const;
|
10
11
|
|
11
12
|
const lt = new Point();
|
12
13
|
const rb = new Point();
|
13
14
|
const lb = new Point();
|
14
15
|
const rt = new Point();
|
15
16
|
|
16
|
-
const
|
17
|
-
const
|
17
|
+
const _intersectCtx = createIntersectContext();
|
18
|
+
const _minTv = _intersectCtx.minTv;
|
19
|
+
const _maxTv = _intersectCtx.maxTv;
|
20
|
+
// [min, max]
|
21
|
+
const _lenMinMax = [0, 0];
|
18
22
|
|
19
23
|
class BoundingRect {
|
20
24
|
|
@@ -24,6 +28,12 @@ class BoundingRect {
|
|
24
28
|
height: number
|
25
29
|
|
26
30
|
constructor(x: number, y: number, width: number, height: number) {
|
31
|
+
BoundingRect.set(this, x, y, width, height);
|
32
|
+
}
|
33
|
+
|
34
|
+
static set<TTarget extends RectLike>(
|
35
|
+
target: TTarget, x: number, y: number, width: number, height: number
|
36
|
+
): TTarget {
|
27
37
|
if (width < 0) {
|
28
38
|
x = x + width;
|
29
39
|
width = -width;
|
@@ -33,10 +43,12 @@ class BoundingRect {
|
|
33
43
|
height = -height;
|
34
44
|
}
|
35
45
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
46
|
+
target.x = x;
|
47
|
+
target.y = y;
|
48
|
+
target.width = width;
|
49
|
+
target.height = height;
|
50
|
+
|
51
|
+
return target;
|
40
52
|
}
|
41
53
|
|
42
54
|
union(other: BoundingRect) {
|
@@ -44,7 +56,7 @@ class BoundingRect {
|
|
44
56
|
const y = mathMin(other.y, this.y);
|
45
57
|
|
46
58
|
// If x is -Infinity and width is Infinity (like in the case of
|
47
|
-
//
|
59
|
+
// IncrementalDisplayable), x + width would be NaN
|
48
60
|
if (isFinite(this.x) && isFinite(this.width)) {
|
49
61
|
this.width = mathMax(
|
50
62
|
other.x + other.width,
|
@@ -80,7 +92,6 @@ class BoundingRect {
|
|
80
92
|
|
81
93
|
const m = matrix.create();
|
82
94
|
|
83
|
-
// 矩阵右乘
|
84
95
|
matrix.translate(m, m, [-a.x, -a.y]);
|
85
96
|
matrix.scale(m, m, [sx, sy]);
|
86
97
|
matrix.translate(m, m, [b.x, b.y]);
|
@@ -88,100 +99,107 @@ class BoundingRect {
|
|
88
99
|
return m;
|
89
100
|
}
|
90
101
|
|
91
|
-
|
92
|
-
|
102
|
+
/**
|
103
|
+
* @see `static intersect`
|
104
|
+
*/
|
105
|
+
intersect(
|
106
|
+
b: RectLike,
|
107
|
+
mtv?: PointLike,
|
108
|
+
opt?: BoundingRectIntersectOpt
|
109
|
+
): boolean {
|
110
|
+
return BoundingRect.intersect(this, b, mtv, opt);
|
111
|
+
}
|
112
|
+
|
113
|
+
/**
|
114
|
+
* [NOTICE]
|
115
|
+
* Touching the edge is considered an intersection.
|
116
|
+
* zero-width/height can still cause intersection if `touchThreshold` is 0.
|
117
|
+
* See more in `BoundingRectIntersectOpt['touchThreshold']`
|
118
|
+
*
|
119
|
+
* @param mtv
|
120
|
+
* If it's not overlapped. it means needs to move `b` rect with Maximum Translation Vector to be overlapped.
|
121
|
+
* Else it means needs to move `b` rect with Minimum Translation Vector to be not overlapped.
|
122
|
+
*/
|
123
|
+
static intersect(
|
124
|
+
a: RectLike,
|
125
|
+
b: RectLike,
|
126
|
+
mtv?: PointLike,
|
127
|
+
opt?: BoundingRectIntersectOpt
|
128
|
+
): boolean {
|
129
|
+
if (mtv) {
|
130
|
+
Point.set(mtv, 0, 0);
|
131
|
+
}
|
132
|
+
const outIntersectRect = opt && opt.outIntersectRect || null;
|
133
|
+
const clamp = opt && opt.clamp;
|
134
|
+
if (outIntersectRect) {
|
135
|
+
outIntersectRect.x = outIntersectRect.y = outIntersectRect.width = outIntersectRect.height = NaN;
|
136
|
+
}
|
137
|
+
|
138
|
+
if (!a || !b) {
|
93
139
|
return false;
|
94
140
|
}
|
95
141
|
|
142
|
+
// Normalize negative width/height.
|
143
|
+
if (!(a instanceof BoundingRect)) {
|
144
|
+
a = BoundingRect.set(_tmpIntersectA, a.x, a.y, a.width, a.height);
|
145
|
+
}
|
96
146
|
if (!(b instanceof BoundingRect)) {
|
97
|
-
|
98
|
-
b = BoundingRect.create(b);
|
147
|
+
b = BoundingRect.set(_tmpIntersectB, b.x, b.y, b.width, b.height);
|
99
148
|
}
|
100
149
|
|
101
|
-
const
|
102
|
-
const ax0 = a.x;
|
103
|
-
const ax1 = a.x + a.width;
|
104
|
-
const ay0 = a.y;
|
105
|
-
const ay1 = a.y + a.height;
|
150
|
+
const useMTV = !!mtv;
|
106
151
|
|
107
|
-
|
108
|
-
const bx1 = b.x + b.width;
|
109
|
-
const by0 = b.y;
|
110
|
-
const by1 = b.y + b.height;
|
152
|
+
_intersectCtx.reset(opt, useMTV);
|
111
153
|
|
112
|
-
|
113
|
-
if (mtv) {
|
114
|
-
let dMin = Infinity;
|
115
|
-
let dMax = 0;
|
116
|
-
const d0 = Math.abs(ax1 - bx0);
|
117
|
-
const d1 = Math.abs(bx1 - ax0);
|
118
|
-
const d2 = Math.abs(ay1 - by0);
|
119
|
-
const d3 = Math.abs(by1 - ay0);
|
120
|
-
const dx = Math.min(d0, d1);
|
121
|
-
const dy = Math.min(d2, d3);
|
122
|
-
// On x axis
|
123
|
-
if (ax1 < bx0 || bx1 < ax0) {
|
124
|
-
if (dx > dMax) {
|
125
|
-
dMax = dx;
|
126
|
-
if (d0 < d1) {
|
127
|
-
Point.set(maxTv, -d0, 0); // b is on the right
|
128
|
-
}
|
129
|
-
else {
|
130
|
-
Point.set(maxTv, d1, 0); // b is on the left
|
131
|
-
}
|
132
|
-
}
|
133
|
-
}
|
134
|
-
else {
|
135
|
-
if (dx < dMin) {
|
136
|
-
dMin = dx;
|
137
|
-
if (d0 < d1) {
|
138
|
-
Point.set(minTv, d0, 0); // b is on the right
|
139
|
-
}
|
140
|
-
else {
|
141
|
-
Point.set(minTv, -d1, 0); // b is on the left
|
142
|
-
}
|
143
|
-
}
|
144
|
-
}
|
154
|
+
const touchThreshold = _intersectCtx.touchThreshold;
|
145
155
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
else {
|
159
|
-
if (dx < dMin) {
|
160
|
-
dMin = dx;
|
161
|
-
if (d2 < d3) {
|
162
|
-
Point.set(minTv, 0, d2); // b is on the bottom
|
163
|
-
}
|
164
|
-
else {
|
165
|
-
Point.set(minTv, 0, -d3); // b is on the top
|
166
|
-
}
|
167
|
-
}
|
168
|
-
}
|
156
|
+
const ax0 = a.x + touchThreshold;
|
157
|
+
const ax1 = a.x + a.width - touchThreshold;
|
158
|
+
const ay0 = a.y + touchThreshold;
|
159
|
+
const ay1 = a.y + a.height - touchThreshold;
|
160
|
+
|
161
|
+
const bx0 = b.x + touchThreshold;
|
162
|
+
const bx1 = b.x + b.width - touchThreshold;
|
163
|
+
const by0 = b.y + touchThreshold;
|
164
|
+
const by1 = b.y + b.height - touchThreshold;
|
165
|
+
|
166
|
+
if (ax0 > ax1 || ay0 > ay1 || bx0 > bx1 || by0 > by1) {
|
167
|
+
return false;
|
169
168
|
}
|
170
169
|
|
171
|
-
|
172
|
-
|
170
|
+
const overlap = !(ax1 < bx0 || bx1 < ax0 || ay1 < by0 || by1 < ay0);
|
171
|
+
|
172
|
+
if (useMTV || outIntersectRect) {
|
173
|
+
_lenMinMax[0] = Infinity;
|
174
|
+
_lenMinMax[1] = 0;
|
175
|
+
|
176
|
+
intersectOneDim(ax0, ax1, bx0, bx1, 0, useMTV, outIntersectRect, clamp);
|
177
|
+
intersectOneDim(ay0, ay1, by0, by1, 1, useMTV, outIntersectRect, clamp);
|
178
|
+
|
179
|
+
if (useMTV) {
|
180
|
+
Point.copy(
|
181
|
+
mtv,
|
182
|
+
overlap
|
183
|
+
? (_intersectCtx.useDir ? _intersectCtx.dirMinTv : _minTv)
|
184
|
+
: _maxTv
|
185
|
+
);
|
186
|
+
}
|
173
187
|
}
|
188
|
+
|
174
189
|
return overlap;
|
175
190
|
}
|
176
191
|
|
177
|
-
contain(x: number, y: number): boolean {
|
178
|
-
const rect = this;
|
192
|
+
static contain(rect: RectLike, x: number, y: number): boolean {
|
179
193
|
return x >= rect.x
|
180
194
|
&& x <= (rect.x + rect.width)
|
181
195
|
&& y >= rect.y
|
182
196
|
&& y <= (rect.y + rect.height);
|
183
197
|
}
|
184
198
|
|
199
|
+
contain(x: number, y: number): boolean {
|
200
|
+
return BoundingRect.contain(this, x, y);
|
201
|
+
}
|
202
|
+
|
185
203
|
clone() {
|
186
204
|
return new BoundingRect(this.x, this.y, this.width, this.height);
|
187
205
|
}
|
@@ -220,11 +238,13 @@ class BoundingRect {
|
|
220
238
|
return new BoundingRect(rect.x, rect.y, rect.width, rect.height);
|
221
239
|
}
|
222
240
|
|
223
|
-
static copy(target:
|
241
|
+
static copy<TTarget extends RectLike>(target: TTarget, source: RectLike): TTarget {
|
224
242
|
target.x = source.x;
|
225
243
|
target.y = source.y;
|
226
244
|
target.width = source.width;
|
227
245
|
target.height = source.height;
|
246
|
+
|
247
|
+
return target;
|
228
248
|
}
|
229
249
|
|
230
250
|
static applyTransform(target: RectLike, source: RectLike, m: matrix.MatrixArray) {
|
@@ -278,6 +298,73 @@ class BoundingRect {
|
|
278
298
|
}
|
279
299
|
}
|
280
300
|
|
301
|
+
const _tmpIntersectA = new BoundingRect(0, 0, 0, 0);
|
302
|
+
const _tmpIntersectB = new BoundingRect(0, 0, 0, 0);
|
303
|
+
|
304
|
+
|
305
|
+
function intersectOneDim(
|
306
|
+
a0: number, a1: number, b0: number, b1: number,
|
307
|
+
updateDimIdx: number,
|
308
|
+
useMTV: boolean,
|
309
|
+
outIntersectRect: BoundingRectIntersectOpt['outIntersectRect'],
|
310
|
+
clamp: BoundingRectIntersectOpt['clamp']
|
311
|
+
): void {
|
312
|
+
const d0 = mathAbs(a1 - b0);
|
313
|
+
const d1 = mathAbs(b1 - a0);
|
314
|
+
const d01min = mathMin(d0, d1);
|
315
|
+
const updateDim = XY[updateDimIdx];
|
316
|
+
const zeroDim = XY[1 - updateDimIdx];
|
317
|
+
const wh = WH[updateDimIdx];
|
318
|
+
|
319
|
+
if (a1 < b0 || b1 < a0) { // No intersection on this dimension.
|
320
|
+
if (d0 < d1) {
|
321
|
+
if (useMTV) {
|
322
|
+
_maxTv[updateDim] = -d0; // b is on the right/bottom(larger x/y)
|
323
|
+
}
|
324
|
+
if (clamp) {
|
325
|
+
outIntersectRect[updateDim] = a1;
|
326
|
+
outIntersectRect[wh] = 0;
|
327
|
+
}
|
328
|
+
}
|
329
|
+
else {
|
330
|
+
if (useMTV) {
|
331
|
+
_maxTv[updateDim] = d1; // b is on the left/top(smaller x/y)
|
332
|
+
}
|
333
|
+
if (clamp) {
|
334
|
+
outIntersectRect[updateDim] = a0;
|
335
|
+
outIntersectRect[wh] = 0;
|
336
|
+
}
|
337
|
+
}
|
338
|
+
}
|
339
|
+
else { // Has intersection
|
340
|
+
if (outIntersectRect) {
|
341
|
+
outIntersectRect[updateDim] = mathMax(a0, b0);
|
342
|
+
outIntersectRect[wh] = mathMin(a1, b1) - outIntersectRect[updateDim];
|
343
|
+
}
|
344
|
+
if (useMTV) {
|
345
|
+
if (d01min < _lenMinMax[0] || _intersectCtx.useDir) {
|
346
|
+
// If bidirectional, both dist0 dist1 need to check,
|
347
|
+
// otherwise only check the smaller one.
|
348
|
+
_lenMinMax[0] = mathMin(d01min, _lenMinMax[0]);
|
349
|
+
if (d0 < d1 || !_intersectCtx.bidirectional) {
|
350
|
+
_minTv[updateDim] = d0; // b is on the right/bottom(larger x/y)
|
351
|
+
_minTv[zeroDim] = 0;
|
352
|
+
if (_intersectCtx.useDir) {
|
353
|
+
_intersectCtx.calcDirMTV();
|
354
|
+
}
|
355
|
+
}
|
356
|
+
if (d0 >= d1 || !_intersectCtx.bidirectional) {
|
357
|
+
_minTv[updateDim] = -d1; // b is on the left/top(smaller x/y)
|
358
|
+
_minTv[zeroDim] = 0;
|
359
|
+
if (_intersectCtx.useDir) {
|
360
|
+
_intersectCtx.calcDirMTV();
|
361
|
+
}
|
362
|
+
}
|
363
|
+
}
|
364
|
+
}
|
365
|
+
}
|
366
|
+
}
|
367
|
+
|
281
368
|
|
282
369
|
export type RectLike = {
|
283
370
|
x: number
|
@@ -286,4 +373,138 @@ export type RectLike = {
|
|
286
373
|
height: number
|
287
374
|
}
|
288
375
|
|
289
|
-
export
|
376
|
+
export interface BoundingRectIntersectOpt {
|
377
|
+
/**
|
378
|
+
* If specified, when overlapping, the output `mtv` is still a minimal vector that can resolve the overlap.
|
379
|
+
* However it is not Minimum Translation Vector, but a vector follow the direction.
|
380
|
+
* Be a radian, representing a vector direction.
|
381
|
+
* `direction=atan2(y, x)`, i.e., `direction=0` is vector(1,0), `direction=PI/4` is vector(1,1).
|
382
|
+
*/
|
383
|
+
direction?: number
|
384
|
+
/**
|
385
|
+
* By default `true`. It means whether `BoundingRectIntersectOpt['direction']` is bidirectional. If `true`,
|
386
|
+
* the returned mtv is the minimal among both `opt.direction` and `opt.direction + Math.PI`.
|
387
|
+
*/
|
388
|
+
bidirectional?: boolean
|
389
|
+
/**
|
390
|
+
* Two rects that touch but are within the threshold do not be considered an intersection.
|
391
|
+
* Scenarios:
|
392
|
+
* - Without a `touchThreshold`, zero-width/height can still cause intersection.
|
393
|
+
* In some scenarios, a rect with border styles still needs to display even if width/height is zero;
|
394
|
+
* but in some other scenarios, zero-width/height represents "nothing", such as in HTML
|
395
|
+
* BoundingClientRect, or when zrender.Group has all children `ignored: true`. In this case, we can use
|
396
|
+
* a non-negative `touchThreshold` to form a "minus width/height" and force it to never cause an
|
397
|
+
* intersection. And in this case, mtv will not be calculated.
|
398
|
+
* - Without a `touchThreshold`, touching the edge is considered an intersection.
|
399
|
+
* - Having a `touchThreshold`, elements can use the same rect instance to achieve compact layout while
|
400
|
+
* still passing through the overlap-hiding handler.
|
401
|
+
* - a positive near-zero number is commonly used in `touchThreshold` for aggressive overlap handling,
|
402
|
+
* such as:
|
403
|
+
* - Hide one element if overlapping.
|
404
|
+
* - Two elements are vertically touching at top/bottom edges, but are restricted to move along
|
405
|
+
* the horizontal direction to resolve overlap.
|
406
|
+
*/
|
407
|
+
touchThreshold?: number
|
408
|
+
|
409
|
+
/**
|
410
|
+
* - If an intersection occur, set the intersection rect to it.
|
411
|
+
* - Otherwise,
|
412
|
+
* - If `clamp: true`, `outIntersectRect` is set with a clamped rect that is on the edge or corner
|
413
|
+
* of the first rect input to `intersect` method.
|
414
|
+
* - Otherwise, set to all NaN (it will not pass `contain` and `intersect`).
|
415
|
+
*/
|
416
|
+
outIntersectRect?: RectLike
|
417
|
+
clamp?: boolean;
|
418
|
+
}
|
419
|
+
|
420
|
+
/**
|
421
|
+
* [CAVEAT] Do not use it other than in `BoundingRect` and `OrientedBoundingRect`.
|
422
|
+
*/
|
423
|
+
export function createIntersectContext() {
|
424
|
+
|
425
|
+
let _direction: BoundingRectIntersectOpt['direction'] = 0;
|
426
|
+
const _dirCheckVec = new Point();
|
427
|
+
const _dirTmp = new Point();
|
428
|
+
|
429
|
+
const _ctx = {
|
430
|
+
minTv: new Point(),
|
431
|
+
maxTv: new Point(),
|
432
|
+
useDir: false as boolean,
|
433
|
+
dirMinTv: new Point(),
|
434
|
+
touchThreshold: 0 as BoundingRectIntersectOpt['touchThreshold'],
|
435
|
+
bidirectional: true as BoundingRectIntersectOpt['bidirectional'],
|
436
|
+
|
437
|
+
negativeSize: false as boolean,
|
438
|
+
|
439
|
+
reset(opt: BoundingRectIntersectOpt | NullUndefined, useMTV: boolean): void {
|
440
|
+
_ctx.touchThreshold = 0;
|
441
|
+
if (opt && opt.touchThreshold != null) {
|
442
|
+
_ctx.touchThreshold = mathMax(0, opt.touchThreshold);
|
443
|
+
}
|
444
|
+
_ctx.negativeSize = false;
|
445
|
+
|
446
|
+
if (!useMTV) {
|
447
|
+
return;
|
448
|
+
}
|
449
|
+
|
450
|
+
_ctx.minTv.set(Infinity, Infinity);
|
451
|
+
_ctx.maxTv.set(0, 0);
|
452
|
+
_ctx.useDir = false;
|
453
|
+
|
454
|
+
if (opt && opt.direction != null) {
|
455
|
+
_ctx.useDir = true;
|
456
|
+
_ctx.dirMinTv.copy(_ctx.minTv);
|
457
|
+
_dirTmp.copy(_ctx.minTv);
|
458
|
+
_direction = opt.direction;
|
459
|
+
_ctx.bidirectional = opt.bidirectional == null || !!opt.bidirectional;
|
460
|
+
if (!_ctx.bidirectional) {
|
461
|
+
_dirCheckVec.set(Math.cos(_direction), Math.sin(_direction));
|
462
|
+
}
|
463
|
+
}
|
464
|
+
},
|
465
|
+
|
466
|
+
calcDirMTV(): void {
|
467
|
+
const minTv = _ctx.minTv;
|
468
|
+
const dirMinTv = _ctx.dirMinTv;
|
469
|
+
const squareMag = minTv.y * minTv.y + minTv.x * minTv.x;
|
470
|
+
const dirSin = Math.sin(_direction);
|
471
|
+
const dirCos = Math.cos(_direction);
|
472
|
+
const dotProd = dirSin * minTv.y + dirCos * minTv.x;
|
473
|
+
|
474
|
+
if (nearZero(dotProd)) {
|
475
|
+
if (nearZero(minTv.x) && nearZero(minTv.y)) {
|
476
|
+
// The two OBBs touch at the edges.
|
477
|
+
dirMinTv.set(0, 0);
|
478
|
+
}
|
479
|
+
// Otherwise `minTv` is perpendicular to `this.direction`.
|
480
|
+
return;
|
481
|
+
}
|
482
|
+
|
483
|
+
_dirTmp.x = squareMag * dirCos / dotProd;
|
484
|
+
_dirTmp.y = squareMag * dirSin / dotProd;
|
485
|
+
if (nearZero(_dirTmp.x) && nearZero(_dirTmp.y)) {
|
486
|
+
// The result includes near-(0,0) regardless of `bidirectional`.
|
487
|
+
dirMinTv.set(0, 0);
|
488
|
+
return;
|
489
|
+
}
|
490
|
+
|
491
|
+
if ((
|
492
|
+
_ctx.bidirectional
|
493
|
+
|| _dirCheckVec.dot(_dirTmp) > 0
|
494
|
+
)
|
495
|
+
&& _dirTmp.len() < dirMinTv.len()
|
496
|
+
) {
|
497
|
+
dirMinTv.copy(_dirTmp);
|
498
|
+
}
|
499
|
+
}
|
500
|
+
};
|
501
|
+
|
502
|
+
function nearZero(val: number): boolean {
|
503
|
+
return mathAbs(val) < 1e-10; // Empirically OK for pixel-scale values.
|
504
|
+
}
|
505
|
+
|
506
|
+
return _ctx;
|
507
|
+
}
|
508
|
+
|
509
|
+
|
510
|
+
export default BoundingRect;
|
@@ -18,14 +18,20 @@
|
|
18
18
|
*/
|
19
19
|
|
20
20
|
import Point, { PointLike } from './Point';
|
21
|
-
import BoundingRect from './BoundingRect';
|
21
|
+
import BoundingRect, { BoundingRectIntersectOpt, createIntersectContext } from './BoundingRect';
|
22
22
|
import { MatrixArray } from './matrix';
|
23
23
|
|
24
|
-
const
|
25
|
-
const
|
24
|
+
const mathMin = Math.min;
|
25
|
+
const mathMax = Math.max;
|
26
|
+
const mathAbs = Math.abs;
|
27
|
+
|
28
|
+
const _extent = [0, 0];
|
29
|
+
const _extent2 = [0, 0];
|
30
|
+
|
31
|
+
const _intersectCtx = createIntersectContext();
|
32
|
+
const _minTv = _intersectCtx.minTv;
|
33
|
+
const _maxTv = _intersectCtx.maxTv;
|
26
34
|
|
27
|
-
const minTv = new Point();
|
28
|
-
const maxTv = new Point();
|
29
35
|
|
30
36
|
class OrientedBoundingRect {
|
31
37
|
|
@@ -80,36 +86,57 @@ class OrientedBoundingRect {
|
|
80
86
|
}
|
81
87
|
|
82
88
|
/**
|
83
|
-
* If intersect with another OBB
|
89
|
+
* If intersect with another OBB.
|
90
|
+
*
|
91
|
+
* [NOTICE]
|
92
|
+
* Touching the edge is considered an intersection.
|
93
|
+
* zero-width/height can still cause intersection if `touchThreshold` is 0.
|
94
|
+
* See more in `BoundingRectIntersectOpt['touchThreshold']`
|
95
|
+
*
|
84
96
|
* @param other Bounding rect to be intersected with
|
85
|
-
* @param mtv
|
86
|
-
* If it's not overlapped. it means needs to move
|
87
|
-
*
|
97
|
+
* @param mtv
|
98
|
+
* If it's not overlapped. it means needs to move `other` rect with Maximum Translation Vector to be overlapped.
|
99
|
+
* FIXME: Maximum Translation Vector is buggy. Fix it before using it. See case in `test/obb-collide.html`.
|
100
|
+
* Else it means needs to move `other` rect with Minimum Translation Vector to be not overlapped.
|
88
101
|
*/
|
89
|
-
intersect(
|
102
|
+
intersect(
|
103
|
+
other: OrientedBoundingRect,
|
104
|
+
mtv?: PointLike,
|
105
|
+
opt?: BoundingRectIntersectOpt
|
106
|
+
): boolean {
|
90
107
|
// OBB collision with SAT method
|
91
108
|
|
92
109
|
let overlapped = true;
|
93
110
|
const noMtv = !mtv;
|
94
|
-
|
95
|
-
|
111
|
+
|
112
|
+
if (mtv) {
|
113
|
+
Point.set(mtv, 0, 0);
|
114
|
+
}
|
115
|
+
|
116
|
+
_intersectCtx.reset(opt, !noMtv);
|
117
|
+
|
96
118
|
// Check two axes for both two obb.
|
97
|
-
if (!this._intersectCheckOneSide(this, other,
|
119
|
+
if (!this._intersectCheckOneSide(this, other, noMtv, 1)) {
|
98
120
|
overlapped = false;
|
99
121
|
if (noMtv) {
|
100
122
|
// Early return if no need to calculate mtv
|
101
123
|
return overlapped;
|
102
124
|
}
|
103
125
|
}
|
104
|
-
if (!this._intersectCheckOneSide(other, this,
|
126
|
+
if (!this._intersectCheckOneSide(other, this, noMtv, -1)) {
|
105
127
|
overlapped = false;
|
106
128
|
if (noMtv) {
|
107
129
|
return overlapped;
|
108
130
|
}
|
109
131
|
}
|
110
132
|
|
111
|
-
if (!noMtv) {
|
112
|
-
Point.copy(
|
133
|
+
if (!noMtv && !_intersectCtx.negativeSize) {
|
134
|
+
Point.copy(
|
135
|
+
mtv,
|
136
|
+
overlapped
|
137
|
+
? (_intersectCtx.useDir ? _intersectCtx.dirMinTv : _minTv)
|
138
|
+
: _maxTv
|
139
|
+
);
|
113
140
|
}
|
114
141
|
|
115
142
|
return overlapped;
|
@@ -119,46 +146,57 @@ class OrientedBoundingRect {
|
|
119
146
|
private _intersectCheckOneSide(
|
120
147
|
self: OrientedBoundingRect,
|
121
148
|
other: OrientedBoundingRect,
|
122
|
-
minTv: Point,
|
123
|
-
maxTv: Point,
|
124
149
|
noMtv: boolean,
|
125
|
-
inverse: 1 | -1
|
150
|
+
inverse: 1 | -1,
|
126
151
|
): boolean {
|
152
|
+
|
153
|
+
// [CAVEAT] Must not use `this` in this method.
|
154
|
+
|
127
155
|
let overlapped = true;
|
128
156
|
for (let i = 0; i < 2; i++) {
|
129
|
-
const axis =
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
//
|
134
|
-
|
157
|
+
const axis = self._axes[i];
|
158
|
+
self._getProjMinMaxOnAxis(i, self._corners, _extent);
|
159
|
+
self._getProjMinMaxOnAxis(i, other._corners, _extent2);
|
160
|
+
|
161
|
+
// Following the behavior in `BoundingRect.ts`, touching the edge is considered
|
162
|
+
// an overlap, but get a mtv [0, 0].
|
163
|
+
if (_intersectCtx.negativeSize || _extent[1] < _extent2[0] || _extent[0] > _extent2[1]) {
|
164
|
+
// Not overlap on the any axis.
|
135
165
|
overlapped = false;
|
136
|
-
if (noMtv) {
|
166
|
+
if (_intersectCtx.negativeSize || noMtv) {
|
137
167
|
return overlapped;
|
138
168
|
}
|
139
|
-
const dist0 =
|
140
|
-
const dist1 =
|
169
|
+
const dist0 = mathAbs(_extent2[0] - _extent[1]);
|
170
|
+
const dist1 = mathAbs(_extent[0] - _extent2[1]);
|
141
171
|
|
142
172
|
// Find longest distance of all axes.
|
143
|
-
if (
|
173
|
+
if (mathMin(dist0, dist1) > _maxTv.len()) {
|
144
174
|
if (dist0 < dist1) {
|
145
|
-
Point.scale(
|
175
|
+
Point.scale(_maxTv, axis, -dist0 * inverse);
|
146
176
|
}
|
147
177
|
else {
|
148
|
-
Point.scale(
|
178
|
+
Point.scale(_maxTv, axis, dist1 * inverse);
|
149
179
|
}
|
150
180
|
}
|
151
181
|
}
|
152
|
-
else if (
|
153
|
-
const dist0 =
|
154
|
-
const dist1 =
|
155
|
-
|
156
|
-
if (
|
157
|
-
|
158
|
-
|
182
|
+
else if (!noMtv) {
|
183
|
+
const dist0 = mathAbs(_extent2[0] - _extent[1]);
|
184
|
+
const dist1 = mathAbs(_extent[0] - _extent2[1]);
|
185
|
+
|
186
|
+
if (_intersectCtx.useDir || mathMin(dist0, dist1) < _minTv.len()) {
|
187
|
+
// If bidirectional, both dist0 dist1 need to check,
|
188
|
+
// otherwise only check the smaller one.
|
189
|
+
if (dist0 < dist1 || !_intersectCtx.bidirectional) {
|
190
|
+
Point.scale(_minTv, axis, dist0 * inverse);
|
191
|
+
if (_intersectCtx.useDir) {
|
192
|
+
_intersectCtx.calcDirMTV();
|
193
|
+
}
|
159
194
|
}
|
160
|
-
|
161
|
-
Point.scale(
|
195
|
+
if (dist0 >= dist1 || !_intersectCtx.bidirectional) {
|
196
|
+
Point.scale(_minTv, axis, -dist1 * inverse);
|
197
|
+
if (_intersectCtx.useDir) {
|
198
|
+
_intersectCtx.calcDirMTV();
|
199
|
+
}
|
162
200
|
}
|
163
201
|
}
|
164
202
|
}
|
@@ -166,7 +204,7 @@ class OrientedBoundingRect {
|
|
166
204
|
return overlapped;
|
167
205
|
}
|
168
206
|
|
169
|
-
private _getProjMinMaxOnAxis(dim: number, corners: Point[], out: number[]) {
|
207
|
+
private _getProjMinMaxOnAxis(dim: number, corners: Point[], out: number[]): void {
|
170
208
|
const axis = this._axes[dim];
|
171
209
|
const origin = this._origin;
|
172
210
|
const proj = corners[0].dot(axis) + origin[dim];
|
@@ -175,13 +213,15 @@ class OrientedBoundingRect {
|
|
175
213
|
|
176
214
|
for (let i = 1; i < corners.length; i++) {
|
177
215
|
const proj = corners[i].dot(axis) + origin[dim];
|
178
|
-
min =
|
179
|
-
max =
|
216
|
+
min = mathMin(proj, min);
|
217
|
+
max = mathMax(proj, max);
|
180
218
|
}
|
181
219
|
|
182
|
-
out[0] = min;
|
183
|
-
out[1] = max;
|
220
|
+
out[0] = min + _intersectCtx.touchThreshold;
|
221
|
+
out[1] = max - _intersectCtx.touchThreshold;
|
222
|
+
|
223
|
+
_intersectCtx.negativeSize = out[1] < out[0];
|
184
224
|
}
|
185
225
|
}
|
186
226
|
|
187
|
-
export default OrientedBoundingRect;
|
227
|
+
export default OrientedBoundingRect;
|