From 5c74b7f84051afd6961c0010afddc7028fe9710a Mon Sep 17 00:00:00 2001 From: ChenX Date: Thu, 28 Nov 2019 11:42:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/webpack.dll.ts | 1 + package.json | 1 + src/Nest/Container.ts | 125 ++++++++++++++----------- src/Nest/Converter/ConverBoard2Part.ts | 5 +- src/Nest/ConvexHull2D.ts | 8 ++ src/Nest/EvaluateType.ts | 11 +++ src/Nest/Individual.ts | 25 +++-- src/Nest/OptimizeMachine.ts | 85 +++++++---------- src/Nest/Part.ts | 19 ++-- src/Nest/Path.ts | 24 ++++- src/Nest/PlaceType.ts | 8 +- src/Nest/Random.ts | 6 +- src/Nest/Shuffle.ts | 8 ++ src/Nest/Test/TestContainer.ts | 9 +- src/Nest/Test/TestHull.ts | 24 +++++ 15 files changed, 210 insertions(+), 149 deletions(-) create mode 100644 src/Nest/ConvexHull2D.ts create mode 100644 src/Nest/EvaluateType.ts create mode 100644 src/Nest/Shuffle.ts create mode 100644 src/Nest/Test/TestHull.ts diff --git a/config/webpack.dll.ts b/config/webpack.dll.ts index a911ac08b..aa888da85 100644 --- a/config/webpack.dll.ts +++ b/config/webpack.dll.ts @@ -18,6 +18,7 @@ const vendors = [ "dxf-parser", "clipper-js-fpoint", "pako", + "monotone-convex-hull-2d", //如果你想调试threejs的代码,那么你应该注释掉下面的代码,然后重新构建dll "three", "three/examples/jsm/loaders/FBXLoader", diff --git a/package.json b/package.json index c18154eda..3a2860714 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "golden-layout": "^1.5.9", "mobx": "^5.15.0", "mobx-react": "^6.1.4", + "monotone-convex-hull-2d": "^1.0.1", "p-queue": "^6.2.1", "pako": "^1.0.10", "react": "^16.12.0", diff --git a/src/Nest/Container.ts b/src/Nest/Container.ts index 7d2529db1..7d1de9071 100644 --- a/src/Nest/Container.ts +++ b/src/Nest/Container.ts @@ -1,35 +1,37 @@ -import { Path } from "./Path"; -import { Part } from "./Part"; -import { Box3, Vector3, Matrix4 } from "three"; -import { Point } from "./Point"; -import { Clipper, PolyType, Paths, ClipType, PolyFillType } from "clipper-js-fpoint"; +import { Clipper, ClipType, Paths, PolyFillType, PolyType } from "clipper-js-fpoint"; +import { Box3, Matrix4, Vector3 } from "three"; import { arrayRemoveIf } from "../Common/ArrayExt"; -import { equaln, AsVector3 } from "../Geometry/GeUtils"; +import { AsVector3, equaln } from "../Geometry/GeUtils"; +import { ConvexHull2D } from "./ConvexHull2D"; import { NestCache } from "./NestCache"; +import { Part } from "./Part"; +import { Path } from "./Path"; +import { PlaceType } from "./PlaceType"; +import { Point } from "./Point"; export class Container { - Holes: Map = new Map(); + Holes: Container[] = []; //放置状态 PlacedArea = 0; PlacedBox: Box3 = new Box3(); Placed: Part[] = []; - + PlacedHull: Point[]; + PlaceType: PlaceType = PlaceType.Box; OCS: Matrix4 = new Matrix4(); StatusKey: string; constructor(protected BinPath: Path) { - this.StatusKey = this.BinPath.Id.toString(); + this.StatusKey = this.BinPath.Id.toString() + "," + this.PlaceType; } get PlacedAll() { let ps: Part[] = [...this.Placed]; - for (let [, cs] of this.Holes) + for (let c of this.Holes) { - for (let c of cs) - ps.push(...c.PlacedAll); + ps.push(...c.PlacedAll); } return ps; } @@ -43,25 +45,19 @@ export class Container PutPart(part: Part): boolean { //------------先放置在孔洞内------------ - if (this.Holes) + for (let c of this.Holes) { - for (let [, containers] of this.Holes) - { - for (let c of containers) - { - if (c.PutPart(part)) - return true; - } - } + if (c.PutPart(part)) + return true; } let cacheKey = this.StatusKey + "," + part.State.PlaceOutline.Id; + let cacheP = NestCache.PositionCache.get(cacheKey); //读取缓存位置 - if (NestCache.PositionCache.has(cacheKey)) + if (cacheP) { NestCache.count1++; - let p = NestCache.PositionCache.get(cacheKey); - part.PlacePosition = p; + part.PlacePosition = cacheP; this.AppendPartHoles(part); return true; } @@ -102,10 +98,13 @@ export class Container } } - let combinedNfp = []; + let combinedNfp: Point[][] = []; if (!clipper.Execute(ClipType.ctUnion, combinedNfp, PolyFillType.pftNonZero, PolyFillType.pftNonZero)) return false; + combinedNfp = Clipper.CleanPolygons(combinedNfp, 0.1); + combinedNfp = combinedNfp.filter(nfp => { return nfp.length > 2; }); + if (combinedNfp.length === 0) return false; //binNfp 减去 combinedNfp 得到最终的nfp @@ -117,46 +116,55 @@ export class Container return false; //清理NFP - finalNfp = Clipper.CleanPolygons(finalNfp, 1); - arrayRemoveIf(finalNfp, nfp => - { - return (nfp.length < 3 || Math.abs(Clipper.Area(nfp)) < 2); - }); + finalNfp = Clipper.CleanPolygons(finalNfp, 0.1); + arrayRemoveIf(finalNfp, nfp => nfp.length < 2); if (finalNfp.length === 0) return false; - /** - * 选择零件放置后的最优占地空间(box). - * 使用凸包看起来不是最佳的 - * TODO:使用重力方向 - */ - let minWidth = Infinity; let minArea: number = Infinity; - let minX: number = Infinity; - let minY: number = Infinity; - let minBox: Box3; - let translate: Vector3; + let translate: Point; + + let tempVec = new Vector3; for (let nfp of finalNfp) { for (let p of nfp) { - let translateTemp = AsVector3(p); + tempVec.set(p.x, p.y, 0); let box2 = part.State.PlaceOutline.BoundingBox.clone(); - box2.translate(translateTemp); + box2.translate(tempVec); let rectBox = this.PlacedBox.clone().union(box2); let size = rectBox.getSize(new Vector3()); - //宽度的权重更大,使得零件的放置优先宽度较小 - let area = (size.x * 1.1) * size.y; - if (area < minArea - || (equaln(minArea, area) && translateTemp.x < minX) - || (equaln(minArea, area) && translateTemp.y < minY)) + + switch (this.PlaceType) { - minArea = area; - minWidth = size.x; - translate = translateTemp; - minX = translateTemp.x; - minY = translateTemp.y; - minBox = rectBox; + case PlaceType.Box: + { + let area = size.x * size.y; + if (area < minArea || + ((equaln(area, minArea, 1)) + && (this.BinPath.Size.x > this.BinPath.Size.y ? p.x < translate.x : p.y < translate.y))) + { + translate = p; + minArea = area; + } + break; + } + case PlaceType.Hull: + { + let pts = Clipper.TranslatePath(part.State.PlaceOutline._Points, p); + let nhull = ConvexHull2D([...pts, ...this.PlacedHull]); + let area = Math.abs(Clipper.Area(nhull)); + if (area < minArea || + ((equaln(area, minArea, 1)) + && (this.BinPath.Size.x > this.BinPath.Size.y ? p.x < translate.x : p.y < translate.y))) + { + translate = p; + minArea = area; + } + break; + } + default: + break; } } } @@ -181,6 +189,15 @@ export class Container this.StatusKey += "," + part.State.PlaceOutline.Id; NestCache.PositionCache.set(this.StatusKey, part.PlacePosition); + //凸包 + if (this.PlaceType === PlaceType.Hull) + { + if (!this.PlacedHull) + this.PlacedHull = part.State.PlaceOutline._Points; + else + this.PlacedHull = ConvexHull2D([...this.PlacedHull, ...part.State.PlaceOutline._Points]); + } + part.Container = this; let holes: Container[] = []; for (let h of part.Holes) @@ -193,7 +210,7 @@ export class Container } if (holes.length > 0) - this.Holes.set(part, holes); + this.Holes.push(...holes); } /** diff --git a/src/Nest/Converter/ConverBoard2Part.ts b/src/Nest/Converter/ConverBoard2Part.ts index 9993f04cf..76493f05c 100644 --- a/src/Nest/Converter/ConverBoard2Part.ts +++ b/src/Nest/Converter/ConverBoard2Part.ts @@ -47,10 +47,13 @@ export function Polylin2Points(pl: Polyline, outside: boolean, knifRadius: numbe let arc = pl.GetCurveAtIndex(i) as Arc; let allAngle = arc.AllAngle; + let arcLength = arc.Length; let splitCount = Math.round(allAngle / 0.4); - if (arc.Length < 300) + if (arcLength < 300) splitCount = 2; + else + splitCount = Math.max(arcLength / 200, splitCount); let radius = arc.Radius; if (outside === bul > 0) diff --git a/src/Nest/ConvexHull2D.ts b/src/Nest/ConvexHull2D.ts new file mode 100644 index 000000000..dad607902 --- /dev/null +++ b/src/Nest/ConvexHull2D.ts @@ -0,0 +1,8 @@ +import { Point } from "./Point"; +import convexHull from 'monotone-convex-hull-2d'; +export function ConvexHull2D(points: Point[]): Point[] +{ + let pts = points.map(p => [p.x, p.y]); + let indexs = convexHull(pts); + return indexs.map(i => points[i]); +} diff --git a/src/Nest/EvaluateType.ts b/src/Nest/EvaluateType.ts new file mode 100644 index 000000000..6f76a6bae --- /dev/null +++ b/src/Nest/EvaluateType.ts @@ -0,0 +1,11 @@ + +/** + * 评估类型, + * 最后一块板的时候,可能希望使用宽度使用量去评估,或者希望是高度使用量 + */ +export enum EvaluateType +{ + Box = 1, + Width = 2, + Length = 3, +} diff --git a/src/Nest/Individual.ts b/src/Nest/Individual.ts index 927ce3314..36a4989b5 100644 --- a/src/Nest/Individual.ts +++ b/src/Nest/Individual.ts @@ -3,6 +3,7 @@ import { Container } from "./Container"; import { Part } from "./Part"; import { Path } from "./Path"; import { RandomIndex } from "./Random"; +import { PlaceType } from "./PlaceType"; /** * 个体 @@ -35,7 +36,7 @@ export class Individual */ Mutate() { - if (this.mutationRate > 0.5) + if (this.mutationRate > 0.6) for (let i = 0; i < this.Parts.length; i++) { let rand = Math.random(); @@ -46,9 +47,9 @@ export class Individual if (j < this.Parts.length) [this.Parts[i], this.Parts[j]] = [this.Parts[j], this.Parts[i]]; } - if (rand < this.mutationRate * 0.3) + if (rand < this.mutationRate) { - this.Parts[i].Rotate(); + this.Parts[i].Mutate(); break; } } @@ -58,19 +59,16 @@ export class Individual if (rand < 0.5) { let index = RandomIndex(this.Parts.length - 2); - let count = Math.ceil(RandomIndex(this.Parts.length - 2 - index) * this.mutationRate * 2); - count = Math.ceil(count * this.mutationRate * 2) || 1; + let count = Math.ceil(RandomIndex(this.Parts.length - 2 - index) * this.mutationRate * 2) || 1; let parts = this.Parts.splice(index, count); this.Parts.push(...parts); } - else - { - let index = RandomIndex(this.Parts.length); - this.Parts[index].Rotate(); - } + + let index = RandomIndex(this.Parts.length); + this.Parts[index].Mutate(); if (this.mutationRate > 0.2) - this.mutationRate -= 0.01; + this.mutationRate -= 0.004; return this; } @@ -78,7 +76,7 @@ export class Individual /** * 评估健康程度 */ - async Evaluate(bin: Path, bestCount: number) + Evaluate(bin: Path, bestCount: number) { let parts = this.Parts; this.Containers = []; @@ -90,7 +88,8 @@ export class Individual return; }; let container = new Container(bin); - if (this.mutationRate > 0.21) + container.PlaceType = PlaceType.Box; + if (this.mutationRate > 0.5) { let area = 0; let maxP: Part; diff --git a/src/Nest/OptimizeMachine.ts b/src/Nest/OptimizeMachine.ts index 7a6548b4c..784ddafc4 100644 --- a/src/Nest/OptimizeMachine.ts +++ b/src/Nest/OptimizeMachine.ts @@ -1,11 +1,12 @@ -import { Path } from "./Path"; -import { Part } from "./Part"; -import { Individual } from "./Individual"; -import { Sleep } from "../Common/Sleep"; -import { NestCache } from "./NestCache"; +import { Vector3 } from "three"; import { arrayRemoveIf } from "../Common/ArrayExt"; +import { Sleep } from "../Common/Sleep"; import { MoveMatrix } from "../Geometry/GeUtils"; -import { Vector3 } from "three"; +import { Individual } from "./Individual"; +import { NestCache } from "./NestCache"; +import { Part } from "./Part"; +import { Path } from "./Path"; +import { ShuffleArray } from "./Shuffle"; /** * 优化器 @@ -62,27 +63,13 @@ export class OptimizeMachine console.log(this.Parts.length); this._IsSuspend = false; NestCache.Clear(); - - //1.初始化种群(n*个体) - this._Individuals = [ - new Individual(this.Parts, 0.8), - new Individual(this.Parts.map(p => p.Clone()).reverse(), 0.8), - ]; - //对插洗牌 - let parts: Part[] = []; - let count = Math.floor(this.Parts.length / 2); - for (let i = 0; i < count; i++) - parts.push(this.Parts[i].Clone(), this.Parts[this.Parts.length - 1 - i].Clone()); - if ((this.Parts.length & 1) === 1) - parts.push(this.Parts[count].Clone()); - - this._Individuals.push(new Individual(parts, 0.8)); - - for (let i = 3; i < this.Config.PopulationCount; i++) + this.Parts.sort((p1, p2) => p2.State.PlaceOutline.Area - p1.State.PlaceOutline.Area); + this._Individuals = [new Individual(this.Parts, 0.8)]; + for (let i = 1; i < this.Config.PopulationCount; i++) { - let p = this._Individuals[i % 3].Mutate(); - p.mutationRate = 0.8; - this._Individuals.push(p); + let parts = this.Parts.map(p => p.Clone()); + ShuffleArray(parts); + this._Individuals.push(new Individual(parts, 0.8)); } //2.执行 await this.Run(); @@ -90,6 +77,7 @@ export class OptimizeMachine calcCount = 0; best = Infinity; + bestP: Individual; bestCount = 0; private async Run() { @@ -98,47 +86,30 @@ export class OptimizeMachine //开始自然选择 while (!this._IsSuspend) //实验停止信号 { - let bestV = Infinity; - let bestP: Individual; - let badV = -Infinity; - let badIndex: number; - + this.calcCount += this._Individuals.length; + let goBack = this.calcCount - this.bestCount > 8000; // console.time(); //1.适应环境(放置零件) for (let i = 0; i < this._Individuals.length; i++) { let p = this._Individuals[i]; - await p.Evaluate(this.Bin, this.best); - if (p.Fitness < bestV) - { - bestV = p.Fitness; - bestP = p; - } - if (p.Fitness > badV) - { - badV = p.Fitness; - badIndex = i; - } + p.Evaluate(this.Bin, this.best); + if (goBack) + p.mutationRate = 0.5; } // console.timeEnd(); await Sleep(0); - this.calcCount += this._Individuals.length; - //自然选择 - this._Individuals.push(bestP.Clone());//繁衍它 - this._Individuals.splice(badIndex, 1);//杀死它 - - - if (this.calcCount - this.bestCount > 8000) - { - for (let i of this._Individuals) - i.mutationRate = 0.5; - } + if (this.calcCount % 5000 === 0) + console.timeLog("1", this.best, this.calcCount, NestCache.count1, NestCache.count2); + this._Individuals.sort((i1, i2) => i1.Fitness - i2.Fitness); + let bestP = this._Individuals[0]; //回调最好的 if (bestP.Fitness < this.best) { this.best = bestP.Fitness; + this.bestP = bestP; console.timeLog("1", this.best, this.calcCount, NestCache.count1, NestCache.count2); if (this.callBack) { @@ -146,6 +117,14 @@ export class OptimizeMachine } } + //自然选择 + this._Individuals.splice(-10);//杀死它 + for (let i = 0; i < 5; i++) + this._Individuals.push(bestP.Clone()); + for (let i = 0; i < 3; i++) + this._Individuals.push(this._Individuals[1].Clone()); + for (let i = 0; i < 2; i++) + this._Individuals.push(this._Individuals[2].Clone()); //全部突变 for (let p of this._Individuals) { diff --git a/src/Nest/Part.ts b/src/Nest/Part.ts index 97d9535f8..9d39f9024 100644 --- a/src/Nest/Part.ts +++ b/src/Nest/Part.ts @@ -1,33 +1,33 @@ import { Matrix4 } from "three"; -import { FixIndex } from "../Common/Utils"; import { Curve } from "../DatabaseServices/Entity/Curve"; import { AsVector3, equaln } from "../Geometry/GeUtils"; import { Container } from "./Container"; import { PartState } from "./PartState"; import { Path } from "./Path"; +import { PlaceType } from "./PlaceType"; import { Point } from "./Point"; import { RandomIndex } from "./Random"; /** * 零件类 * 零件类可以绑定数据,也存在位置和旋转状态的信息 - * + * * 初始化零件: * 传入零件的轮廓,刀半径,包围容器(或者为空?) * 初始化用于放置的轮廓。将轮廓首点移动到0点,记录移动的点P。 - * + * * 零件放置位置: * 表示零件轮廓首点的位置。 - * + * * 零件的旋转: * 表示零件轮廓按照首点(0)旋转。 - * + * * 还原零件的放置状态: * 同样将零件移动到0点 * 同样将零件旋转 * 同样将零件移动到指定的位置 * 零件可能处于容器中,变换到容器坐标系 - * + * */ export class Part { @@ -129,12 +129,9 @@ export class Part } //旋转起来,改变自身旋转状态(变异) - Rotate(): this + Mutate(): this { - let newIndex = RandomIndex(this.RotatedStates.length); - if (newIndex === this.StateIndex) - newIndex = FixIndex(this.StateIndex + 1, this.RotatedStates); - this.StateIndex = newIndex; + this.StateIndex = RandomIndex(this.RotatedStates.length, this.StateIndex); return this; } } diff --git a/src/Nest/Path.ts b/src/Nest/Path.ts index 4ef980353..347925619 100644 --- a/src/Nest/Path.ts +++ b/src/Nest/Path.ts @@ -15,7 +15,7 @@ import { Point } from "./Point"; */ export class Path implements ISerialize { - private _Points: Point[]; + _Points: Point[]; private OutsideNFPCache: Map = new Map(); private InsideNFPCache: Map = new Map(); @@ -99,13 +99,19 @@ export class Path implements ISerialize for (let np of nfp) { let { x, y } = np; + let isOut = false; for (let p of path._Points) { tp.set(p.x + x, p.y + y, 0); let dir = this.Check.GetPointAtCurveDir(tp); if (dir === 0) continue; if (dir === -1) return false; - else break; + else + { + if (path.Area < this.Area) return true; + isOut = true; + break; + } } for (let p of this._Points) @@ -114,7 +120,11 @@ export class Path implements ISerialize let dir = path.Check.GetPointAtCurveDir(tp); if (dir === 0) continue; if (dir === -1) return false; - else break; + else + { + if (isOut || this.Area < path.Area) return true; + break; + } } } } @@ -155,6 +165,14 @@ export class Path implements ISerialize else nfps = this.GetNFPs(path, true); this.OutsideNFPCache.set(path, 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[][] diff --git a/src/Nest/PlaceType.ts b/src/Nest/PlaceType.ts index 192245fa4..34f021ec7 100644 --- a/src/Nest/PlaceType.ts +++ b/src/Nest/PlaceType.ts @@ -1,10 +1,6 @@ - export enum PlaceType { - MinBox = 0, - MinX = 1, - MinY = 2, - MaxX = 3, - MaxY = 4, + Box = 0, + Hull = 1, } diff --git a/src/Nest/Random.ts b/src/Nest/Random.ts index 1d1d3fb11..4846cd548 100644 --- a/src/Nest/Random.ts +++ b/src/Nest/Random.ts @@ -1,8 +1,10 @@ +import { FixIndex } from "../Common/Utils"; -export function RandomIndex(count: number): number +export function RandomIndex(count: number, exclude?: number): number { let r = Math.random(); let index = Math.floor(r * count); - if (index === count) return 0; + if (index === count) index = 0; + if (index === exclude) index = FixIndex(index + 1, count); return index; } diff --git a/src/Nest/Shuffle.ts b/src/Nest/Shuffle.ts new file mode 100644 index 000000000..85bf91c9a --- /dev/null +++ b/src/Nest/Shuffle.ts @@ -0,0 +1,8 @@ +export function ShuffleArray(array: T[]) +{ + for (let i = array.length - 1; i > 0; i--) + { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} diff --git a/src/Nest/Test/TestContainer.ts b/src/Nest/Test/TestContainer.ts index 6710e7b78..e7cbb190e 100644 --- a/src/Nest/Test/TestContainer.ts +++ b/src/Nest/Test/TestContainer.ts @@ -16,13 +16,10 @@ export class TestContainer extends Container //------------先放置在孔洞内------------ if (this.Holes) { - for (let [, containers] of this.Holes) + for (let c of this.Holes) { - for (let c of containers) - { - if (c.PutPart(part)) - return true; - } + if (c.PutPart(part)) + return true; } } diff --git a/src/Nest/Test/TestHull.ts b/src/Nest/Test/TestHull.ts new file mode 100644 index 000000000..53c312e1e --- /dev/null +++ b/src/Nest/Test/TestHull.ts @@ -0,0 +1,24 @@ +import { app } from "../../ApplicationServices/Application"; +import { Point } from "../../DatabaseServices/Entity/Point"; +import { PromptStatus } from "../../Editor/PromptResult"; +import { ConvexHull2D } from "../ConvexHull2D"; +import { Path2Polyline } from "../Converter/Path2Polyline"; +import { HotCMD } from "../../Hot/HotCommand"; + +@HotCMD +export class Command_TestHull +{ + async exec() + { + let ssres = await app.Editor.GetSelection({ Filter: { filterTypes: [Point] } }); + if (ssres.Status !== PromptStatus.OK) return; + + let ptents = ssres.SelectSet.SelectEntityList as Point[]; + + let pts = ptents.map(p => p.Position); + let hull = ConvexHull2D(pts); + let pl = Path2Polyline(hull); + pl.CloseMark = true; + app.Database.ModelSpace.Append(pl); + } +}