import { Box2 } from '../Box2.js' import { clipperCpp } from '../ClipperCpp.js' import type { NestFiler } from '../Filer.js' import type { Point } from '../Point.js' import { equaln } from '../Util.js' import { Vector2 } from '../Vector2.js' /** * 轮廓路径类 * 可以求NFP,和保存NFPCahce * 因为NFP结果是按照最低点移动的,所以将点旋转后,按照盒子将点移动到0点. */ export class Path { Id: number Points: Point[] OutsideNFPCache: { [key: number]: Point[][] } = {} InsideNFPCache: { [key: number]: Point[][] } = {} constructor(public OrigionPoints?: Point[], rotation: number = 0) { if (OrigionPoints) this.Init(OrigionPoints, rotation) } Origion: Path // 点表在旋转后的原始最小点.使用这个点将轮廓移动到0点 OrigionMinPoint: Vector2 Rotation: number Size: Vector2// 序列化 private 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 = clipperCpp.lib.simplifyPolygons(nfps) 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] let l = ax - bx let h = ay - by const MinNumber = 200// 清理的数值是100,所以200是可以接受的, 200=0.020问题不大(过盈配合) if (l < -MinNumber || h < -MinNumber) return if (l < MinNumber) l = MinNumber else l += MinNumber if (h < MinNumber) h = MinNumber else h += MinNumber nfps = [[ { x: 0, y: 0 }, { x: l, y: 0 }, { x: l, y: h }, { x: 0, y: h }, ]] } 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 TranslatePath_Self(pts: Point[], mx: number, my: number): Point[] { for (let pt of pts) { pt.x += mx pt.y += my } return pts } // 缩放点表,返回原始点表 export function PathScale(pts: Point[], scale: number): Point[] { for (let p of pts) { p.x *= scale p.y *= scale } return pts }