This commit is contained in:
xiefan 2020-04-27 16:39:48 +08:00
parent 30ef9522a4
commit 80c7f6c3ca
40 changed files with 2776 additions and 1119 deletions

View File

@ -1,31 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint"
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
},
overrides: [
{
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
env: {
jest: true
}
}
]
};

View File

@ -1,29 +1,3 @@
# cut-demo
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### 依赖
- js-angusj-clipper
- monotone-convex-hull-2d

1067
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,27 +10,26 @@
},
"dependencies": {
"core-js": "^3.6.4",
"element-ui": "^2.13.1",
"iconv-lite": "^0.5.1",
"js-angusj-clipper": "^1.0.4",
"jszip": "^3.4.0",
"monotone-convex-hull-2d": "^1.0.1",
"vue": "^2.6.11"
},
"devDependencies": {
"@types/jest": "^24.0.19",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"@types/node": "^13.13.4",
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-eslint": "~4.3.0",
"@vue/cli-plugin-typescript": "~4.3.0",
"@vue/cli-plugin-unit-jest": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "1.0.0-beta.31",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"sass": "^1.26.3",
"sass-loader": "^8.0.2",
"typescript": "~3.8.3",
"vue-template-compiler": "^2.6.11"
"vue-template-compiler": "^2.6.11",
"worker-loader": "^2.0.0"
}
}

View File

@ -1,29 +1,64 @@
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
<select-file accept=".cfdat" button-type="primary" @change="loadCFData"
>导入CFDATA</select-file
>
<el-button @click="stopThread">停止优化</el-button>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import HelloWorld from "./components/HelloWorld.vue";
import SelectFile from "@/components/SelectFile.vue";
import * as fileHelper from "@/Business/FileHelper";
// import JSZip from "jszip";
// import iconv from "iconv-lite";
import { BlockConvert } from "./Business/BlockConvert";
import { LayoutEngine } from "./Business/LayoutEngine";
let engine: { stopThread: () => void };
export default Vue.extend({
name: "App",
components: {
HelloWorld
name: "app",
components: { SelectFile },
data() {
return {
activeGroupName: "config",
activeName: "0",
planOrder: null,
importCncText: "",
files: new Array<string>()
};
},
mounted() {
document.title = "开料调试工具 - " + document.title;
},
methods: {
/**读取文件 */
async loadCFData(files: FileList) {
const json = await fileHelper.readText(files[0]);
this.newLayout(json);
},
newLayout(planJson: string) {
const plan = JSON.parse(planJson);
console.log(plan);
const engine = new LayoutEngine();
const blockConvert = new BlockConvert();
const parts = [];
for (const block of plan["BlockList"]) {
parts.push(blockConvert.createPart(block, engine.board));
}
console.log(parts);
engine.init(parts);
// engine.run();
engine.startThread();
},
stopThread() {
engine.stopThread();
}
}
});
</script>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

View File

@ -0,0 +1,21 @@
import { Path } from '../Nest/Core/Path';
import { Part } from '../Nest/Core/Part';
export class BlockConvert{
/**
*
*/
constructor() {
// super();
}
createPart(data:any,board:Path){
let id = data['BlockID'];
let width = data['Width'];
let height = data['Length'];
let part = new Part();
part.Id = id;
part.Init(new Path([{ x: 0, y: 0 }, { x: width, y: 0 }, { x: width, y: height }, { x: 0, y: height }]),board);
part.UserData = data;
return part;
}
}

11
src/Business/BlockView.ts Normal file
View File

@ -0,0 +1,11 @@
export class BlockView{
/**
*
*/
constructor(canvas:HTMLCanvasElement) {
}
drawBlock(){
}
}

View File

@ -0,0 +1,9 @@
export async function readText(file:Blob,encoding?:string):Promise<string>{
return new Promise((resolve)=>{
let fileReader = new FileReader();
fileReader.onload=function(e){
resolve(<string>this.result);
}
fileReader.readAsText(file,encoding)
})
}

View File

@ -0,0 +1,5 @@
export class LayoutConfig{
loadLegacyConfig(){
}
}

View File

@ -0,0 +1,88 @@
import { Part } from '../Nest/Core/Part';
import { Path } from '../Nest/Core/Path';
import { DefaultBin } from '../Nest/Core/DefaultBin';
import { PathGeneratorSingle } from '../Nest/Core/PathGenerator';
import { NestCache } from '../Nest/Core/NestCache';
import { NestDatabase } from '../Nest/Core/NestDatabase';
import { NestFiler } from '../Nest/Common/Filer';
import { OptimizeMachine } from '../Nest/Core/OptimizeMachine';
import { Individual } from '../Nest/Core/Individual';
import Worker from "../Nest/Core/OptimizeWorker.worker";
import { InitClipperCpp } from '../Nest/Common/ClipperCpp';
export class LayoutEngine {
parts: Part[] = [];
board: Path;
threads: Worker[] = [];
/**
*
*/
constructor() {
//清理缓存,这个缓存是全局的
PathGeneratorSingle.Clear();
NestCache.Clear();
DefaultBin.InsideNFPCache = {};
//注册bin
let binPath = DefaultBin;
// binPath.Id = undefined; //清除这个缓存
PathGeneratorSingle.RegisterId(binPath);
this.board = binPath;
}
init(parts: Part[]) {
this.parts = parts;
}
async run() {
await InitClipperCpp();
let m = new OptimizeMachine;//优化器
m.Bin = this.board; //指定Bin容器
m.PutParts(this.parts);//把零件放上去
let count = 0;
m.callBack = async (i: Individual) => {
console.log('优化结果', i);
count++;
if(count>10){
m.Suspend();//关闭优化器
}
}//当有新结果时,回调
await m.Start();//启动这个
}
async startThread() {
let db = new NestDatabase();
db.Paths = PathGeneratorSingle.paths;
db.Parts = this.parts;
db.Bin = DefaultBin
let f = new NestFiler();
db.WriteFile(f);//写入到文件中
let best = Infinity;
let t = new Worker;
this.threads = [t];
t.onmessage = (e) => {
let f = new NestFiler(e.data);
let inv = new Individual(db.Parts);
inv.ReadFile(f);
if (best <= inv.Fitness)
return;
best = inv.Fitness;
let text = `优化率:${inv.Fitness}`;
console.log(text);
};
t.postMessage(f._datas);//Post给Worker
}
async stopThread() {
this.threads[0].terminate()
}
}

185
src/Common/ArrayExt.ts Normal file
View File

@ -0,0 +1,185 @@
/**
* ,
* @param {Array<any>} arr
* @param {*} el
*/
export function arrayRemove<T>(arr: Array<T>, el: T): Array<T>
{
let j = 0;
for (let i = 0, l = arr.length; i < l; i++)
{
if (arr[i] !== el)
{
arr[j++] = arr[i];
}
}
arr.length = j;
return arr;
}
export function arrayRemoveOnce<T>(arr: Array<T>, el: T): Array<T>
{
let index = arr.indexOf(el);
if (index !== -1)
arr.splice(index, 1);
return arr;
}
/**
*
* @param {(e: T) => boolean} checkFuntion
*/
export function arrayRemoveIf<T>(arr: Array<T>, checkFuntion: (e: T) => boolean): Array<T>
{
let j = 0;
for (let i = 0, l = arr.length; i < l; i++)
{
if (!checkFuntion(arr[i]))
{
arr[j++] = arr[i];
}
}
arr.length = j;
return arr;
}
export function arrayFirst<T>(arr: Array<T>): T
{
return arr[0];
}
export function arrayLast<T>(arr: { [key: number]: T, length: number; }): T
{
return arr[arr.length - 1];
}
/**
*
* @param {Array<T>} arr
* @returns {Array<T>}
*/
export function arraySortByNumber<T>(arr: Array<T>): Array<T>
{
arr.sort(sortNumberCompart);
return arr;
}
/**
*
* @param {(e1, e2) => boolean} [checkFuction]
* @returns {Array<T>}
*/
export function arrayRemoveDuplicateBySort<T>(arr: Array<T>, checkFuction: (e1: T, e2: T) => boolean = checkEqual): Array<T>
{
if (arr.length < 2) return arr;
let j = 1;
for (let i = 1, l = arr.length; i < l; i++)
if (!checkFuction(arr[j - 1], arr[i]))
arr[j++] = arr[i];
arr.length = j;
return arr;
}
/**
*
* @param {(e1, e2) => boolean} [checkFuction]
* @returns {Array<T>}
*/
export function arrayRemoveDuplicateBySort2<T>(arr: Array<T>, checkFuction: (e1: T, e2: T) => boolean = checkEqual): Array<T>
{
if (arr.length < 2) return arr;
let pre = arr[0],
newArr = [pre],
now: T;
for (let i = 1, len = arr.length; i < len; i++)
{
now = arr[i];
if (!checkFuction(pre, now))
{
newArr.push(now);
pre = now;
}
}
if (pre !== now) newArr.push(now);
return newArr;
}
//原地更新数组,注意这个函数并不会比map快.
export function arrayMap<T>(arr: Array<T>, mapFunc: (v: T) => T): Array<T>
{
for (let i = 0, count = arr.length; i < count; i++)
arr[i] = mapFunc(arr[i]);
return arr;
}
function sortNumberCompart(e1: any, e2: any)
{
return e1 - e2;
}
function checkEqual(e1: any, e2: any): boolean
{
return e1 === e2;
}
/**
*
* @param arr
* @param index //将index位置以后的值放到起始位置
*/
export function changeArrayStartIndex<T>(arr: T[], index: number): T[]
{
arr.unshift(...arr.splice(index));
return arr;
}
export function equalArray<T>(a: T[], b: T[], checkF = checkEqual)
{
if (a === b) return true;
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i)
if (!checkF(a[i], b[i])) return false;
return true;
}
export function arrayClone<T>(arr: T[]): T[]
{
return arr.slice();
}
//https://jsperf.com/merge-array-implementations/30
export function arrayPushArray<T>(arr1: T[], arr2: T[]): T[]
{
let arr1Length = arr1.length;
let arr2Length = arr2.length;
arr1.length = arr1Length + arr2Length;
for (let i = 0; i < arr2Length; i++)
arr1[arr1Length + i] = arr2[i];
return arr1;
}
export function arraySum(arr: number[])
{
let sum = 0;
for (let n of arr) sum += n;
return sum;
}
export function FilterSet<T>(s: Set<T>, fn: (el: T) => boolean): Set<T>
{
let ns = new Set<T>();
for (let el of s)
{
if (fn(el))
ns.add(el);
}
return ns;
}

