346 lines
9.6 KiB
TypeScript
346 lines
9.6 KiB
TypeScript
![]() |
import { Box2 } from "../Common/Box2";
|
|||
|
import { clipperCpp } from "../Common/ClipperCpp";
|
|||
|
import { NestFiler } from "../Common/Filer";
|
|||
|
import { Point } from "../Common/Point";
|
|||
|
import { equaln } from "../Common/Util";
|
|||
|
import { Vector2 } from "../Common/Vector2";
|
|||
|
|
|||
|
/**
|
|||
|
* 轮廓路径类
|
|||
|
* 可以求NFP,和保存NFPCahce
|
|||
|
* 因为NFP结果是按照最低点移动的,所以将点旋转后,按照盒子将点移动到0点.
|
|||
|
*/
|
|||
|
export class Path
|
|||
|
{
|
|||
|
Id: number;
|
|||
|
Points: Point[];
|
|||
|
OutsideNFPCache: { [key: number]: Point[][]; } = {};
|
|||
|
InsideNFPCache: { [key: number]: Point[][]; } = {};
|
|||
|
|
|||
|
constructor(origionPoints?: Point[], rotation: number = 0)
|
|||
|
{
|
|||
|
if (origionPoints)
|
|||
|
this.Init(origionPoints, rotation);
|
|||
|
}
|
|||
|
|
|||
|
Origion: Path;
|
|||
|
//点表在旋转后的原始最小点.使用这个点将轮廓移动到0点
|
|||
|
OrigionMinPoint: Vector2;
|
|||
|
Rotation: number;
|
|||
|
|
|||
|
Size: Vector2;//序列化
|
|||
|
Init(origionPoints: Point[], rotation: number)
|
|||
|
{
|
|||
|
this.Rotation = rotation;
|
|||
|
if (rotation === 0)
|
|||
|
this.Points = origionPoints.map(p => { return { ...p }; });
|
|||
|
else
|
|||
|
{
|
|||
|
let c = Math.cos(rotation);
|
|||
|
let s = Math.sin(rotation);
|
|||
|
|
|||
|
let npts: Point[] = [];
|
|||
|
for (let p of origionPoints)
|
|||
|
{
|
|||
|
let x = p.x;
|
|||
|
let y = p.y;
|
|||
|
const x1 = x * c - y * s;
|
|||
|
const y1 = x * s + y * c;
|
|||
|
npts.push({ x: x1, y: y1 });
|
|||
|
}
|
|||
|
this.Points = npts;
|
|||
|
}
|
|||
|
|
|||
|
let box = new Box2();
|
|||
|
let v2 = new Vector2();
|
|||
|
for (let p of this.Points)
|
|||
|
{
|
|||
|
v2.x = p.x;
|
|||
|
v2.y = p.y;
|
|||
|
box.expandByPoint(v2);
|
|||
|
}
|
|||
|
|
|||
|
this.OrigionMinPoint = box.min;
|
|||
|
this.Size = box.max.sub(box.min);
|
|||
|
|
|||
|
for (let p of this.Points)
|
|||
|
{
|
|||
|
p.x -= box.min.x;
|
|||
|
p.y -= box.min.y;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
GetNFPs(path: Path, outside: boolean): Point[][]
|
|||
|
{
|
|||
|
// 寻找内轮廓时,面积应该比本path小,这个判断移交给使用者自己判断
|
|||
|
// if (!outside && this.Area < path.Area) return [];
|
|||
|
let nfps = clipperCpp.lib.minkowskiSumPath(this.BigIntPoints, path.MirrorPoints, true);
|
|||
|
nfps = nfps.filter((nfp) =>
|
|||
|
{
|
|||
|
let area = Area(nfp);
|
|||
|
if (area > 1) return outside;//第一个不一定是外轮廓,但是面积为正时肯定为外轮廓
|
|||
|
if (Math.abs(area) < 10) return false;//应该不用在移除这个了
|
|||
|
|
|||
|
let { x, y } = nfp[0];
|
|||
|
if (outside)
|
|||
|
{
|
|||
|
if (this.Area > path.Area)
|
|||
|
{
|
|||
|
let p = { x: path.InPoint.x + x, y: path.InPoint.y + y };
|
|||
|
if (p.x < 0 || p.y < 0 || p.x > this.BigSize.x || p.y > this.BigSize.y)
|
|||
|
return true;
|
|||
|
let dir = clipperCpp.lib.pointInPolygon(p, this.BigIntPoints);
|
|||
|
return dir === 0;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
let p = { x: this.InPoint.x - x, y: this.InPoint.y - y };
|
|||
|
if (p.x < 0 || p.y < 0 || p.x > path.BigSize.x || p.y > path.BigSize.y)
|
|||
|
return true;
|
|||
|
let dir = clipperCpp.lib.pointInPolygon(p, path.BigIntPoints);
|
|||
|
return dir === 0;
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
let p = { x: path.InPoint.x + x, y: path.InPoint.y + y };
|
|||
|
if (p.x < 0 || p.y < 0 || p.x > this.BigSize.x || p.y > this.BigSize.y)
|
|||
|
return false;
|
|||
|
let dir = clipperCpp.lib.pointInPolygon(p, this.BigIntPoints);
|
|||
|
return dir === 1;
|
|||
|
}
|
|||
|
});
|
|||
|
return nfps;
|
|||
|
}
|
|||
|
|
|||
|
GetOutsideNFP(path: Path): Point[][]
|
|||
|
{
|
|||
|
let nfps = this.OutsideNFPCache[path.Id];
|
|||
|
if (nfps) return nfps;
|
|||
|
|
|||
|
if (this.IsRect && path.IsRect)
|
|||
|
{
|
|||
|
let [ax, ay] = [this.Size.x * 1e4, this.Size.y * 1e4];
|
|||
|
let [bx, by] = [path.Size.x * 1e4, path.Size.y * 1e4];
|
|||
|
nfps = [[
|
|||
|
{ x: -bx, y: -by },
|
|||
|
{ x: ax, y: -by },
|
|||
|
{ x: ax, y: ay },
|
|||
|
{ x: -bx, y: ay },
|
|||
|
]];
|
|||
|
}
|
|||
|
else
|
|||
|
nfps = this.GetNFPs(path, true);
|
|||
|
this.OutsideNFPCache[path.Id] = nfps;
|
|||
|
//虽然有这种神奇的特性,但是好像并不会提高性能。
|
|||
|
// path.OutsideNFPCache[this.id] = (this, nfps.map(nfp =>
|
|||
|
// {
|
|||
|
// return nfp.map(p =>
|
|||
|
// {
|
|||
|
// return { x: -p.x, y: -p.y };
|
|||
|
// });
|
|||
|
// }));
|
|||
|
return nfps;
|
|||
|
}
|
|||
|
GetInsideNFP(path: Path): Point[][]
|
|||
|
{
|
|||
|
if (path.Area > this.Area) return;
|
|||
|
let nfp = this.InsideNFPCache[path.Id];
|
|||
|
if (nfp) return nfp;
|
|||
|
|
|||
|
let nfps: Point[][];
|
|||
|
if (this.IsRect)
|
|||
|
{
|
|||
|
let [ax, ay] = [this.Size.x * 1e4, this.Size.y * 1e4];
|
|||
|
let [bx, by] = [path.Size.x * 1e4, path.Size.y * 1e4];
|
|||
|
|
|||
|
if (ax === bx) ax += 1;
|
|||
|
if (ay === by) ay += 1;
|
|||
|
|
|||
|
if (bx > ax || by > ay)
|
|||
|
return;
|
|||
|
nfps = [[
|
|||
|
{ x: 0, y: 0 },
|
|||
|
{ x: ax - bx, y: 0 },
|
|||
|
{ x: ax - bx, y: ay - by },
|
|||
|
{ x: 0, y: ay - by }
|
|||
|
]];
|
|||
|
}
|
|||
|
else
|
|||
|
nfps = this.GetNFPs(path, false);
|
|||
|
|
|||
|
if (path.Id !== undefined)
|
|||
|
this.InsideNFPCache[path.Id] = nfps;
|
|||
|
return nfps;
|
|||
|
}
|
|||
|
|
|||
|
private _InPoint: Point;
|
|||
|
|
|||
|
/**
|
|||
|
* 用这个点来检测是否在Path内部
|
|||
|
*/
|
|||
|
private get InPoint()
|
|||
|
{
|
|||
|
if (this._InPoint) return this._InPoint;
|
|||
|
let mp = { x: (this.Points[0].x + this.Points[1].x) / 2, y: (this.Points[0].y + this.Points[1].y) / 2 };
|
|||
|
let normal = new Vector2(this.Points[1].x - this.Points[0].x, this.Points[1].y - this.Points[0].y).normalize();
|
|||
|
// [normal.x, normal.y] = [normal.y, -normal.x];
|
|||
|
mp.x -= normal.y;
|
|||
|
mp.y += normal.x;
|
|||
|
|
|||
|
mp.x *= 1e4;
|
|||
|
mp.y *= 1e4;
|
|||
|
this._InPoint = mp;
|
|||
|
return mp;
|
|||
|
}
|
|||
|
|
|||
|
protected _BigIntPoints: Point[];
|
|||
|
get BigIntPoints()
|
|||
|
{
|
|||
|
if (this._BigIntPoints) return this._BigIntPoints;
|
|||
|
this._BigIntPoints = this.Points.map(p =>
|
|||
|
{
|
|||
|
return {
|
|||
|
x: Math.round(p.x * 1e4),
|
|||
|
y: Math.round(p.y * 1e4),
|
|||
|
};
|
|||
|
});
|
|||
|
return this._BigIntPoints;
|
|||
|
}
|
|||
|
|
|||
|
private _BigSize: Vector2;
|
|||
|
get BigSize()
|
|||
|
{
|
|||
|
if (this._BigSize) return this._BigSize;
|
|||
|
this._BigSize = new Vector2(this.Size.x * 1e4, this.Size.y * 1e4);
|
|||
|
return this._BigSize;
|
|||
|
}
|
|||
|
|
|||
|
protected _MirrorPoints: Point[];
|
|||
|
get MirrorPoints()
|
|||
|
{
|
|||
|
if (!this._MirrorPoints)
|
|||
|
this._MirrorPoints = this.BigIntPoints.map(p =>
|
|||
|
{
|
|||
|
return { x: -p.x, y: -p.y };
|
|||
|
});
|
|||
|
|
|||
|
return this._MirrorPoints;
|
|||
|
}
|
|||
|
|
|||
|
protected _BoundingBox: Box2;
|
|||
|
get BoundingBox()
|
|||
|
{
|
|||
|
if (!this._BoundingBox)
|
|||
|
this._BoundingBox = new Box2(new Vector2, this.Size);
|
|||
|
return this._BoundingBox;
|
|||
|
}
|
|||
|
|
|||
|
protected _Area: number;
|
|||
|
get Area()
|
|||
|
{
|
|||
|
if (this._Area === undefined)
|
|||
|
this._Area = Area(this.Points);
|
|||
|
return this._Area;
|
|||
|
}
|
|||
|
set Area(a: number)
|
|||
|
{
|
|||
|
this._Area = a;
|
|||
|
}
|
|||
|
|
|||
|
private _IsRect: boolean;
|
|||
|
get IsRect()
|
|||
|
{
|
|||
|
if (this._IsRect === undefined)
|
|||
|
{
|
|||
|
let s = this.BoundingBox.getSize(new Vector2);
|
|||
|
this._IsRect = equaln(this.Area, s.x * s.y, 1);
|
|||
|
}
|
|||
|
return this._IsRect;
|
|||
|
}
|
|||
|
|
|||
|
ReadFile(file: NestFiler): void
|
|||
|
{
|
|||
|
let ver = file.Read();
|
|||
|
this.Id = file.Read();
|
|||
|
let arr = file.Read();
|
|||
|
this.Points = [];
|
|||
|
for (let i = 0; i < arr.length; i += 2)
|
|||
|
{
|
|||
|
let p = { x: arr[i], y: arr[i + 1] };
|
|||
|
this.Points.push(p);
|
|||
|
}
|
|||
|
|
|||
|
this.Size = new Vector2(file.Read(), file.Read());
|
|||
|
this._Area = file.Read();
|
|||
|
let id = file.Read();
|
|||
|
if (id !== -1)
|
|||
|
{
|
|||
|
this.Origion = id;
|
|||
|
this.Rotation = file.Read();
|
|||
|
this.OrigionMinPoint = new Vector2(file.Read(), file.Read());
|
|||
|
}
|
|||
|
}
|
|||
|
WriteFile(file: NestFiler): void
|
|||
|
{
|
|||
|
file.Write(1);//ver
|
|||
|
file.Write(this.Id);
|
|||
|
let arr: number[] = [];
|
|||
|
for (let p of this.Points)
|
|||
|
arr.push(p.x, p.y);
|
|||
|
file.Write(arr);
|
|||
|
|
|||
|
file.Write(this.Size.x);
|
|||
|
file.Write(this.Size.y);
|
|||
|
file.Write(this._Area);
|
|||
|
if (this.Origion && this.Origion.Id)
|
|||
|
{
|
|||
|
//如果有原始的id,则传递它,以便后续进行NFP复用.
|
|||
|
file.Write(this.Origion.Id);
|
|||
|
file.Write(this.Rotation);
|
|||
|
file.Write(this.OrigionMinPoint.x);
|
|||
|
file.Write(this.OrigionMinPoint.y);
|
|||
|
}
|
|||
|
else
|
|||
|
file.Write(-1);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
//点表面积
|
|||
|
export function Area(pts: Point[]): number
|
|||
|
{
|
|||
|
let cnt = pts.length;
|
|||
|
if (cnt < 3)
|
|||
|
return 0;
|
|||
|
let a = 0;
|
|||
|
for (let i = 0, j = cnt - 1; i < cnt; ++i)
|
|||
|
{
|
|||
|
a += (pts[j].x + pts[i].x) * (pts[j].y - pts[i].y);
|
|||
|
j = i;
|
|||
|
}
|
|||
|
return -a * 0.5;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 平移点表,返回新点表
|
|||
|
*/
|
|||
|
export function TranslatePath(pts: Point[], p: Point): Point[]
|
|||
|
{
|
|||
|
return pts.map(px =>
|
|||
|
{
|
|||
|
return { x: p.x + px.x, y: p.y + px.y };
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
//缩放点表,返回原始点表
|
|||
|
export function PathScale(pts: Point[], scale: number): Point[]
|
|||
|
{
|
|||
|
for (let p of pts)
|
|||
|
{
|
|||
|
p.x *= scale;
|
|||
|
p.y *= scale;
|
|||
|
}
|
|||
|
return pts;
|
|||
|
}
|