init
This commit is contained in:
parent
30ef9522a4
commit
80c7f6c3ca
31
.eslintrc.js
31
.eslintrc.js
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
32
README.md
32
README.md
@ -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
1067
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
69
src/App.vue
69
src/App.vue
@ -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>
|
||||
|
21
src/Business/BlockConvert.ts
Normal file
21
src/Business/BlockConvert.ts
Normal 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
11
src/Business/BlockView.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class BlockView{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(canvas:HTMLCanvasElement) {
|
||||
|
||||
}
|
||||
drawBlock(){
|
||||
|
||||
}
|
||||
}
|
9
src/Business/FileHelper.ts
Normal file
9
src/Business/FileHelper.ts
Normal 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)
|
||||
})
|
||||
}
|
5
src/Business/LayoutConfig.ts
Normal file
5
src/Business/LayoutConfig.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class LayoutConfig{
|
||||
loadLegacyConfig(){
|
||||
|
||||
}
|
||||
}
|
88
src/Business/LayoutEngine.ts
Normal file
88
src/Business/LayoutEngine.ts
Normal 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
185
src/Common/ArrayExt.ts
Normal 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
7
src/Common/Sleep.ts
Normal 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
160
src/Nest/Common/Box2.ts
Normal 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);
|
||||
}
|
||||
};
|
21
src/Nest/Common/ClipperCpp.ts
Normal file
21
src/Nest/Common/ClipperCpp.ts
Normal 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("载入成功!");
|
||||
});
|
||||
});
|
||||
}
|
8
src/Nest/Common/ConvexHull2D.ts
Normal file
8
src/Nest/Common/ConvexHull2D.ts
Normal 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
30
src/Nest/Common/Filer.ts
Normal 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
6
src/Nest/Common/Point.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export interface Point
|
||||
{
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
9
src/Nest/Common/Random.ts
Normal file
9
src/Nest/Common/Random.ts
Normal 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;
|
||||
}
|
8
src/Nest/Common/Shuffle.ts
Normal file
8
src/Nest/Common/Shuffle.ts
Normal 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
35
src/Nest/Common/Util.ts
Normal 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
287
src/Nest/Common/Vector2.ts
Normal 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
373
src/Nest/Core/Container.ts
Normal 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
|
||||
}
|
4
src/Nest/Core/DefaultBin.ts
Normal file
4
src/Nest/Core/DefaultBin.ts
Normal 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
190
src/Nest/Core/Individual.ts
Normal 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
|
||||
}
|
37
src/Nest/Core/NestCache.ts
Normal file
37
src/Nest/Core/NestCache.ts
Normal 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 = {};
|
||||
}
|
||||
}
|
54
src/Nest/Core/NestDatabase.ts
Normal file
54
src/Nest/Core/NestDatabase.ts
Normal 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
|
||||
}
|
160
src/Nest/Core/OptimizeMachine.ts
Normal file
160
src/Nest/Core/OptimizeMachine.ts
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
27
src/Nest/Core/OptimizeWorker.worker.ts
Normal file
27
src/Nest/Core/OptimizeWorker.worker.ts
Normal 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
178
src/Nest/Core/Part.ts
Normal 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";
|
||||
|
||||
/**
|
||||
* 零件类
|
||||
* 零件类可以绑定数据,也存在位置和旋转状态的信息
|
||||
*
|
||||
* 初始化零件:
|
||||
* 传入零件的轮廓,刀半径,包围容器(或者为空?)
|
||||
* 初始化用于放置的轮廓。将轮廓首点移动到0点,记录移动的点P。
|
||||
*
|
||||
* 零件放置位置:
|
||||
* 表示零件轮廓首点的位置。
|
||||
*
|
||||
* 零件的旋转:
|
||||
* 表示零件轮廓按照首点(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
|
||||
}
|
35
src/Nest/Core/PartState.ts
Normal file
35
src/Nest/Core/PartState.ts
Normal 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
345
src/Nest/Core/Path.ts
Normal 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";
|
||||
|
||||
/**
|
||||
* 轮廓路径类
|
||||
* 可以求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;
|
||||
}
|
85
src/Nest/Core/PathGenerator.ts
Normal file
85
src/Nest/Core/PathGenerator.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Path } from "./Path";
|
||||
import { equaln, FixIndex } from "../Common/Util";
|
||||
|
||||
/**
|
||||
* 轮廓路径构造器
|
||||
* 传递一组简化后的点表过来,如果已经有同样的点表时,返回已经生产的Path,避免重复产生Path。
|
||||
* 使用相同的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;
|
7
src/Nest/Core/PlaceType.ts
Normal file
7
src/Nest/Core/PlaceType.ts
Normal 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 |
@ -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>
|
36
src/components/SelectFile.vue
Normal file
36
src/components/SelectFile.vue
Normal 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>
|
48
src/main.ts
48
src/main.ts
@ -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");
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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
44
vue.config.js
Normal 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();
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user