7
src/Common/Sleep.ts Normal file
View File

@ -0,0 +1,7 @@
export async function Sleep(time: number)
{
return new Promise(res =>
{
setTimeout(res, time);
});
}

160
src/Nest/Common/Box2.ts Normal file
View File

@ -0,0 +1,160 @@
import { Vector2 } from "./Vector2";
import { Point } from "./Point";
export class Box2
{
min: Vector2;
max: Vector2;
constructor(min = new Vector2(+ Infinity, + Infinity), max = new Vector2(- Infinity, - Infinity))
{
this.min = min;
this.max = max;
}
get area(): number
{
return (this.max.x - this.min.x) * (this.max.y - this.min.y);
}
set(min: Vector2, max: Vector2): Box2
{
this.min.copy(min);
this.max.copy(max);
return this;
}
setFromPoints(points: Vector2[]): Box2
{
this.makeEmpty();
for (let i = 0, il = points.length; i < il; i++)
{
this.expandByPoint(points[i]);
}
return this;
}
private static _setFromCenterAndSize_v1 = new Vector2();
setFromCenterAndSize(center: Vector2, size: Vector2): Box2
{
const v1 = Box2._setFromCenterAndSize_v1;
const halfSize = v1.copy(size).multiplyScalar(0.5);
this.min.copy(center).sub(halfSize);
this.max.copy(center).add(halfSize);
return this;
}
clone(): Box2
{
return new (this.constructor as any)().copy(this);
}
copy(box: Box2): Box2
{
this.min.copy(box.min);
this.max.copy(box.max);
return this;
}
makeEmpty(): Box2
{
this.min.x = this.min.y = + Infinity;
this.max.x = this.max.y = - Infinity;
return this;
}
isEmpty(): boolean
{
// this is a more robust check for empty than (volume <= 0) because volume can get positive with two negative axes
return (this.max.x < this.min.x) || (this.max.y < this.min.y);
}
getCenter(result: Vector2 = new Vector2()): Vector2
{
return this.isEmpty() ? result.set(0, 0) : result.addVectors(this.min, this.max).multiplyScalar(0.5);
}
getSize(result: Vector2 = new Vector2()): Vector2
{
return this.isEmpty() ? result.set(0, 0) : result.subVectors(this.max, this.min);
}
expandByPoint(point: Vector2): Box2
{
this.min.min(point);
this.max.max(point);
return this;
}
expandByVector(vector: Vector2): Box2
{
this.min.sub(vector);
this.max.add(vector);
return this;
}
expandByScalar(scalar: number): Box2
{
this.min.addScalar(- scalar);
this.max.addScalar(scalar);
return this;
}
containsPoint(point: Vector2): boolean
{
if (point.x < this.min.x || point.x > this.max.x ||
point.y < this.min.y || point.y > this.max.y)
{
return false;
}
return true;
}
containsBox(box: Box2): boolean
{
if ((this.min.x <= box.min.x) && (box.max.x <= this.max.x) &&
(this.min.y <= box.min.y) && (box.max.y <= this.max.y))
{
return true;
}
return false;
}
getParameter(point: Vector2, result: Vector2 = new Vector2()): Vector2
{
// This can potentially have a divide by zero if the box
// has a size dimension of 0.
return result.set(
(point.x - this.min.x) / (this.max.x - this.min.x),
(point.y - this.min.y) / (this.max.y - this.min.y)
);
}
intersectsBox(box: Box2): boolean
{
// using 6 splitting planes to rule out intersections.
if (box.max.x < this.min.x || box.min.x > this.max.x ||
box.max.y < this.min.y || box.min.y > this.max.y)
{
return false;
}
return true;
}
clampPoint(point: Vector2, result: Vector2 = new Vector2()): Vector2
{
return result.copy(point).clamp(this.min, this.max);
}
private static _distanceToPoint_v1 = new Vector2();
distanceToPoint(point: Vector2): number
{
const v1 = Box2._distanceToPoint_v1;
const clampedPoint = v1.copy(point).clamp(this.min, this.max);
return clampedPoint.sub(point).length();
}
intersect(box: Box2): Box2
{
this.min.max(box.min);
this.max.min(box.max);
return this;
}
union(box: Box2): Box2
{
this.min.min(box.min);
this.max.max(box.max);
return this;
}
translate(offset: Point): Box2
{
this.min.add(offset);
this.max.add(offset);
return this;
}
equals(box: Box2): boolean
{
return box.min.equals(this.min) && box.max.equals(this.max);
}
};

