zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
228 lines (193 loc) • 7.57 kB
text/typescript
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Point, { PointLike } from './Point';
import BoundingRect, { BoundingRectIntersectOpt, createIntersectContext } from './BoundingRect';
import { MatrixArray } from './matrix';
const mathMin = Math.min;
const mathMax = Math.max;
const mathAbs = Math.abs;
const _extent = [0, 0];
const _extent2 = [0, 0];
const _intersectCtx = createIntersectContext();
const _minTv = _intersectCtx.minTv;
const _maxTv = _intersectCtx.maxTv;
class OrientedBoundingRect {
// lt, rt, rb, lb
private _corners: Point[] = [];
private _axes: Point[] = [];
private _origin: number[] = [0, 0];
constructor(rect?: BoundingRect, transform?: MatrixArray) {
for (let i = 0; i < 4; i++) {
this._corners[i] = new Point();
}
for (let i = 0; i < 2; i++) {
this._axes[i] = new Point();
}
if (rect) {
this.fromBoundingRect(rect, transform);
}
}
fromBoundingRect(rect: BoundingRect, transform?: MatrixArray) {
const corners = this._corners;
const axes = this._axes;
const x = rect.x;
const y = rect.y;
const x2 = x + rect.width;
const y2 = y + rect.height;
corners[0].set(x, y);
corners[1].set(x2, y);
corners[2].set(x2, y2);
corners[3].set(x, y2);
if (transform) {
for (let i = 0; i < 4; i++) {
corners[i].transform(transform);
}
}
// Calculate axes
Point.sub(axes[0], corners[1], corners[0]);
Point.sub(axes[1], corners[3], corners[0]);
axes[0].normalize();
axes[1].normalize();
// Calculate projected origin
for (let i = 0; i < 2; i++) {
this._origin[i] = axes[i].dot(corners[0]);
}
}
/**
* If intersect with another OBB.
*
* [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 other Bounding rect to be intersected with
* @param mtv
* If it's not overlapped. it means needs to move `other` rect with Maximum Translation Vector to be overlapped.
* FIXME: Maximum Translation Vector is buggy. Fix it before using it. See case in `test/obb-collide.html`.
* Else it means needs to move `other` rect with Minimum Translation Vector to be not overlapped.
*/
intersect(
other: OrientedBoundingRect,
mtv?: PointLike,
opt?: BoundingRectIntersectOpt
): boolean {
// OBB collision with SAT method
let overlapped = true;
const noMtv = !mtv;
if (mtv) {
Point.set(mtv, 0, 0);
}
_intersectCtx.reset(opt, !noMtv);
// Check two axes for both two obb.
if (!this._intersectCheckOneSide(this, other, noMtv, 1)) {
overlapped = false;
if (noMtv) {
// Early return if no need to calculate mtv
return overlapped;
}
}
if (!this._intersectCheckOneSide(other, this, noMtv, -1)) {
overlapped = false;
if (noMtv) {
return overlapped;
}
}
if (!noMtv && !_intersectCtx.negativeSize) {
Point.copy(
mtv,
overlapped
? (_intersectCtx.useDir ? _intersectCtx.dirMinTv : _minTv)
: _maxTv
);
}
return overlapped;
}
private _intersectCheckOneSide(
self: OrientedBoundingRect,
other: OrientedBoundingRect,
noMtv: boolean,
inverse: 1 | -1,
): boolean {
// [CAVEAT] Must not use `this` in this method.
let overlapped = true;
for (let i = 0; i < 2; i++) {
const axis = self._axes[i];
self._getProjMinMaxOnAxis(i, self._corners, _extent);
self._getProjMinMaxOnAxis(i, other._corners, _extent2);
// Following the behavior in `BoundingRect.ts`, touching the edge is considered
// an overlap, but get a mtv [0, 0].
if (_intersectCtx.negativeSize || _extent[1] < _extent2[0] || _extent[0] > _extent2[1]) {
// Not overlap on the any axis.
overlapped = false;
if (_intersectCtx.negativeSize || noMtv) {
return overlapped;
}
const dist0 = mathAbs(_extent2[0] - _extent[1]);
const dist1 = mathAbs(_extent[0] - _extent2[1]);
// Find longest distance of all axes.
if (mathMin(dist0, dist1) > _maxTv.len()) {
if (dist0 < dist1) {
Point.scale(_maxTv, axis, -dist0 * inverse);
}
else {
Point.scale(_maxTv, axis, dist1 * inverse);
}
}
}
else if (!noMtv) {
const dist0 = mathAbs(_extent2[0] - _extent[1]);
const dist1 = mathAbs(_extent[0] - _extent2[1]);
if (_intersectCtx.useDir || mathMin(dist0, dist1) < _minTv.len()) {
// If bidirectional, both dist0 dist1 need to check,
// otherwise only check the smaller one.
if (dist0 < dist1 || !_intersectCtx.bidirectional) {
Point.scale(_minTv, axis, dist0 * inverse);
if (_intersectCtx.useDir) {
_intersectCtx.calcDirMTV();
}
}
if (dist0 >= dist1 || !_intersectCtx.bidirectional) {
Point.scale(_minTv, axis, -dist1 * inverse);
if (_intersectCtx.useDir) {
_intersectCtx.calcDirMTV();
}
}
}
}
}
return overlapped;
}
private _getProjMinMaxOnAxis(dim: number, corners: Point[], out: number[]): void {
const axis = this._axes[dim];
const origin = this._origin;
const proj = corners[0].dot(axis) + origin[dim];
let min = proj;
let max = proj;
for (let i = 1; i < corners.length; i++) {
const proj = corners[i].dot(axis) + origin[dim];
min = mathMin(proj, min);
max = mathMax(proj, max);
}
out[0] = min + _intersectCtx.touchThreshold;
out[1] = max - _intersectCtx.touchThreshold;
_intersectCtx.negativeSize = out[1] < out[0];
}
}
export default OrientedBoundingRect;