注意,这个功能是基于035的绘制画布UI控件而实现的!
具体效果:
export class QuadrilateralSelector {
private _enable: boolean = false;
private _clickPointBegin: mw.Vector2 = null;
private _clickPointEnd: mw.Vector2 = null;
private _befDrawID: number = null;
private readonly _vertex: mw.UIDrawCustomVertex[] = [];
private readonly _evs: mw.EventListener[] = [];
private readonly viewToScreenRate: { x: number, y: number } = { x: 1, y: 1 };
private _tempcanvas: mw.DrawCanvas = null;
private _isInnerCnvas: boolean = false;
/**绘制颜色 */
public readonly drawColor = new LinearColor(0, 1, 0, 0.5);
/**绘制画布 */
public getDrawCanvas: () => mw.DrawCanvas = null;
private createTempCanvas(): mw.DrawCanvas {
// if (!this._tempcanvas) {
// this._tempcanvas = new DrawCanvas();
// this._tempcanvas.setSize(WindowUtil.getViewportSize());
// this._tempcanvas.setPosition(new Vector2(0, 0));
// this._tempcanvas.setAnchor(new Vector2(0, 0));
// this._tempcanvas.setPivot(new Vector2(0, 0));
// }
const rootui = UIService.canvas;
this._tempcanvas = DrawCanvas.newObject(rootui);
this._tempcanvas.position = new Vector2(0, 0);
this._tempcanvas.size = rootui.size;
this._isInnerCnvas = true;
return this._tempcanvas;
}
/**选择回调 会触发4个视口坐标作为参数 */
public readonly selectCallback: Action1<mw.Vector2[]> = new Action1();
public readonly selectBeginCallback: Action = new Action();
public readonly selectEndCallback: Action = new Action();
/**执行转世界坐标时,用于射线检查,哪些物体算是可命中的点 */
public checkWorldObject: (g: mw.GameObject) => boolean = null;
constructor() {
const screenSize = WindowUtil.getViewportSize();
const viewSize = mw.getViewportSize();
// const [rateX, rateY] = [viewSize.x / screenSize.x, viewSize.y / screenSize.y]
this.viewToScreenRate.x = viewSize.x / screenSize.x;
this.viewToScreenRate.y = viewSize.y / screenSize.y;
}
/**视口坐标转屏幕坐标 */
public viewPointToScreen(v: Vector2): Vector2 {
return new Vector2(v.x * this.viewToScreenRate.x, v.y * this.viewToScreenRate.y);
}
public get enable() { return this._enable; }
public set enable(v: boolean) {
if (this._enable != v) {
this._enable = v;
if (v) {
this._evs.push(
InputUtil.onTouchBegin(this.touchBegin),
InputUtil.onTouchMove(this.touchMove),
InputUtil.onTouchEnd(this.touchEnd)
)
} else {
this._evs.forEach(ev => { ev.disconnect(); });
this._evs.length = 0;
}
}
}
private touchBegin = () => {
this.selectBeginCallback.call();
this._clickPointBegin = mw.getMousePositionOnViewport();
this._isInnerCnvas = false;
this._tempcanvas = this.getDrawCanvas ? this.getDrawCanvas() ?? this.createTempCanvas() : this.createTempCanvas();
}
private checkSimplePointDistance(p1: Vector2, p2: Vector2, checkLength: number = 3): boolean {
const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
return Math.abs(x1 - x2) > checkLength && Math.abs(y1 - y2) > checkLength;
}
private touchMove = () => {
if (!this._tempcanvas) { return; }
if (this._befDrawID != null) {
this._tempcanvas.removeDrawById(this._befDrawID);
}
this._clickPointEnd = mw.getMousePositionOnViewport();
if (!this.checkSimplePointDistance(this._clickPointBegin, this._clickPointEnd)) {
return;
}
const [xmin, ymin, xmax, ymax] = [
Math.min(this._clickPointBegin.x, this._clickPointEnd.x),
Math.min(this._clickPointBegin.y, this._clickPointEnd.y),
Math.max(this._clickPointBegin.x, this._clickPointEnd.x),
Math.max(this._clickPointBegin.y, this._clickPointEnd.y)
]
const begin = new Vector2(xmin, ymin);
const end = new Vector2(xmax, ymax);
this._vertex.length = 0;
let v1 = new UIDrawCustomVertex();
v1.color = this.drawColor;
v1.position = begin;
v1.texCoord = new Vector2(1, 1);
this._vertex.push(v1);
let v2 = new UIDrawCustomVertex();
v2.color = this.drawColor;
v2.position = new Vector2(end.x, begin.y);
v2.texCoord = new Vector2(1, 1);
this._vertex.push(v2);
let v3 = new UIDrawCustomVertex();
v3.color = this.drawColor;
v3.position = end;
v3.texCoord = new Vector2(1, 1);
this._vertex.push(v3);
let v4 = new UIDrawCustomVertex();
v4.color = this.drawColor;
v4.position = new Vector2(begin.x, end.y);
v4.texCoord = new Vector2(1, 1);
this._vertex.push(v4);
this._befDrawID = this._tempcanvas.drawCustom(this._vertex, [0, 1, 3, 3, 1, 2]);
this.selectCallback.call(this._vertex.map(v => { return v.position; }));
}
/**视口坐标转世界坐标 */
public getWorldPoint(viewPoint: mw.Vector2 | number[]): mw.Vector2 {
if (viewPoint instanceof Array) {
viewPoint = new Vector2(viewPoint[0], viewPoint[1]);
}
const screenPoint = this.viewPointToScreen(viewPoint);
const worldinfo = InputUtil.convertScreenLocationToWorldSpace(screenPoint.x, screenPoint.y);
if (worldinfo.result) {
const startLoc = worldinfo.worldPosition;
const dir = worldinfo.worldDirection;
const endLoc = Vector.add(startLoc, dir.multiply(99999));
let hits = mw.QueryUtil.lineTrace(startLoc, endLoc, true, false).filter(hit => {
return this.checkWorldObject ? this.checkWorldObject(hit.gameObject) : true;
});
let { x, y } = hits[0] ? hits[0].impactPoint : endLoc;
return new Vector2(x, y);
}
return null;
}
private touchEnd = () => {
if (this._befDrawID != null) {
this._tempcanvas.removeDrawById(this._befDrawID);
}
if (this._isInnerCnvas) {
this._tempcanvas.destroyObject();
this._tempcanvas = null;
}
this.selectEndCallback.call();
}
/**基于2维平面下,指定点 p 是否在 abcd 所成的四边形内 */
public isPointInsideQuadrilateral(a: Vector2, b: Vector2, c: Vector2, d: Vector2, p: Vector2): boolean {
// Calculate vectors for each side of the quadrilateral
const ab = Vector2.subtract(b, a);
const bc = Vector2.subtract(c, b);
const cd = Vector2.subtract(d, c);
const da = Vector2.subtract(a, d);
// Calculate vectors from each corner to point p
const ap = Vector2.subtract(p, a);
const bp = Vector2.subtract(p, b);
const cp = Vector2.subtract(p, c);
const dp = Vector2.subtract(p, d);
// Check if the cross product of each side vector with the corresponding corner-to-p vector is non-negative
const flag1 = Vector2.cross(ab, ap) >= 0;
const flag2 = Vector2.cross(bc, bp) >= 0;
const flag3 = Vector2.cross(cd, cp) >= 0;
const flag4 = Vector2.cross(da, dp) >= 0;
// If all flags are true, then point p is inside the quadrilateral
return flag1 && flag2 && flag3 && flag4;
}
}
上面动图的实现示例代码:
其中,这里使用的是tag作为检查标准,需要地面设置tag为 "2" ,可选的小方块tag设置为 "1"
具体实际逻辑代码可以自行定义,本质上是设置 SelectorTest.checkWorldObject 的值作为检查条件
let gs1: mw.GameObject[] = [];
const SelectorTest: QuadrilateralSelector = new QuadrilateralSelector();
//指示标签为2的物体作为辅助转世界坐标的射线命中物体
SelectorTest.checkWorldObject = g => { return g.tag === "2"; }
//逻辑代码,在进行拖拽开始和结束时,分别锁定和解锁相机
SelectorTest.selectBeginCallback.add(() => { cameraController.scrollCameraEnable = false; });
SelectorTest.selectEndCallback.add(() => { cameraController.scrollCameraEnable = true; });
SelectorTest.selectCallback.add((viewPoints) => {
//视口坐标转世界坐标
const [a, b, c, d] = viewPoints.map(v => { return SelectorTest.getWorldPoint(v); });
//检查所有感兴趣的物体
let finds = GameObject.findGameObjectsByTag("1");
finds = finds.filter(g => {
//目标物体所在世界坐标
const p = new Vector2(g.worldTransform.position.x, g.worldTransform.position.y);
//使用选择器辅助函数,判断目标物体是否在四边形内
return SelectorTest.isPointInsideQuadrilateral(a, b, c, d, p);
});
gs1?.forEach(g => {//取消上一次框选的描边
(g as Model).setPostProcessOutline(false);
})
gs1 = finds;
finds.forEach(g => {//描边
(g as Model).setPostProcessOutline(true, LinearColor.red, 3);
})
});
SelectorTest.enable = true;//启动选择器
|