View File

@ -0,0 +1,21 @@
import * as clipperLib from "js-angusj-clipper/web"; // nodejs style require
export let clipperCpp: { lib?: clipperLib.ClipperLibWrapper; } = {};
export function InitClipperCpp(): Promise<void>
{
if (clipperCpp.lib) return;
if (!globalThis.document)
globalThis.document = {} as any;
return new Promise((res, rej) =>
{
clipperLib.loadNativeClipperLibInstanceAsync(
// let it autodetect which one to use, but also available WasmOnly and AsmJsOnly
clipperLib.NativeClipperLibRequestedFormat.WasmWithAsmJsFallback
).then(c =>
{
clipperCpp.lib = c;
res();
console.log("载入成功!");
});
});
}

View File

@ -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: number[] = convexHull(pts);
return indexs.map(i => points[i]);
}

30
src/Nest/Common/Filer.ts Normal file
View File

@ -0,0 +1,30 @@
/**
* CAD文件数据
*/
export class NestFiler
{
private readIndex: number = 0;
constructor(public _datas: any[] = []) { }
Clear()
{
this._datas.length = 0;
return this.Reset();
}
Reset()
{
this.readIndex = 0;
return this;
}
Write(data: any)
{
this._datas.push(data);
return this;
}
Read(): any
{
return this._datas[this.readIndex++];
}
}

6
src/Nest/Common/Point.ts Normal file
View File

@ -0,0 +1,6 @@
export interface Point
{
x: number;
y: number;
}

View File

@ -0,0 +1,9 @@
import { FixIndex } from "./Util";
export function RandomIndex(count: number, exclude?: number): number
{
let index = Math.floor(Math.random() * count);
if (index === count) index = 0;
if (index === exclude) index = FixIndex(index + 1, count);
return index;
}

View File

@ -0,0 +1,8 @@
export function ShuffleArray<T = any>(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]];
}
}

35
src/Nest/Common/Util.ts Normal file
View File

@ -0,0 +1,35 @@
export function equaln(v1: number, v2: number, fuzz = 1e-5)
{
return Math.abs(v1 - v2) <= fuzz;
}
export function FixIndex(index: number, arr: Array<any> | number)
{
let count = (arr instanceof Array) ? arr.length : arr;
if (index < 0)
return count + index;
else if (index >= count)
return index - count;
else
return index;
}
/**
* @param compart t2大于t1那么返回t2
* @returns
*/
export function Max<T>(arr: T[], compart: (t1: T, t2: T) => boolean): number
{
let best: T = arr[0];
let bestIndex = 0;
for (let i = 1; i < arr.length; i++)
{
let t1 = arr[i];
if (compart(best, t1))
{
best = t1;
bestIndex = i;
}
}
return bestIndex;
}

287
src/Nest/Common/Vector2.ts Normal file
View File

@ -0,0 +1,287 @@
import { Point } from "./Point";
export class Vector2
{
x: number;
y: number;
readonly isVector2: boolean = true;
constructor(x: number = 0, y: number = 0)
{
this.x = x;
this.y = y;
}
get width(): number { return this.x; }
set width(value: number) { this.x = value; }
get height(): number { return this.y; }
set height(value: number) { this.y = value; }
set(x: number, y: number): Vector2
{
this.x = x;
this.y = y;
return this;
}
setScalar(scalar: number): Vector2
{
this.x = scalar;
this.y = scalar;
return this;
}
setX(x: number): Vector2
{
this.x = x;
return this;
}
setY(y: number): Vector2
{
this.y = y;
return this;
}
setComponent(index: number, value: number): Vector2
{
switch (index)
{
case 0: this.x = value; break;
case 1: this.y = value; break;
default: throw new Error('index is out of range: ' + index);
}
return this;
}
getComponent(index: number): number
{
switch (index)
{
case 0: return this.x;
case 1: return this.y;
default: throw new Error('index is out of range: ' + index);
}
}
clone(): Vector2
{
return new (this.constructor as any)().copy(this);
}
copy(v: Vector2): Vector2
{
this.x = v.x;
this.y = v.y;
return this;
}
add(v: Point): Vector2
{
this.x += v.x;
this.y += v.y;
return this;
}
addScalar(s: number): Vector2
{
this.x += s;
this.y += s;
return this;
}
addVectors(a: Vector2, b: Vector2): Vector2
{
this.x = a.x + b.x;
this.y = a.y + b.y;
return this;
}
addScaledVector(v: Vector2, s: number): Vector2
{
this.x += v.x * s;
this.y += v.y * s;
return this;
}
sub(v: Vector2): Vector2
{
this.x -= v.x;
this.y -= v.y;
return this;
}
subScalar(s: number): Vector2
{
this.x -= s;
this.y -= s;
return this;
}
subVectors(a: Vector2, b: Vector2): Vector2
{
this.x = a.x - b.x;
this.y = a.y - b.y;
return this;
}
multiply(v: Vector2): Vector2
{
this.x *= v.x;
this.y *= v.y;
return this;
}
multiplyScalar(scalar: number): Vector2
{
if (isFinite(scalar))
{
this.x *= scalar;
this.y *= scalar;
} else
{
this.x = 0;
this.y = 0;
}
return this;
}
divide(v: Vector2): Vector2
{
this.x /= v.x;
this.y /= v.y;
return this;
}
divideScalar(scalar: number): Vector2
{
return this.multiplyScalar(1 / scalar);
}
min(v: Vector2): Vector2
{
this.x = Math.min(this.x, v.x);
this.y = Math.min(this.y, v.y);
return this;
}
max(v: Vector2): Vector2
{
this.x = Math.max(this.x, v.x);
this.y = Math.max(this.y, v.y);
return this;
}
clamp(min: Vector2, max: Vector2): Vector2
{
// This function assumes min < max, if this assumption isn't true it will not operate correctly
this.x = Math.max(min.x, Math.min(max.x, this.x));
this.y = Math.max(min.y, Math.min(max.y, this.y));
return this;
}
private static clampScalar_min = new Vector2();
private static clampScalar_max = new Vector2();
clampScalar(minVal: number, maxVal: number): Vector2
{
const min: Vector2 = Vector2.clampScalar_min.set(minVal, minVal);
const max: Vector2 = Vector2.clampScalar_max.set(maxVal, maxVal);
return this.clamp(min, max);
}
clampLength(min: number, max: number): Vector2
{
const length: number = this.length();
return this.multiplyScalar(Math.max(min, Math.min(max, length)) / length);
}
floor(): Vector2
{
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
return this;
}
ceil(): Vector2
{
this.x = Math.ceil(this.x);
this.y = Math.ceil(this.y);
return this;
}
round(): Vector2
{
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
}
roundToZero(): Vector2
{
this.x = (this.x < 0) ? Math.ceil(this.x) : Math.floor(this.x);
this.y = (this.y < 0) ? Math.ceil(this.y) : Math.floor(this.y);
return this;
}
negate(): Vector2
{
this.x = - this.x;
this.y = - this.y;
return this;
}
dot(v: Vector2): number
{
return this.x * v.x + this.y * v.y;
}
lengthSq(): number
{
return this.x * this.x + this.y * this.y;
}
length(): number
{
return Math.sqrt(this.x * this.x + this.y * this.y);
}
lengthManhattan(): number
{
return Math.abs(this.x) + Math.abs(this.y);
}
normalize(): Vector2
{
return this.divideScalar(this.length());
}
angle(): number
{
// computes the angle in radians with respect to the positive x-axis
let angle: number = Math.atan2(this.y, this.x);
if (angle < 0) angle += 2 * Math.PI;
return angle;
}
distanceTo(v: Vector2): number
{
return Math.sqrt(this.distanceToSquared(v));
}
distanceToSquared(v: Vector2): number
{
const dx: number = this.x - v.x, dy: number = this.y - v.y;
return dx * dx + dy * dy;
}
distanceToManhattan(v: Vector2): number
{
return Math.abs(this.x - v.x) + Math.abs(this.y - v.y);
}
setLength(length: number): Vector2
{
return this.multiplyScalar(length / this.length());
}
lerp(v: Vector2, alpha: number): Vector2
{
this.x += (v.x - this.x) * alpha;
this.y += (v.y - this.y) * alpha;
return this;
}
lerpVectors(v1: Vector2, v2: Vector2, alpha: number): Vector2
{
return this.subVectors(v2, v1).multiplyScalar(alpha).add(v1);
}
equals(v: Vector2): boolean
{
return ((v.x === this.x) && (v.y === this.y));
}
fromArray(array: Float32Array | number[], offset: number = 0): Vector2
{
this.x = array[offset];
this.y = array[offset + 1];
return this;
}
toArray(array: Float32Array | number[] = [], offset: number = 0): Float32Array | number[]
{
array[offset] = this.x;
array[offset + 1] = this.y;
return array;
}
fromAttribute(attribute: any, index: number, offset: number = 0): Vector2
{
index = index * attribute.itemSize + offset;
this.x = attribute.array[index];
this.y = attribute.array[index + 1];
return this;
}
rotateAround(center: Vector2, angle: number): Vector2
{
const c: number = Math.cos(angle), s: number = Math.sin(angle);
const x: number = this.x - center.x;
const y: number = this.y - center.y;
this.x = x * c - y * s + center.x;
this.y = x * s + y * c + center.y;
return this;
}
}

