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
|
### 依赖
|
||||||
|
- js-angusj-clipper
|
||||||
## Project setup
|
- monotone-convex-hull-2d
|
||||||
```
|
|
||||||
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/).
|
|
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": {
|
"dependencies": {
|
||||||
"core-js": "^3.6.4",
|
"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"
|
"vue": "^2.6.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^24.0.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
"@types/node": "^13.13.4",
|
||||||
"@typescript-eslint/parser": "^2.26.0",
|
|
||||||
"@vue/cli-plugin-babel": "~4.3.0",
|
"@vue/cli-plugin-babel": "~4.3.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.3.0",
|
|
||||||
"@vue/cli-plugin-typescript": "~4.3.0",
|
"@vue/cli-plugin-typescript": "~4.3.0",
|
||||||
"@vue/cli-plugin-unit-jest": "~4.3.0",
|
"@vue/cli-plugin-unit-jest": "~4.3.0",
|
||||||
"@vue/cli-service": "~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",
|
"@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",
|
"prettier": "^1.19.1",
|
||||||
"sass": "^1.26.3",
|
"sass": "^1.26.3",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"typescript": "~3.8.3",
|
"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>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<img alt="Vue logo" src="./assets/logo.png" />
|
<select-file accept=".cfdat" button-type="primary" @change="loadCFData"
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
|
>导入CFDATA</select-file
|
||||||
|
>
|
||||||
|
<el-button @click="stopThread">停止优化</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
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({
|
export default Vue.extend({
|
||||||
name: "App",
|
name: "app",
|
||||||
components: {
|
components: { SelectFile },
|
||||||
HelloWorld
|
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>
|
</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 Vue from 'vue';
|
||||||
import App from "./App.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": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"strict": true,
|
// "strict": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
@ -10,21 +10,11 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": [
|
"types": ["node","webpack-env", "jest"],
|
||||||
"webpack-env",
|
|
||||||
"jest"
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["src/*"]
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"lib": [
|
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
@ -33,7 +23,5 @@
|
|||||||
"tests/**/*.ts",
|
"tests/**/*.ts",
|
||||||
"tests/**/*.tsx"
|
"tests/**/*.tsx"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"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