373
src/Nest/Core/Container.ts Normal file
View File

@ -0,0 +1,373 @@
import { ClipType, PolyFillType, Paths } from "js-angusj-clipper/web";
import { SubjectInput } from "js-angusj-clipper/web/clipFunctions";
import { Box2 } from "../Common/Box2";
import { clipperCpp } from "../Common/ClipperCpp";
import { ConvexHull2D } from "../Common/ConvexHull2D";
import { NestFiler } from "../Common/Filer";
import { NestCache } from "./NestCache";
import { Part } from "./Part";
import { Area, Path, TranslatePath } from "./Path";
import { PlaceType } from "./PlaceType";
import { Point } from "../Common/Point";
import { equaln } from "../Common/Util";
import { Vector2 } from "../Common/Vector2";
/**
* ,,
*/
interface PartPlacedContainerState
{
p: Point;
area?: number;
hull?: Point[];
box?: Box2;
}
/**
* ,
*
*
*
*/
export class Container
{
ParentId: number = -1;//父亲id,-1表示默认的bin,-2开始表示余料,大于等于0表示板件
ChildrenIndex: number = 0;//网洞的索引位置
ParentM: Point;
Placed: Part[] = [];
PlaceType: PlaceType = PlaceType.Box;
//放置状态
PlacedArea = 0;
PlacedBox: Box2;
PlacedHull: Point[];
StatusKey: string;
constructor(protected BinPath?: Path)
{
if (BinPath)
this.StatusKey = this.BinPath.Id.toString() + "," + this.PlaceType;
}
get UseRatio(): number
{
return this.PlacedBox.area / this.BinPath.Area;
}
private _NotPuts: Set<number>[] = [];//已经无法放置的pathId
private PrePut(part: Part): PartPlacedContainerState
{
//无法容纳
if (this.BinPath.Area - this.PlacedArea < part.State.Contour.Area)
return;
let cacheKey = this.StatusKey + "," + part.State.Contour.Id;
let cacheP = NestCache.PositionCache[cacheKey];
//读取缓存位置
if (cacheP)
{
NestCache.count1++;
return this.Calc(part, cacheP);
}
let binNfp = this.BinPath.GetInsideNFP(part.State.Contour);
if (!binNfp || binNfp.length === 0) return;
//首个
if (this.Placed.length === 0)
{
let p = this.GetFarLeftP(binNfp);
NestCache.PositionCache[cacheKey] = p;
return this.Calc(part, p);
}
//快速退出
let noSet = NestCache.NoPutCache[this.StatusKey];
if (noSet) this._NotPuts.push(noSet);
for (let set of this._NotPuts)
{
if (set.has(part.State.Contour.Id))
{
NestCache.count2++;
return;
}
}
let finalNfp = this.GetNFPs(part, binNfp);
if (!finalNfp || finalNfp.length === 0)
{
if (noSet) noSet.add(part.State.Contour.Id);
else
{
noSet = new Set([part.State.Contour.Id]);
NestCache.NoPutCache[this.StatusKey] = noSet;
}
return;
}
//选择合适的放置点
let minArea: number = Infinity;
let translate: Point;
let bestBox: Box2;
let bestHull: Point[];
let tempVec = new Vector2;
for (let nfp of finalNfp)
{
for (let p of nfp)
{
tempVec.set(p.x * 1e-4, p.y * 1e-4);
switch (this.PlaceType)
{
case PlaceType.Box:
{
let box2 = part.State.Contour.BoundingBox.clone();
box2.translate(tempVec);
let rectBox = this.PlacedBox.clone().union(box2);
let size = rectBox.getSize();
let area = size.x * size.y;
if (area < minArea ||
((equaln(area, minArea, 1))
&& (p.x < translate.x || (p.x === translate.x && p.y < translate.y))))
// && (p.y < translate.y || (p.y === translate.y && p.x < translate.x))))
// && (this.BinPath.Size.x > this.BinPath.Size.y ? p.x < translate.x : p.y < translate.y)))
{
translate = p;
minArea = area;
bestBox = rectBox;
}
break;
}
case PlaceType.Hull:
{
let pts = TranslatePath(part.State.Contour.Points, tempVec);
let nhull = ConvexHull2D([...pts, ...this.PlacedHull]);
let area = Math.abs(Area(nhull));
if (area < minArea ||
((equaln(area, minArea, 1))
&& (p.x < translate.x || (p.x === translate.x && p.y < translate.y))))
// && (p.y < translate.y || (p.y === translate.y && p.x < translate.x))))
// && (this.BinPath.Size.x > this.BinPath.Size.y ? p.x < translate.x : p.y < translate.y)))
{
translate = p;
minArea = area;
bestHull = nhull;
}
break;
}
case PlaceType.Gravity:
{
if (!translate || p.x < translate.x || (p.x === translate.x && p.y < translate.y))
translate = p;
}
default:
break;
}
}
}
if (translate)
{
NestCache.PositionCache[cacheKey] = translate;
if (!bestBox)
bestBox = this.PlacedBox.clone().union(part.State.Contour.BoundingBox.clone().translate({ x: translate.x * 1e-4, y: translate.y * 1e-4 }));
return { p: translate, area: minArea, box: bestBox, hull: bestHull };
}
}
private Calc(part: Part, p: Point): PartPlacedContainerState
{
let d: PartPlacedContainerState = { p };
let m: Point = { x: p.x * 1e-4, y: p.y * 1e-4 };
if (this.PlacedBox)
d.box = this.PlacedBox.clone().union(part.State.Contour.BoundingBox.clone().translate(m));
else
d.box = part.State.Contour.BoundingBox.clone().translate(m);
//凸包
if (this.PlaceType === PlaceType.Hull)
{
if (!this.PlacedHull)
{
d.hull = TranslatePath(part.State.Contour.Points, m);
d.area = part.State.Contour.Area;
}
else
{
d.hull = ConvexHull2D([...this.PlacedHull, ...TranslatePath(part.State.Contour.Points, m)]);
d.area = Area(d.hull);
}
d.area = Math.abs(d.area);
}
else
{
d.area = d.box.area;
}
return d;
}
PutPart(p: Part, greedy = false): boolean
{
let bestD: PartPlacedContainerState;
if (greedy)
{
let bestI: number;
for (let i = 0; i < p.RotatedStates.length; i++)
{
p.StateIndex = i;
let d = this.PrePut(p);
if (d && (!bestD || bestD.area > d.area ||
(
this.PlaceType === PlaceType.Hull
&& equaln(bestD.area, d.area, 0.1)
&& d.box.area < bestD.box.area
)
))
{
bestD = d;
bestI = i;
}
}
if (bestD)
p.StateIndex = bestI;
}
else
bestD = this.PrePut(p);
if (bestD)
{
p.PlacePosition = bestD.p;
this.PlacedBox = bestD.box ?? this.PlacedBox;
this.PlacedHull = bestD.hull;
this.AppendPart(p, false);
return true;
}
return false;
}
protected GetNFPs(part: Part, binNfp: Point[][]): Paths
{
//合并(零件和所有已经放置零件的NFP)
let nfps: SubjectInput[] = [];
for (let placedPart of this.Placed)
{
let nfp = placedPart.State.Contour.GetOutsideNFP(part.State.Contour);
if (!nfp) return;
for (let n of nfp)
{
let nnfp = TranslatePath(n, placedPart.PlacePosition);
nfps.push({ data: nnfp, closed: true });
}
}
//合并nfp
let combinedNfp = clipperCpp.lib.clipToPaths({
subjectInputs: nfps,
clipType: ClipType.Union,
subjectFillType: PolyFillType.NonZero
});
combinedNfp = clipperCpp.lib.cleanPolygons(combinedNfp, 100);
if (combinedNfp.length === 0) return;
//减去nfp
let finalNfp = clipperCpp.lib.clipToPaths({
subjectInputs: [{ data: binNfp, closed: true }],
clipInputs: [{ data: combinedNfp }],
clipType: ClipType.Difference,
subjectFillType: PolyFillType.NonZero
});
finalNfp = clipperCpp.lib.cleanPolygons(finalNfp, 100);
return finalNfp;
}
/**
* Part添加的Placed列表
* @param part ,
* @param [calc=true] ?
*/
private AppendPart(part: Part, calc = true): void
{
this.StatusKey += "," + part.State.Contour.Id;
this.Placed.push(part);
this.PlacedArea += part.State.Contour.Area;
let m = { x: part.PlacePosition.x * 1e-4, y: part.PlacePosition.y * 1e-4 };
if (calc)
{
//凸包
if (this.PlaceType === PlaceType.Hull)
{
if (!this.PlacedHull)
this.PlacedHull = TranslatePath(part.State.Contour.Points, m);
else
this.PlacedHull = ConvexHull2D([...this.PlacedHull, ...TranslatePath(part.State.Contour.Points, m)]);
}
}
if (calc || this.PlaceType !== PlaceType.Box)
{
if (this.PlacedBox)
this.PlacedBox.union(part.State.Contour.BoundingBox.clone().translate(m));
else
this.PlacedBox = part.State.Contour.BoundingBox.clone().translate(m);
}
}
/**
*
*/
protected GetFarLeftP(nfp: Point[][]): Point
{
let leftP: Point = { x: Infinity, y: Infinity };
for (let path of nfp)
{
for (let p of path)
{
if (p.x < leftP.x)
leftP = p;
else if (p.x === leftP.x && p.y < leftP.y)
leftP = p;
}
}
return leftP;
}
//#region -------------------------File-------------------------
//对象从文件中读取数据,初始化自身
ReadFile(file: NestFiler, parts: Part[])
{
this.ParentId = file.Read();
this.ChildrenIndex = file.Read();
this.ParentM = file.Read();
let count = file.Read() as number;
this.Placed = [];
for (let i = 0; i < count; i++)
{
let index = file.Read() as number;
let part = parts[index];
part.StateIndex = file.Read();
part.PlacePosition = file.Read();
this.Placed.push(part);
}
return this;
}
//对象将自身数据写入到文件.
WriteFile(file: NestFiler)
{
file.Write(this.ParentId);
file.Write(this.ChildrenIndex);
file.Write(this.ParentM);
file.Write(this.Placed.length);
for (let p of this.Placed)
{
file.Write(p.Id);
file.Write(p.StateIndex);
file.Write(p.PlacePosition);
}
}
//#endregion
}

View File

@ -0,0 +1,4 @@
import { Path } from "./Path";
let width = 1221;
let height = 2441;
export let DefaultBin = new Path([{ x: 0, y: 0 }, { x: width, y: 0 }, { x: width, y: height }, { x: 0, y: height }]);

190
src/Nest/Core/Individual.ts Normal file
View File

@ -0,0 +1,190 @@
import { arrayLast } from "../../Common/ArrayExt";
import { Container } from "./Container";
import { NestFiler } from "../Common/Filer";
import { Part } from "./Part";
import { Path } from "./Path";
import { PlaceType } from "./PlaceType";
import { RandomIndex } from "../Common/Random";
/**
* (,)
*
*
*
*
*
*
* ()
*
*/
export class Individual
{
//强壮程度 越低越好
Fitness: number;
constructor(public Parts?: Part[], public mutationRate = 0.5) { }
Clone()
{
let p = new Individual(this.Parts.map(p => p.Clone(), this.mutationRate));
p.Mutate();
return p;
}
/**
*
*/
Mutate()
{
if (this.mutationRate > 0.5)
for (let i = 0; i < this.Parts.length; i++)
{
let rand = Math.random();
if (rand < this.mutationRate * 0.5)
{
//和下一个调换顺序
let j = i + 1;
if (j < this.Parts.length)
[this.Parts[i], this.Parts[j]] = [this.Parts[j], this.Parts[i]];
}
if (rand < this.mutationRate)
this.Parts[i].Mutate();
}
else
{
//洗牌
let rand = Math.random();
if (rand < 0.5)
{
let index = RandomIndex(this.Parts.length - 2);
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);
this.Parts[index].Mutate();
}
else
for (let i = 0; i < this.Parts.length; i++)
{
let rand = Math.random();
if (rand < this.mutationRate)
{
this.Parts[i].Mutate();
i += 5;
}
}
}
if (this.mutationRate > 0.2)
this.mutationRate -= 0.004;
return this;
}
Containers: Container[];
HoleContainers: Container[];
/**
*
*/
Evaluate(bin: Path, bestCount: number, greedy = false, type?: PlaceType)
{
let parts = this.Parts;
this.Containers = [];
this.HoleContainers = [];
for (let part of parts)
{
for (let i = 0; i < part.Holes.length; i++)
{
let hole = part.Holes[i];
let container = new Container(hole.Contour);
container.ParentId = part.Id;
container.ChildrenIndex = i;
container.ParentM = hole.OrigionMinPoint;
this.HoleContainers.push(container);
}
}
//网洞优先
parts = parts.filter(p =>
{
for (let hole of this.HoleContainers)
{
if (hole.PutPart(p))
return false;
}
return true;
});
while (parts.length > 0)
{
if (this.Containers.length > bestCount)
{
this.Fitness = Math.ceil(bestCount) + 1;
return;
}
let container = new Container(bin);
container.ParentId = -1;
if (greedy)
container.PlaceType = type;
else
container.PlaceType = RandomIndex(2);
if (this.mutationRate > 0.5)
{
let area = 0;
let maxP: Part;
for (let i = 0; i < parts.length; i++)
{
let p = parts[i];
if (p.State.Contour.Area > area)
{
maxP = parts[i];
area = p.State.Contour.Area;
}
}
if (container.PutPart(maxP, greedy))
parts = parts.filter(p => p !== maxP);
}
parts = parts.filter(p =>
{
return container.PutPart(p, greedy) !== true;
});
if (!greedy)
{
parts = parts.filter(p =>
{
return container.PutPart(p, true) !== true;
});
}
this.Containers.push(container);
}
this.Fitness = this.Containers.length - 1 + arrayLast(this.Containers).UseRatio;
}
//#region -------------------------File-------------------------
//对象从文件中读取数据,初始化自身
ReadFile(file: NestFiler)
{
this.Fitness = file.Read();
let count = file.Read() as number;
this.Containers = [];
for (let i = 0; i < count; i++)
this.Containers.push(new Container().ReadFile(file, this.Parts));
count = file.Read();
this.HoleContainers = [];
for (let i = 0; i < count; i++)
this.HoleContainers.push(new Container().ReadFile(file, this.Parts));
}
//对象将自身数据写入到文件.
WriteFile(f: NestFiler)
{
f.Write(this.Fitness);
f.Write(this.Containers.length);
for (let c of this.Containers)
c.WriteFile(f);
f.Write(this.HoleContainers.length);
for (let c of this.HoleContainers)
c.WriteFile(f);
}
//#endregion
}

View File

@ -0,0 +1,37 @@
import { Path } from "./Path";
import { Point } from "../Common/Point";
export class NestCache
{
static count1 = 0;
static count2 = 0;//noset
static PositionCache: { [key: string]: Point; } = {};
static NoPutCache: { [key: string]: Set<number>; } = {};
private static CacheRect = new Map<string, Path>();
/**
* 0
*/
static CreatePath(x: number, y: number, knifRadius = 3.5): Path
{
let minX = -knifRadius;
let maxX = x + knifRadius;
let minY = -knifRadius;
let maxY = y + knifRadius;
return new Path([
{ x: minX, y: minY },
{ x: maxX, y: minY },
{ x: maxX, y: maxY },
{ x: minX, y: maxY },
]);
}
static Clear()
{
this.count1 = 0;
this.count2 = 0;
this.CacheRect.clear();
this.PositionCache = {};
}
}

View File

@ -0,0 +1,54 @@
import { Part } from "./Part";
import { Path } from "./Path";
import { NestFiler } from "../Common/Filer";
import { PathGeneratorSingle } from "./PathGenerator";
/**
* ,
* Work间传输
*/
export class NestDatabase
{
Paths: Path[]; //所有的Path都在这里
Bin: Path; //默认的容器
Parts: Part[]; //所有的零件
//#region -------------------------File-------------------------
//对象从文件中读取数据,初始化自身
ReadFile(file: NestFiler)
{
file.Read();
let count = file.Read() as number;
this.Paths = [];
for (let i = 0; i < count; i++)
{
let path = new Path();
path.ReadFile(file);
this.Paths.push(path);
}
this.Bin = this.Paths[file.Read()];
PathGeneratorSingle.paths = this.Paths;
count = file.Read();
this.Parts = [];
for (let i = 0; i < count; i++)
{
let part = new Part();
part.ReadFile(file);
this.Parts.push(part);
}
}
//对象将自身数据写入到文件.
WriteFile(file: NestFiler)
{
file.Write(1);
file.Write(this.Paths.length);
for (let path of this.Paths)
path.WriteFile(file);
file.Write(this.Bin.Id);
file.Write(this.Parts.length);
for (let part of this.Parts)
part.WriteFile(file);
}
//#endregion
}

View File

@ -0,0 +1,160 @@
import { arrayRemoveIf } from "../../Common/ArrayExt";
import { Sleep } from "../../Common/Sleep";
import { clipperCpp } from "../Common/ClipperCpp";
import { Individual } from "./Individual";
import { NestCache } from "./NestCache";
import { Part } from "./Part";
import { Path } from "./Path";
/**
*
*
*
*
*
*
*/
export class OptimizeMachine
{
//配置
Config: {
PopulationCount: number;//种群个数
};
//容器板
Bin: Path;
//机台上的零件
Parts: Part[];
private _IsSuspend = false;
protected _Individuals: Individual[];
constructor()
{
this.Config = { PopulationCount: 50 };
}
//放入零件
PutParts(parts: Part[])
{
if (globalThis.document) parts = parts.slice();
arrayRemoveIf(parts, p => p.RotatedStates.length === 0);
this.Parts = parts;
}
callBack: (i: Individual) => Promise<void>;
//启动
async Start()
{
if (this.Parts.length === 0) return;
console.log(this.Parts.length);
this._IsSuspend = false;
NestCache.Clear();
this.Parts.sort((p1, p2) => p2.State.Contour.Area - p1.State.Contour.Area);
this._Individuals = [new Individual(this.Parts, 0.8)];
for (let i = 1; i < this.Config.PopulationCount; i++)
{
let parts = this.Parts.map(p => p.Clone());
if (i < 3)
{
for (let i = parts.length - 1; i > 0; i--)
{
const j = Math.floor(Math.random() * (i + 1));
[parts[i], parts[j]] = [parts[j], parts[i]];
parts[i].Mutate();
}
}
this._Individuals.push(new Individual(parts, 0.8));
}
//2.执行
await this.Run();
}
calcCount = 0;
best = Infinity;
bestP: Individual;
bestCount = 0;
private async Run()
{
console.time("1");
if (this.Parts.length === 0) return;
//开始自然选择
while (!this._IsSuspend) //实验停止信号
{
let goBack = this.calcCount - this.bestCount > 8000;
//1.适应环境(放置零件)
for (let i = 0; i < this._Individuals.length; i++)
{
if (globalThis.document || !clipperCpp.lib)
await Sleep(0);
let p = this._Individuals[i];
if (this.calcCount < 1000 || this.calcCount % 1000 === 0)
p.Evaluate(this.Bin, this.best, true, i % 2);
else
p.Evaluate(this.Bin, this.best);
if (!this.bestP || p.Fitness < this.bestP.Fitness)
{
this.bestP = p;
this.best = p.Fitness;
await this.callBack(p);
}
if (goBack)
p.mutationRate = 0.5;
}
this.calcCount += this._Individuals.length;
if (this.calcCount % 100 === 0)
{
console.timeLog("1", this.bestP.Fitness, this.calcCount, NestCache.count1, NestCache.count2);
await Sleep(0);
}
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)
// await this.callBack(bestP);
// }
//自然选择
this._Individuals.splice(-10);//杀死它
for (let i = 0; i < 4; i++)
this._Individuals.push(bestP.Clone());
this._Individuals.push(this.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)
{
if (p.Fitness !== undefined)
p.Mutate();
}
}
}
//暂停
Suspend()
{
console.timeEnd("1");
console.log(this.best, this.calcCount, NestCache.count1);
this._IsSuspend = true;
console.log("暂停");
}
//继续
Continue()
{
this._IsSuspend = false;
this.Run();
}
//清理机台
Clear()
{
}
}

View File

@ -0,0 +1,27 @@
const ctx: Worker = self as any;
import { InitClipperCpp } from "../Common/ClipperCpp";
import { NestFiler } from "../Common/Filer";
import { NestDatabase } from "./NestDatabase";
import { OptimizeMachine } from "./OptimizeMachine";
ctx.addEventListener("message", async (event) =>
{
await InitClipperCpp();
let f = new NestFiler(event.data);
let db = new NestDatabase;
db.ReadFile(f);
let m = new OptimizeMachine;
m.Bin = db.Bin;
m.PutParts(db.Parts);
m.callBack = async (inv) =>
{
let f = new NestFiler();
inv.WriteFile(f);
ctx.postMessage(f._datas);
};
await m.Start();
});
export default {} as typeof Worker & (new () => Worker);

178
src/Nest/Core/Part.ts Normal file
View File

@ -0,0 +1,178 @@
import { NestFiler } from "../Common/Filer";
import { PartState } from "./PartState";
import { Path } from "./Path";
import { PathGeneratorSingle } from "./PathGenerator";
import { Point } from "../Common/Point";
import { RandomIndex } from "../Common/Random";
import { Vector2 } from "../Common/Vector2";
/**
*
*
*
*
*
* 0P
*
*
*
*
*
* 0
*
*
* 0
*
*
*
*
*/
export class Part<T = any, Matrix = any>
{
Id: number;//用于确定Part的唯一性,并且有助于在其他Work中还原
//下面数据只用于还原零件的显示状态,在优化中无作用
UserData: T; //应该也是可序列化的实体
PlaceCS: Matrix;//放置矩阵 Matrix4
Parent: Part;
Holes: PartState[] = [];
//零件的不同旋转状态,这个数组会在所有的状态间共享,以便快速切换状态
RotatedStates: PartState[] = [];
PlacePosition: Point; //放置位置(相对于容器的位置)
StateIndex = 0; //放置状态
get State(): PartState //零件当前的状态
{
return this.RotatedStates[this.StateIndex];
}
//初始化零件的各个状态,按360度旋转个数
Init(path: Path, bin: Path, rotateCount = 4): this
{
let rotations: number[] = [];
let a = 2 * Math.PI / rotateCount;
for (let i = 0; i < rotateCount; i++)
{
rotations.push(a * i);
}
this.Init2(path, bin, rotations);
return this;
}
//初始化零件的各个状态,按旋转角度表
Init2(path: Path, bin: Path, rotations: number[] = []): this
{
let pathP = path.OrigionMinPoint;
let path_0 = PathGeneratorSingle.Allocate(path);
let pathSet = new Set<Path>();
//初始化零件的状态集合
for (let pa of rotations)
{
let partState = new PartState();
partState.Rotation = pa;
if (pa === 0)
{
partState.Contour = path_0;
partState.OrigionMinPoint = pathP;
}
else
{
let path_r = new Path(path.Points, pa);
partState.Contour = PathGeneratorSingle.Allocate(path_r);
partState.Contour.Area = path_0.Area;
//避免重复的Path进入State
if (pathSet.has(partState.Contour)) continue;
let p0 = path_r.OrigionMinPoint;
let c = Math.cos(-pa);
let s = Math.sin(-pa);
let x1 = p0.x * c - p0.y * s;
let y1 = p0.x * s + p0.y * c;
partState.OrigionMinPoint = new Vector2(pathP.x + x1, pathP.y + y1);
if (partState.Contour.Origion === undefined)
{
partState.Contour.Origion = path_0;
partState.Contour.OrigionMinPoint = p0;
}
}
//记录已有Path
pathSet.add(partState.Contour);
//必须能放置
if (bin.GetInsideNFP(partState.Contour))
{
this.RotatedStates.push(partState);
PathGeneratorSingle.RegisterId(partState.Contour);
}
}
//为了复用NFP,不管第0个Path是否可用,都注册它.
if (this.RotatedStates.length > 4)
PathGeneratorSingle.RegisterId(path_0);
return this;
}
AppendHole(path: Path)
{
let hole = new PartState();
hole.Contour = PathGeneratorSingle.Allocate(path);
PathGeneratorSingle.RegisterId(hole.Contour);
hole.OrigionMinPoint = path.OrigionMinPoint;
hole.Rotation = 0;
this.Holes.push(hole);
}
//浅克隆
Clone()
{
let part = new Part();
part.Id = this.Id;
part.UserData = this.UserData;
part.RotatedStates = this.RotatedStates;
part.StateIndex = this.StateIndex;
part.Holes = this.Holes;
return part;
}
//旋转起来,改变自身旋转状态(变异)
Mutate(): this
{
this.StateIndex = RandomIndex(this.RotatedStates.length, this.StateIndex);
return this;
}
//#region -------------------------File-------------------------
ReadFile(file: NestFiler)
{
this.Id = file.Read();
let count = file.Read() as number;
this.RotatedStates = [];
for (let i = 0; i < count; i++)
{
let state = new PartState();
state.ReadFile(file);
this.RotatedStates.push(state);
}
count = file.Read();
this.Holes = [];
for (let i = 0; i < count; i++)
{
let state = new PartState();
state.ReadFile(file);
this.Holes.push(state);
}
}
WriteFile(file: NestFiler)
{
file.Write(this.Id);
file.Write(this.RotatedStates.length);
for (let state of this.RotatedStates)
state.WriteFile(file);
file.Write(this.Holes.length);
for (let hole of this.Holes)
hole.WriteFile(file);
}
//#endregion
}

View File

@ -0,0 +1,35 @@
import { Path } from "./Path";
import { Point } from "../Common/Point";
import { PathGeneratorSingle } from "./PathGenerator";
import { NestFiler } from "../Common/Filer";
/**
*
* 0便
*/
export class PartState
{
Rotation: number;
OrigionMinPoint: Point;
Contour: Path;//轮廓
//#region -------------------------File-------------------------
ReadFile(file: NestFiler)
{
this.Rotation = file.Read();
this.OrigionMinPoint = file.Read();
let index = file.Read() as number;
this.Contour = PathGeneratorSingle.paths[index];
if (!this.Contour)
console.log(index);
}
WriteFile(file: NestFiler)
{
file.Write(this.Rotation);
file.Write(this.OrigionMinPoint);
file.Write(this.Contour.Id);
}
//#endregion
}

345
src/Nest/Core/Path.ts Normal file
View File

@ -0,0 +1,345 @@
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";
/**
*
* NFPNFPCahce
* 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;
}

View File

@ -0,0 +1,85 @@
import { Path } from "./Path";
import { equaln, FixIndex } from "../Common/Util";
/**
*
* PathPath
* 使PATH有复用路径缓存
*
* Path生成是对本次优化唯一
*/
class PathGenerator
{
paths: Path[] = [];
pathAreaMap: { [key: string]: Path[]; } = {};
//缓存命中次数
cacheCount = 0;
/**
*
*
* id没有被注册,bin中
*/
Allocate(path: Path): Path
{
let area = path.Area.toFixed(0);
let paths = this.pathAreaMap[area];
if (paths)
{
for (let ps of paths)
{
if (EqualPath(ps, path))
{
this.cacheCount++;
return ps;
}
}
paths.push(path);
}
else
this.pathAreaMap[area] = [path];
return path;
}
RegisterId(path: Path)
{
if (path.Id === undefined)
{
path.Id = this.paths.length;
this.paths.push(path);
}
}
Clear()
{
this.paths.length = 0;
this.pathAreaMap = {};
this.cacheCount = 0;
}
}
/**
* ,
*/
function EqualPath(path1: Path, path2: Path): boolean
{
if (path1.Points.length !== path2.Points.length) return false;
let p0 = path1.Points[0];
let p2Index = path2.Points.findIndex(p =>
{
return equaln(p.x, p0.x, 1e-3) && equaln(p.y, p0.y, 1e-3);
});
for (let i = 0; i < path1.Points.length; i++)
{
let p1 = path1.Points[i];
let p2 = path2.Points[FixIndex(p2Index + i, path2.Points)];
if (!equaln(p1.x, p2.x, 1e-4) || !equaln(p1.y, p2.y, 1e-4))
return false;
}
return true;
}
export let PathGeneratorSingle = new PathGenerator;

View File

@ -0,0 +1,7 @@
export enum PlaceType
{
Hull = 0,//凸包模式 (凸包面积)
Box = 1, //盒子模式 (长乘以宽)
Gravity = 2,//重力模式(重力)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,132 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
target="_blank"
rel="noopener"
>typescript</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest"
target="_blank"
rel="noopener"
>unit-jest</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "HelloWorld",
props: {
msg: String
}
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<span>
<el-button :type="buttonType" @click="$refs.inputFile.click()">
<slot>
选择文件
</slot>
</el-button>
<input
ref="inputFile"
v-show="false"
type="file"
id="input"
:accept="accept"
@change="
e => {
$emit('change', e.target.files);
}
"
/>
</span>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
props: {
accept: {
type: String,
default: ""
},
buttonType: {
type: String,
default: ""
}
}
});
</script>

View File

@ -1,8 +1,44 @@
import Vue from "vue";
import App from "./App.vue";
import Vue from 'vue';
// import dayjs from 'dayjs';
import App from './App.vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import 'element-ui/lib/theme-chalk/display.css';
// import 'element-ui/lib/theme-default/index.css'
// import './icons'; // icon
// import '@/styles/index.scss'; // global css
// import '@/styles/custom.scss'; // custom css
// import VueRouter from 'vue-router';
// import store from './store';
// import Vuex from 'vuex';
// import router from './routes';
// import './permission'; // permission control
// import 'dayjs/locale/zh-cn'; // load on demand
// import GlobalConfig from "./config";
// GlobalConfig.event.saveAllConfig = ()=>{};
// console.log(GlobalConfig.event)
// dayjs.locale('zh-cn');
// import('element-ui').then(ElementUI=>{
Vue.use(ElementUI, { size: 'small' });
// Vue.use(VueRouter);
// Vue.use(Vuex);
new Vue({
// el: '#app',
// template: '<App/>',
// router,
// store,
// components: { App }
render: (h: any) => h(App),
}).$mount('#app');
// })
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount("#app");

View File

@ -1,12 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
});
expect(wrapper.text()).toMatch(msg);
});
});

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
// "strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
@ -10,21 +10,11 @@
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"types": ["node","webpack-env", "jest"],
"paths": {
"@/*": [
"src/*"
]
"@/*": ["src/*"]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/**/*.ts",
@ -33,7 +23,5 @@
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

44
vue.config.js Normal file
View File

@ -0,0 +1,44 @@
// vue.config.js
const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
// publicPath: '',
runtimeCompiler: true,
productionSourceMap: false,
configureWebpack: {
devtool: "source-map",
output: {
globalObject: "this",
},
},
chainWebpack(config) {
let tsRule = config.module
.rule("ts").uses;
config.module
.rule("worker")
.test(/\.worker\.ts$/i)
.use("worker-loader")
.loader("worker-loader").end()
.use("ts-loader")
.loader("ts-loader").options({
transpileOnly: true,
happyPackMode: false
}).end();
config.module
.rule("fonts")
.use("url-loader")
.loader("url-loader")
.options({
limit: 4096,
fallback: {
loader: "file-loader",
options: {
name: "fonts/[name].[contenthash].[ext]",
},
},
})
.end();
},
};