2 Commits

Author SHA1 Message Date
f25592d145 feat:提交 2025-07-23 10:12:11 +08:00
2ebb3e1abe feat:提交 2025-07-22 18:22:31 +08:00
128 changed files with 76645 additions and 302 deletions

View File

@@ -1,81 +1,7 @@
# Cut Abstractions
这是一个用于MES新版生产的抽象库提供了一套可扩展的处理器和解析器用于处理各种切割相关的指令和数据。
## 核心概念
- **处理器 (Processor)**: 负责执行具体的加工任务。每个处理器都包含名称、版本和执行方法。开发者可以继承 `ProcessorBase` 来实现自定义的处理器。
- **解析器 (Parser)**: 负责解析文本指令,并调用相应的处理代码。`ParserBase` 提供了解析和执行指令的基本框架。
- **上下文 (Context)**: 在处理器执行期间传递数据,包含输入、参数和输出。
## 主要功能
- **可扩展的处理器架构**: 允许开发者轻松添加新的加工处理器,以适应不同的业务需求。
- **灵活的指令解析**: 支持自定义指令集,可以解析文本格式的指令并执行相应的操作。
- **清晰的数据流**: 通过上下文对象在处理器之间传递数据,使得数据流清晰可控。
## 使用示例
以下是一个简单的示例,展示了如何使用本库:
```typescript
import { ProcessorBase, ProcessorContext } from 'cut-abstractions';
// 定义输入、输出和配置类型
interface MyInput {
data: string;
}
interface MyOutput {
result: string;
}
interface MyConfig {
param: string;
}
// 创建一个自定义处理器
class MyProcessor extends ProcessorBase<MyInput, MyOutput, MyConfig> {
get name() {
return 'my-processor';
}
get version() {
return '1.0.0';
}
exec(context: ProcessorContext<MyInput, MyOutput, MyConfig>) {
// 执行处理逻辑
const inputData = context.input?.data || '';
const param = context.params?.param || '';
context.output = {
result: `Processed: ${inputData} with param: ${param}`,
};
}
}
// 使用处理器
const processor = new MyProcessor();
const context: ProcessorContext<MyInput, MyOutput, MyConfig> = {
input: { data: 'hello' },
params: { param: 'world' },
};
processor.exec(context);
console.log(context.output?.result); // "Processed: hello with param: world"
```
## 模块
- `base`: 提供了处理器的基本抽象。
- `parsers`: 提供了指令解析器的基本抽象。
- `models`: 定义了项目中使用的数据模型,如 `Config``Knife``File`
## 术语表
## 生产接口协议
本项目使用typescript编写IDE推荐使用vscode。
### 术语表
| 中文 | CAD | MES | IMES | 备注 |
| --- | --- | --- | --- | --- |
| 房名 | RoomName | roomName | roomName | |
@@ -86,12 +12,14 @@ console.log(context.output?.result); // "Processed: hello with param: world"
| 余料 | 无 | scrap | remain | |
| 排单 | 无 | planOrder |planOrder | |
## 编译与发布
更新 package.json 版本号
```shell
pnpm clean
pnpm build
pnpm release
### 编译与发布
更新 package.json 版本号
```shell
pnpm clean
pnpm build
pnpm release
```
### 开发建议
MES与IMES存在不少命名上的差异可以考虑 接口类型独立, 参数与配置单独创建类型

View File

@@ -1,6 +1,6 @@
{
"name": "cut-abstractions",
"version": "0.2.0",
"version": "0.1.7",
"description": "",
"files": [
"dist/**/*"
@@ -28,7 +28,15 @@
"@types/jest": "^30.0.0",
"cadapi": "http://gitea.cf/MES-FE/webcad-api/archive/0.0.60.tar.gz",
"jest": "^30.0.2",
"jest-worker": "^30.0.2",
"js-angusj-clipper": "^1.0.4",
"rimraf": "^6.0.1",
"ts-jest": "^29.4.0",
"typescript": "^5.8.3"
},
"dependencies": {
"jest-worker": "^30.0.2",
"three": "^0.178.0",
"webworker": "^0.8.4"
}
}

144
pnpm-lock.yaml generated
View File

@@ -7,6 +7,16 @@ settings:
importers:
.:
dependencies:
jest-worker:
specifier: ^30.0.2
version: 30.0.2
three:
specifier: ^0.178.0
version: 0.178.0
webworker:
specifier: ^0.8.4
version: 0.8.4
devDependencies:
'@jest/globals':
specifier: ^30.0.2
@@ -26,9 +36,15 @@ importers:
jest:
specifier: ^30.0.2
version: 30.0.4(@types/node@24.0.12)
js-angusj-clipper:
specifier: ^1.0.4
version: 1.3.1
rimraf:
specifier: ^6.0.1
version: 6.0.1
ts-jest:
specifier: ^29.4.0
version: 29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.28.0))(jest-util@30.0.2)(jest@30.0.4(@types/node@24.0.12))(typescript@5.8.3)
typescript:
specifier: ^5.8.3
version: 5.8.3
@@ -624,6 +640,9 @@ packages:
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
babel-jest@30.0.4:
resolution: {integrity: sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -667,6 +686,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
bs-logger@0.2.6:
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
engines: {node: '>= 6'}
bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
@@ -763,6 +786,11 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ejs@3.1.10:
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
engines: {node: '>=0.10.0'}
hasBin: true
electron-to-chromium@1.5.180:
resolution: {integrity: sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==}
@@ -810,6 +838,9 @@ packages:
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -938,6 +969,11 @@ packages:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jake@10.9.2:
resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==}
engines: {node: '>=10'}
hasBin: true
jest-changed-files@30.0.2:
resolution: {integrity: sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -1066,6 +1102,9 @@ packages:
node-notifier:
optional: true
js-angusj-clipper@1.3.1:
resolution: {integrity: sha512-/qru4QXxN/gBbQjL4WaFl296YSM8kh5XKpNuNqfZhJ4t4Hw3KeLc5ERj3XHAeLi6pBrqeh6o9PFZUpS3QThEEQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1100,6 +1139,9 @@ packages:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -1114,6 +1156,9 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
@@ -1135,6 +1180,10 @@ packages:
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -1354,6 +1403,9 @@ packages:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
three@0.178.0:
resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==}
tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
@@ -1361,6 +1413,33 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
ts-jest@29.4.0:
resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==}
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@babel/core': '>=7.0.0-beta.0 <8'
'@jest/transform': ^29.0.0 || ^30.0.0
'@jest/types': ^29.0.0 || ^30.0.0
babel-jest: ^29.0.0 || ^30.0.0
esbuild: '*'
jest: ^29.0.0 || ^30.0.0
jest-util: ^29.0.0 || ^30.0.0
typescript: '>=4.3 <6'
peerDependenciesMeta:
'@babel/core':
optional: true
'@jest/transform':
optional: true
'@jest/types':
optional: true
babel-jest:
optional: true
esbuild:
optional: true
jest-util:
optional: true
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -1372,6 +1451,10 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
@@ -1396,6 +1479,10 @@ packages:
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
webworker@0.8.4:
resolution: {integrity: sha512-zzsVxtHf+mCn0WuYLarSWfRGmX7JiYKkKvso5FYC7rJ9G8svwGQA5a51Sjq9D2c/rKVU6U/kyBcaI7gUTVlsJg==}
engines: {node: '>=0.4.3'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -2104,6 +2191,8 @@ snapshots:
dependencies:
sprintf-js: 1.0.3
async@3.2.6: {}
babel-jest@30.0.4(@babel/core@7.28.0):
dependencies:
'@babel/core': 7.28.0
@@ -2180,6 +2269,10 @@ snapshots:
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.1)
bs-logger@0.2.6:
dependencies:
fast-json-stable-stringify: 2.1.0
bser@2.1.1:
dependencies:
node-int64: 0.4.0
@@ -2245,6 +2338,10 @@ snapshots:
eastasianwidth@0.2.0: {}
ejs@3.1.10:
dependencies:
jake: 10.9.2
electron-to-chromium@1.5.180: {}
emittery@0.13.1: {}
@@ -2292,6 +2389,10 @@ snapshots:
dependencies:
bser: 2.1.1
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -2421,6 +2522,13 @@ snapshots:
dependencies:
'@isaacs/cliui': 8.0.2
jake@10.9.2:
dependencies:
async: 3.2.6
chalk: 4.1.2
filelist: 1.0.4
minimatch: 3.1.2
jest-changed-files@30.0.2:
dependencies:
execa: 5.1.1
@@ -2732,6 +2840,8 @@ snapshots:
- supports-color
- ts-node
js-angusj-clipper@1.3.1: {}
js-tokens@4.0.0: {}
js-yaml@3.14.1:
@@ -2755,6 +2865,8 @@ snapshots:
dependencies:
p-locate: 4.1.0
lodash.memoize@4.1.2: {}
lru-cache@10.4.3: {}
lru-cache@11.1.0: {}
@@ -2767,6 +2879,8 @@ snapshots:
dependencies:
semver: 7.7.2
make-error@1.3.6: {}
makeerror@1.0.12:
dependencies:
tmpl: 1.0.5
@@ -2788,6 +2902,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.12
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
@@ -2970,12 +3088,34 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.2
three@0.178.0: {}
tmpl@1.0.5: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.28.0))(jest-util@30.0.2)(jest@30.0.4(@types/node@24.0.12))(typescript@5.8.3):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 30.0.4(@types/node@24.0.12)
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
semver: 7.7.2
type-fest: 4.41.0
typescript: 5.8.3
yargs-parser: 21.1.1
optionalDependencies:
'@babel/core': 7.28.0
'@jest/transform': 30.0.4
'@jest/types': 30.0.1
babel-jest: 30.0.4(@babel/core@7.28.0)
jest-util: 30.0.2
tslib@2.8.1:
optional: true
@@ -2983,6 +3123,8 @@ snapshots:
type-fest@0.21.3: {}
type-fest@4.41.0: {}
typescript@5.8.3: {}
undici-types@7.8.0: {}
@@ -3027,6 +3169,8 @@ snapshots:
dependencies:
makeerror: 1.0.12
webworker@0.8.4: {}
which@2.0.2:
dependencies:
isexe: 2.0.0

View File

@@ -0,0 +1,152 @@
import { FaceType, PlaceBlock, PlaceBlockDetail, PlaceBorderContour, PlaceMaterial, PlaceStyle, SizeExpand } from "../confClass"
import { BlockDetailHelperBase } from "../handleAbility/blockDetailHelperBase"
import { PolylineHelper } from "../handleAbility/common/LayoutEngine/PolylineHelper"
import { KnifeHelper } from "../handleAbility/knifeHelper"
/** 小板明细 相关的计算 孔 造型 以及轮廓 */
export class BlockDetailHelper extends BlockDetailHelperBase {
/** 预铣(板件外扩)值 */
preMillingSize = 0;
constructor(config?: any) {
super()
if(config){
this.updateConfig(config)
}
}
updateConfig(config) {
if (config) {
let keys = Object.keys(config)
for (const key of keys) {
if (Reflect.has(this, key)) {
this[key] = config[key]
}
}
}
}
/** 小板初始化1各种轮廓|走刀路径|2尺寸扩展后的轮廓与走刀路径 */
initBlock(block: PlaceBlock, pm: PlaceMaterial) {
if (block.blockDetail == null) {
// 异常情况
console.error(`【initBlock()】${block.blockNo}没用板件明细,请检查`);
return
}
if (block.blockDetail.borderContour) return // 已初始化
// const pm = block.placeMaterial
const bd = block.blockDetail
if (pm == null) {
return // 异常
}
const cutKnifeR = pm.diameter / 2
const cutGap = pm.cutKnifeGap
// 开料偏移值 ,矩形波倒角
bd.offsetKnifeRadius = pm.diameter / 2
bd.isOffsetRounding = true // 默认要倒角
bd.preMillingSize = pm.preMillingSize
if (bd.points.length == 0) // 矩形板,设置不倒角
{
bd.isOffsetRounding = false// sysConfig.RegularBlockFilletCurve;
}
else // 异形板
{
// 异形板倒角
bd.isOffsetRounding = true // sysConfig.isUnRegularBlockChamfer
}
// 初始化 小板基础轮廓
// 1.原始轮廓,成品轮廓
const border_final = this.createFinalBorder(bd, block)
// 2.开料轮廓 <不含预铣>
const border_org = this.createOrgBorder(bd)
bd.borderContour = new PlaceBorderContour(PlaceStyle.FRONT, border_final, border_org)
// 3.开料轮廓_带预铣, 外扩尺寸
bd.borderContour.borderPreMilling = border_org
bd.preMillingExpandSize = new SizeExpand()
// console.log('开料轮廓_带预铣, 外扩尺寸', block, bd.preMillingExpandSize, sysConfig.preMillingSize)
if (this.preMillingSize > 0.0001) {
const rt = this.creatOrgBorderWithPrevCut(block, border_org, this.sysConfig.preMillingSize)
bd.borderContour.borderPreMilling = rt.newBorders
bd.preMillingExpandSize = rt.newSizeExpand
// console.log('开料轮廓_带预铣, 外扩尺寸==》after', block, rt.newSizeExpand)
}
// 4. 板内走刀路径,板内空间
const innerGroup = this.analyeInners(bd, cutKnifeR, cutGap)
bd.borderContour.borderModelThrough = innerGroup.borders_inner_org
bd.borderContour.borderModelThroughR = innerGroup.borders_inner_r
bd.borderContour.cutLinesModelThrough = innerGroup.borders_inner_cut
bd.borderContour.borderInnerPlace = innerGroup.borders_inner_place
bd.borderContour.blockInnerSpace = innerGroup.spaces
// 5 造型外扩轮廓, 外扩尺寸
const { pls_model, sizeout: sizeout_model } = this.createPolyline_model(block)
bd.borderContour.polylinesOutModel = pls_model
bd.modelExpandSize = sizeout_model
// 6. 2V刀路 外扩轮廓,外扩尺寸
const { pls_2v, sizeout_2v } = this.createPolyline_2vModel(block)
bd.borderContour.polylines2vModel = pls_2v
bd.vKnifeModelExpandSize = sizeout_2v
// 7.同刀辅助 外扩尺寸
let isUseSameKnifeToHelpCut = this.sysConfig.helpCutStyle || 0
let useSameKnifeToHelpCutGap = this.sysConfig.helpCutGap || 0
const isSameKnifeToCut = isUseSameKnifeToHelpCut && useSameKnifeToHelpCutGap > 0
if (isSameKnifeToCut == false || bd.isUseSameKnifeToHelpCut == false) {
// 未启动同刀辅助, 或 该小板 不需要同刀辅助
bd.useSameKnifeToHelpCutGap = 0
bd.sameKnfieHelpExpandSize = new SizeExpand()
}
else {
const gap = useSameKnifeToHelpCutGap
bd.useSameKnifeToHelpCutGap = gap
bd.sameKnfieHelpExpandSize = new SizeExpand({ left: gap, right: gap, under: gap, upper: gap })
}
// 2V刀路,预洗,同刀辅助开料,出板造型刀
this.resetBlock(block, pm)
}
/** 二维刀路初始化 */
init2VModel(blockDetail: PlaceBlockDetail, isCNC = false, _knifeHelper: KnifeHelper) {
for (let model of blockDetail.models) {
if (!model.isVKnifeModel)
continue
let vModels: any = []
model.VLines = vModels
let isFaceB = model.face == FaceType.BACK
if (model.pointList.length < 1)
continue
let ps = model.pointList.map((t) => { return { x: t.pointX, y: t.pointY, bul: t.curve } })
let pl = PolylineHelper.create(ps)
if (model.VLines?.length > 0)
return // 已经分析了
model.VLines = []
for (let os of model.offsetList) {
let knife1 = isCNC ? null : _knifeHelper.getModelKnifeByName(os.name) // getModelKnifeByName(os.name)
let knifeR = os.radius
let knifeId = knife1 ? knife1.knifeId : -1
try {
let vps_1 = PolylineHelper.getVModelPoints_offset(pl, os.offset, os.depth, os.angle)
let vLine = { isFaceB, name: os.name, value: os.offset, knife: knife1, knifeId, knifeRadius: knifeR, depth: os.depth, points: vps_1, offset: os }
vModels.push(vLine) // 偏移路径
model.VLines.push(vLine)
}
catch (err) {
console.log('v型刀走刀路径算法出错。' + err)
}
}
model.VLines = vModels
}
}
}

View File

@@ -0,0 +1,283 @@
import { ConfigBase } from "../../src/models/config";
import { confItem, HoleArrange, Knife, PlaceBlock, PlaceBlockDetail, PlaceMaterial } from "../confClass";
import { Big_bang, ComposingType, LineType, WorkerItemType, xbang } from "../handleAbility/RectOptimizeWorker/bang";
/** 配置列表 */
export const confList: confItem[] = [
{
key: 'isCutProcess',
label: '开料机(雕刻机)加工',
value: true
},
{
key: 'isCutAndCNCProcess',
label: '开料机CNC组合',
value: false
},
{
key: 'isCustomized',
label: '定制加工',
value: false
},
{
key: 'cutBoardBorder',
label: '总修边宽度',
value: 3
},
{
key: 'blockKnifeLineSpacing',
label: '刀路间距',
value: 0
},
{
key: 'isDoubleFaceBlockFirst',
label: '双面开料优先排版',
value: true
},
{
key: 'isRectPlace',
label: '新优化规则排版',
value: false
},
{
// yuLiaoBoardDo2FaceBlock
key: 'isDoubleFaceBlockInRemain',
label: '余料板允许排入双面加工的小板',
value: true
},
]
/**
* 新优化
* 调用流程
* 1、准备工作 加载配置 + 加载刀库 + 设置 回调函数
* 2、调用 startPlaceThreed
* 入参data (数据源 必须包含 小板数据列表和小板明细数据列表)
* pm 优化的材质
* pm.diameter = knife?.diameter
pm.cutKnifeName = knife.knifeName
*/
export class BusinessRectOptimizeMachine extends ConfigBase {
/** 多线程数据集 */
workerList: WorkerItemType[] = []
/** 优化回调 */
placeCallBack: Function = () => { }
constructor() {
super()
}
/** 设置优化回调 */
setPlaceTaskCallBackFun(func: Function) {
this.placeCallBack = func
}
/**开始优化线程 输入的数据没有初始化
* @param data 数据源 包含小板列表和小板明细列表 data内需要包含blockList小板列表 和 blockDetailList小板明细列表
* @param pm 优化的材质 包含该材质的可用开料大板信息 需包含开料刀的信息 pm.diameter pm.cutKnifeName
*/
startPlaceThreed(data, pm: PlaceMaterial) {
let { blockList, blockDetailList } = data
let bList: any = []
blockList.map(e => {
if (e.goodsId == pm.goodsId) {
bList[e.blockNo] = e
let detail = blockDetailList.find(x => x.blockId == e.blockId)
if (!Reflect.has(bList[e.blockNo], 'blockDetail')) {
bList[e.blockNo].blockDetail = new PlaceBlockDetail(detail)
}
// bList[e.blockNo].isTurnFaceToPlace = !this.getDoFace(bList[e.blockNo], this.processMode())
// 是否翻面后续处理
bList[e.blockNo].isTurnFaceToPlace = true
pm.blockList.push(e)
}
})
pm.cutBorder = this.cutBoardBorder
pm.cutKnifeGap = this.blockKnifeLineSpacing
/** 小板 */
let bans: xbang[] = []
// 实际开料大板的列表
let big_Bang: Big_bang[] = []
let big_BangSL: number[] = []
let border = this.cutBoardBorder
let borderOff = (pm.diameter + pm.cutKnifeGap) / 2
// 余料板 以及实际开料大板
for (const cuttingBoard of pm.remainBoardList) {
big_Bang.push({ w: cuttingBoard.width - border * 2 + borderOff * 2, l: cuttingBoard.length - border * 2 + borderOff * 2, x: border - borderOff, y: border - borderOff })
big_BangSL.push(cuttingBoard.count || 999)
}
// big_Bang = []
// big_BangSL = []
// 母板 兜底的
big_Bang.push({ w: pm.width - border * 2 + borderOff * 2, l: pm.length - border * 2 + borderOff * 2, x: border - borderOff, y: border - borderOff })
// 生成小板
for (let key in bList) {
let b = bList[key]
let bid = b.blockNo
let width = b.placeFullWidth
let length = b.placeFullLength
let line = this.toLine(b.texture)
bans.push({
l: length,
w: width,
line,
face: this.toface(b),
id: bid,
bno: b.blockNo,
holeFaceCount: 3,
isRect: !b.isUnRegular,
hasHole: false,
isdtwosided: true,
})
}
let bestCount = 0
if (bans.length == 0) // 没有板了
{
let best = []
let yl: Big_bang[] = []
let fit = 0
let resObj = {
data: { bList, best, yl, fit, bans, width: pm.width, length: pm.length, bestCount: bestCount++, pm },
info: {
times: -1,
type: 'noBan'
}
}
this.placeCallBack(resObj)
return
}
let xyhcs = 50
if (bans.length > 1000) { xyhcs = 40 }
else if (bans.length > 1500) { xyhcs = 30 }
else if (bans.length > 2000) { xyhcs = 25 }
else if (bans.length > 4000) { xyhcs = 20 }
else if (bans.length > 6000) { xyhcs = 15 }
else if (bans.length > 10000) { xyhcs = 10 }
else if (bans.length > 15000) { xyhcs = 5 }
else if (bans.length > 20000) { xyhcs = 1 }
let isDoubleFaceBlockFirst = this.isDoubleFaceBlockFirst // 双面加工排前面
let gap = this.blockKnifeLineSpacing
// this.bestfit = 0
for (let j = 0; j < 1; j++) {
let w = new Worker(new URL('../handleAbility/RectOptimizeWorker/RectOptimizeWorker.worker', import.meta.url), { type: 'module' })
const data = {
type: 'start',
data: [bans, big_Bang, big_BangSL, xyhcs, isDoubleFaceBlockFirst, gap, this.isRectPlace, this.isDoubleFaceBlockInRemain],
}
let item: WorkerItemType = {
w: w,
goodsId: pm.goodsId,
pm,
status: 'start'
}
if (this.workerList.findIndex(e => e.goodsId == item.goodsId) == -1) {
this.workerList.push(item)
}
let workItem = this.workerList.find(e => e.goodsId == pm.goodsId)
if (workItem && workItem != undefined) {
workItem.w?.postMessage(data)
workItem.w.onmessage = async (d) => {
let [best, yl, fit, info] = d.data as [any[], Big_bang[], number, any]
switch (info.type) {
case 'loop':
let resObj = {
data: { bList, best, yl, fit, bans, width: pm.width, length: pm.length, bestCount: bestCount++, pm },
info
}
this.placeCallBack(resObj)
break;
case 'stop':
console.error('stop =》dataHandleBase', info, this.workerList)
this.terminateWorker({ goodsId: pm.goodsId })
break;
case 'isStop':
// console.error('isStop', info)
break;
default:
break;
}
}
}
console.log('新算法线程启动', pm.goodsId, this.workerList)
}
}
/** 停止优化 */
async stopPlaceTask() {
for (const key in this.workerList) {
await this.terminateWorker({ key })
}
}
/**
* 获取加工面
* @param block
* @param processMode
* @returns
*/
getDoFace(block: PlaceBlock, processMode: number = 0): boolean {
// 模式0: 开料机处理排钻, 造型, 采用大孔面作为正面的模式. 正面多加工, cnc多加工
if (processMode == 0) {
// 先考虑 设计开料面
if (block.placeHole == HoleArrange.FRONT)
return true
if (block.placeHole == HoleArrange.BACK)
return false
// 造型单面 作为开料面
if (block.modelCountFront() > 0 && block.modelCountBack() == 0)
return true
if (block.modelCountFront() == 0 && block.modelCountBack() > 0)
return false
// 优先考虑 大孔面 多孔面
return this.getHoleFaceMore(block)
} else {
return false
}
}
processMode() {
let processMode = 0;
if (this.isCutProcess) processMode = 0;
if (this.isCutAndCNCProcess) processMode = 1;
if (this.isCustomized) processMode = 2;
return processMode;
}
/**
* 正纹 = 0, 可翻转 = 1, 反纹 = 2;
* 正文 Positive = 0, 反纹 Reverse = 1, 可翻转 CanReversal = 2,
*/
toLine(texture): LineType {
if (texture == 0)
return LineType.Positive
if (texture == 1)
return LineType.CanReversal
return LineType.Reverse
}
/** 小板加工面Positive=0正面 Reverse=1反面 Arbitrary=2任意 */
toface(block: PlaceBlock): ComposingType {
let turnF = block.isTurnFaceToPlace
if (this.isTurnFaceToPlace)
turnF = !turnF
if (!turnF)
return ComposingType.Positive
return ComposingType.Reverse
}
}

2762
samples/confClass.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import { Processor, ProcessorCollection, ProcessorModule } from "../src/device";
export class ProcessorManager<T, R> implements ProcessorCollection<T, R> {
private processors = new Map<string, Processor<T, R>>();
private currentProcessor?: Processor<T, R>;
constructor(){
}
/** 注册模块流程 */
registerProcessor(name: string, processor: Processor<T, R>): this {
this.processors.set(name, processor);
return this;
}
/** 使用处理器 */
useProcessor(name: string): Processor<T, R> {
const processor = this.processors.get(name);
if (!processor) {
throw new Error(`Processor ${name} not found`);
}
this.currentProcessor = processor;
return processor;
}
/** 获取处理器 */
getProcessor(name: string): Processor<T, R> | undefined {
return this.processors.get(name);
}
/** 获取正在使用的处理器 */
getCurrentProcessor(): Processor<T, R> | undefined {
return this.currentProcessor;
}
}

View File

@@ -0,0 +1,75 @@
import { ProcessorModule, StepControllerProcessor } from "../../src/device";
import { RectOptimizeMachineModule } from "../moduleManager/module1";
/**
* demo 开料机处理器
*
*
*/
export class demoHandleGroupCutting {
processorName = "cutting"
processor: StepControllerProcessor<any, any>
constructor() {
const callbackStyleModule: ProcessorModule<any, any> = {
moduleName: "callbackStyle",
process(input, next, context) {
console.log("做优化");
const _input = input
const _next = next
const _context = context
// 可以在这里调用异步操作
Reflect.set(context, 'CallBack', callBack1)
// 决定是否调用 next
function callBack1(v) {
console.log('接收到其它模块回传的数据', v);
}
// 调用 next 继续流程
return next(input);
}
};
const demoModule: ProcessorModule<string, string> = {
moduleName: "demoModule",
process(input, next, context) {
// 写入上下文
context.processedAt = new Date().toLocaleString();
context.originalLength = input.length;
// 设置下一步需要的上下文
context.previousStep = "demoModule";
if (context.CallBack) {
context.CallBack("demoModule end and callback")
}
return next(input);
}
};
this.processor = new StepControllerProcessor<any, any>();
this.processor.use([
{
moduleName: "traditional",
handle(input, next) {
// 第一个流程
console.log(`第一个模块功能:有${input?.blockList.length}片小板,可以做些计算`)
return next ? next(input) : input;
}
},
callbackStyleModule,
demoModule,
RectOptimizeMachineModule,
{
moduleName: "final",
process(input, next) {
// 不调用 next终止流程
console.log('结束了')
return next(input);
}
}
])
}
}

View File

@@ -0,0 +1,18 @@
import { DeviceBase } from "../../src/device";
import { ConfigBase } from "../../src/models/config";
import { BusinessRectOptimizeMachine } from "../businessCapability/businessRectOptimizeMachine";
/**
* 某个处理器 一个 功能 和 业务功能的集合
* 可以包含多个业务功能和 基础功能
*
*
*/
export class demoHandleGroup extends DeviceBase {
constructor(){
super()
}
}

112
samples/demoKnives.ts Normal file
View File

@@ -0,0 +1,112 @@
import { _knifeType, Knife } from "./confClass"
/** demo 刀具数据 */
export const knifeData :Knife= {
"isEnabled": true,
"axleId": 2,
"knifeId": 2,
"knifeName": "T1",
"knifeType": 3,
"ability": [
5
],
"diameter": 5,
"length": 20,
"sawThiness": 7,
"sawDirection": 2,
"processDirection": 4,
"speed": 0,
"stepDepth": 0,
"offsetX": 0,
"offsetY": 0,
"offsetZ": 0,
"baseX": 0,
"baseY": 0,
"isModularDrill": false,
"isPreStartEnabled": false,
"preStartAheadActionCount": 5,
"isPreStartToolChangeDelay": false,
"preStartToolChangeDelayCode": "",
"isAxisStartCodePostpost": false,
"isAxisStopCodePrepose": false,
"drillGroupCode": "",
"axisStartCode": "M03 S18000\n",
"knifeStartCode": `M06 T1\nG43 H1\n`,
"drillGroupStartCode": "T1",
"drillGroupEndCode": "",
"knifeStopCode": "",
"axisStopCode": "M05\n",
}
export const knifeData1:_knifeType = {
"isEnabled": true,
"axleId": 2,
"knifeId": 2,
"knifeName": "T2",
"knifeType": 3,
"ability": [
5
],
"diameter": 6,
"length": 20,
"sawThiness": 7,
"sawDirection": 2,
"processDirection": 4,
"speed": 0,
"stepDepth": 0,
"offsetX": 0,
"offsetY": 0,
"offsetZ": 0,
"baseX": 0,
"baseY": 0,
"isModularDrill": false,
"isPreStartEnabled": false,
"preStartAheadActionCount": 5,
"isPreStartToolChangeDelay": false,
"preStartToolChangeDelayCode": "",
"isAxisStartCodePostpost": false,
"isAxisStopCodePrepose": false,
"drillGroupCode": "",
"axisStartCode": "M03 S18000\n",
"knifeStartCode": `M06 T2\nG43 H2\n`,
"drillGroupStartCode": "T2",
"drillGroupEndCode": "",
"knifeStopCode": "",
"axisStopCode": "M05\n",
}
export const knifeData2:_knifeType = {
"isEnabled": true,
"axleId": 2,
"knifeId": 2,
"knifeName": "T3",
"knifeType": 3,
"ability": [
5
],
"diameter": 6,
"length": 20,
"sawThiness": 7,
"sawDirection": 2,
"processDirection": 4,
"speed": 0,
"stepDepth": 0,
"offsetX": 0,
"offsetY": 0,
"offsetZ": 0,
"baseX": 0,
"baseY": 0,
"isModularDrill": false,
"isPreStartEnabled": false,
"preStartAheadActionCount": 5,
"isPreStartToolChangeDelay": false,
"preStartToolChangeDelayCode": "",
"isAxisStartCodePostpost": false,
"isAxisStopCodePrepose": false,
"drillGroupCode": "",
"axisStartCode": "M03 S18000\n",
"knifeStartCode": `M06 T2\nG43 H2\n`,
"drillGroupStartCode": "T3",
"drillGroupEndCode": "",
"knifeStopCode": "",
"axisStopCode": "M05\n",
}

126
samples/demoParser.ts Normal file
View File

@@ -0,0 +1,126 @@
import { ConfigBase } from "../src/base";
import { ParserBase } from "../src/parsers";
import { _knifeType, CodeParams } from "./confClass";
import { knifeData, knifeData1, knifeData2 } from "./demoKnives";
import { GCodeAction } from "./gcodes";
export class DemoParser extends ParserBase {
usedKnife?:_knifeType
knifeList:_knifeType[] = [
knifeData,
knifeData1,
knifeData2
]
constructor(config?: Record<string, ConfigBase>) {
super();
// if(config){
// if(config['knives']){
//
// }
// }
const gcodeActions = new GCodeAction();
this.codeManager['FSTART'] = {
name: 'FileStart',
// type:'Global',
// paramType:'array',
exec:(params) =>{
return `G54 G90\nM03\n`
}
};
this.codeManager['FEND'] = {
name: 'FileEnd',
exec:(params) =>{
return `M30\n`;
}
}
this.codeManager['G0'] = {
name: 'G0',
paramType: 'kv:number',
exec:(params: CodeParams) => {
return gcodeActions.G0(params) + '\n';
}
}
this.codeManager['G2'] = {
name: 'G2',
paramType: 'kv:number',
exec:(params: CodeParams)=> {
return gcodeActions.G2(params) + '\n';
}
}
this.codeManager['G3'] = {
name: 'G3',
paramType: 'kv:number',
exec:(params: CodeParams)=> {
return gcodeActions.G3(params) + '\n';
}
}
this.codeManager['TD'] = {
name: '',
exec:(diameter:number)=> {
let code = ''
let _knife: any
if (diameter) {
_knife = this.knifeList?.find(e => e.diameter == diameter)
}
if (_knife != undefined) {
code += this.checkChangeKnife(_knife)
this.usedKnife = _knife
// code += _knife.axisStartCode
code += _knife.knifeStartCode
}
return code
}
}
this.codeManager['TN'] = {
name: 'TN',
exec:(knifeName:string) =>{ /**根据刀名称找刀 换刀 */
let code = ''
let _knife: any = null
if (knifeName) {
_knife = this.knifeList?.find(e => e.knifeName == knifeName)
}
if (_knife != undefined) {
code += this.checkChangeKnife(_knife)
this.usedKnife = _knife
// code += _knife.axisStartCode
code += _knife.knifeStartCode
}
return code
}
}
this.codeManager['TE'] = {
name:'TE',
exec:()=>{
if(!this.usedKnife) return '';
return this.usedKnife.knifeStopCode+'\n';
}
}
}
/** 内部功能逻辑 换刀时判断是否需要 上一把刀的停止代码 */
private checkChangeKnife(knife: _knifeType) {
let code = '';
/** true 需要 输出停刀代码 false 不需要 */
let flag = false
// 必须要有刀
if (this.usedKnife) {
let keys = Object.keys(knife)
for (const key of keys) {
if (knife[key] != this.usedKnife[key]) {
flag = true
continue
}
}
}
if (flag) {
code += this.exec('TE',[]);
}
return code;
}
}

528
samples/gcodes.ts Normal file
View File

@@ -0,0 +1,528 @@
import { CodeParams } from "./confClass"
function checkVal(val) {
let r = true
if ((val == undefined || val == '')) {
r = false
}
return r
}
/**G代码指令
* 目前 只需要做 G0 - G3
*/
export enum GCode {
/**快速定位 空程移动 范例G00 X100 Y50刀具快速移动至X100、Y50位置 */
G0 = 'G0',
/**快速定位 空程移动 范例G00 X100 Y50刀具快速移动至X100、Y50位置*/
G00 = 'G00',
/**直线 直线插补 范例G01 X200 Y300 F150刀具以150mm/min的进给速度直线移动至X200、Y300*/
G1 = 'G1',
/**直线 范例G01 X200 Y300 F150刀具以150mm/min的进给速度直线移动至X200、Y300*/
G01 = 'G01',
/** 顺时针 范例G17 G02 X150 Y50 I50 J0 F100在XY平面以半径50mm顺时针圆弧插补至X150、Y50*/
G2 = 'G2',
/** 顺时针 范例G17 G02 X150 Y50 I50 J0 F100在XY平面以半径50mm顺时针圆弧插补至X150、Y50*/
G02 = 'G02',
/** 逆时针 范例G17 G03 X150 Y50 I50 J0 F100在XY平面以半径50mm逆时针圆弧插补至X150、Y50*/
G3 = 'G3',
/** 逆时针 范例G17 G03 X150 Y50 I50 J0 F100在XY平面以半径50mm逆时针圆弧插补至X150、Y50*/
G03 = 'G03',
/**暂停 范例G04 X2暂停2秒*/
G4 = 'G4',
/**暂停 范例G04 X2暂停2秒*/
G04 = 'G04',
/** 通过中间点圆弧插补
* 1. 通过中间点圆弧插补
* 示例G05 X60 Z50 IX50 IZ60 F120 刀具从当前位置出发,经过中间点 (50, 60),最终到达终点 (60, 50),形成圆弧轨迹。
* 2. 高精轨迹控制HPCC模式
* 功能G05 可启动高精轨迹控制模式HPCC通过曲线拟合和参数优化实现高精度、高速度的轨迹加工适用于复杂曲面或高精度零件。
* 指令格式:
* 启动G05 P10000P值设为10000
* 关闭G05 P0P值设为0
* 3、注意事项
* 轴类型限制建议线性轴参与HPCC模式旋转轴需谨慎设置。
与其他功能冲突:
在HPCC模式下G61准确停止检查、G63攻螺纹等功能可能失效需退出HPCC模式后恢复。
启用 RTCP旋转刀具中心点或 STCP 模式时禁止同时启用HPCC功能。
不支持的功能:
不支持单节停止C40/M00、反向手轮模拟、图形模拟等。
剩余距离显示可能不准确,因显示的是离曲线终点的距离,而非单节终点。
* */
G5 = 'G5',
/** 通过中间点圆弧插补 */
G05 = 'G05',
/** 抛物线插补 */
G6 = 'G6',
/** 抛物线插补 */
G06 = 'G06',
/** z样条曲线插补 */
G7 = 'G7',
/** z样条曲线插补 */
G07 = 'G07',
/** 进给加速 */
G8 = 'G8',
/** 进给加速 */
G08 = 'G08',
/** 进给减速 */
G9 = 'G9',
/** 进给减速 */
G09 = 'G09',
/** 参数写入方式有效 */
G10 = 'G10',
/** 参数写入方式取消 */
G11 = 'G11',
/** 极坐标变成 */
G16 = 'G16',
/** XY平面选择 */
G17 = 'G17',
/** XZ平面选择 */
G18 = 'G18',
/** YZ平面选择 */
G19 = 'G19',
/** 英制输入 */
G20 = 'G20',
/** 公制输入 */
G21 = 'G21',
/** 半径尺寸编程方式 */
G22 = 'G22',
/** 系统操作界面上使用 */
G220 = 'G220',
/** 直径尺寸编程方式 */
G23 = 'G23',
/** 系统操作界面上使用 */
G230 = 'G230',
/** 子程序结束 */
G24 = 'G24',
/** 跳转加工 */
G25 = 'G25',
/** 循环加工 */
G26 = 'G26',
/** 参考点返回 */
G28 = 'G28',
/** 倍率注销 */
G30 = 'G30',
/** 倍率定义 */
G31 = 'G31',
/** 等螺距螺纹切削,英制 */
G32 = 'G32',
/** 等螺距螺纹切削,公制 */
G33 = 'G33',
/** 增螺距螺纹切削 */
G34 = 'G34',
/** 减螺距螺纹切削 */
G35 = 'G35',
//** 刀具半径补正取消 */
G40 = 'G40',
//** 刀具半径补正 左*/
G41 = 'G41',
//** 刀具半径补正 右*/
G42 = 'G42',
//** 刀具长度补正+ */
G43 = 'G43',
//** 刀具长度补正- */
G44 = 'G44',
//** 道具偏置+/+ */
G45 = 'G45',
//** 道具偏置+/- */
G46 = 'G46',
//** 道具偏置-/- */
G47 = 'G47',
//** 道具偏置-/+ */
G48 = 'G48',
//** 刀具长度 补正取消 */
G49 = 'G49',
//** 局部坐标系设定 */
G52 = 'G52',
//** 机床坐标系选择 */
G53 = 'G53',
//** 工件坐标系选择1 */
G54 = 'G54',
//** 工件坐标系选择2 */
G55 = 'G55',
//** 工件坐标系选择3 */
G56 = 'G56',
//** 工件坐标系选择4 */
G57 = 'G57',
//** 工件坐标系选择5 */
G58 = 'G58',
//** 工件坐标系选择6 */
G59 = 'G59',
//** 坐标系旋转有效 */
G68 = 'G68',
//** 坐标系旋转取消 */
G69 = 'G69',
/** 高速深孔钻 */
G73 = 'G73',
/** 精搪孔 */
G76 = 'G76',
/** 固定循环取消/取消循环指令 */
G80 = 'G80',
/**程序停止 */
M00 = 'M00',
/**选择性停止 */
M01 = 'M01',
/** 程序结束 */
M02 = 'M02',
/**主轴正转 */
M03 = 'M03',
/**主轴反转 */
M04 = 'M04',
/**主轴停止 */
M05 = 'M05',
/**自动换刀 */
M06 = 'M06',
}
/**G代码基类 */
export class GCodeAction {
/** 空程直线 */
G0(params: CodeParams) {
const { x, y, z, f, xKey, yKey, zKey, fKey, codeKey } = params
let val: string = GCode.G0
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 空程直线 */
G00(params: CodeParams) {
const { x, y, z, f, xKey, yKey, zKey, fKey, codeKey } = params
let val: string = GCode.G00
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 直线 */
G1(params: CodeParams) {
const { x, y, z, f, xKey, yKey, zKey, fKey, codeKey } = params
let val: string = GCode.G1
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 直线 */
G01(params: CodeParams) {
const { x, y, z, f, xKey, yKey, zKey, fKey, codeKey } = params
let val: string = GCode.G01
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 顺时针 弧线
* @param x x坐标
* @param y y坐标
* @param z z坐标
* @param i 圆弧 对于起点的偏移量 x
* @param y 圆弧 对于起点的偏移量 Y
* @param z 圆弧 对于起点的偏移量 Y
* @param f 速度
*/
G2(params: CodeParams) {
const { x, y, z, i, j, k, r, f, xKey, yKey, zKey, fKey, iKey, jKey, kKey, codeKey } = params
let val: string = GCode.G2
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
let _iKey = iKey || 'I'
let _jKey = jKey || 'J'
let _kKey = kKey || 'K'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(r)) {
val += ` R${r}`
} else {
if (checkVal(i)) {
val += ` ${_iKey}${i}`
}
if (checkVal(j)) {
val += ` ${_jKey}${j}`
}
if (checkVal(k)) {
val += ` ${_kKey}${k}`
}
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 顺时针 弧线
* @param x x坐标
* @param y y坐标
* @param z z坐标
* @param i 圆弧 对于起点的偏移量 x
* @param y 圆弧 对于起点的偏移量 Y
* @param z 圆弧 对于起点的偏移量 Y
* @param f 速度
*/
G02(params: CodeParams) {
const { x, y, z, i, j, k, r, f, xKey, yKey, zKey, fKey, iKey, jKey, kKey, codeKey } = params
let val: string = GCode.G02
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
let _iKey = iKey || 'I'
let _jKey = jKey || 'J'
let _kKey = kKey || 'K'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(r)) {
val += ` R${r}`
} else {
if (checkVal(i)) {
val += ` ${_iKey}${i}`
}
if (checkVal(j)) {
val += ` ${_jKey}${j}`
}
if (checkVal(k)) {
val += ` ${_kKey}${k}`
}
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 逆时针 弧线
* @param x x坐标
* @param y y坐标
* @param z z坐标
* @param i 圆弧 对于起点的偏移量 x
* @param y 圆弧 对于起点的偏移量 Y
* @param z 圆弧 对于起点的偏移量 Y
* @param f 速度
*/
G3(params: CodeParams) {
const { x, y, z, i, j, k, r, f, xKey, yKey, zKey, fKey, iKey, jKey, kKey, codeKey } = params
let val: string = GCode.G3
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
let _iKey = iKey || 'I'
let _jKey = jKey || 'J'
let _kKey = kKey || 'K'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(r)) {
val += ` R${r}`
} else {
if (checkVal(i)) {
val += ` ${_iKey}${i}`
}
if (checkVal(j)) {
val += ` ${_jKey}${j}`
}
if (checkVal(k)) {
val += ` ${_kKey}${k}`
}
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 逆时针 弧线
* @param x x坐标
* @param y y坐标
* @param z z坐标
* @param i 圆弧 对于起点的偏移量 x
* @param y 圆弧 对于起点的偏移量 Y
* @param z 圆弧 对于起点的偏移量 Y
* @param f 速度
*/
G03(params: CodeParams) {
const { x, y, z, i, j, k, r, f, xKey, yKey, zKey, fKey, iKey, jKey, kKey, codeKey } = params
let val: string = GCode.G03
if (typeof (codeKey) == 'string' && codeKey != '') {
val = codeKey
}
let _xkey = xKey || 'X'
let _yKey = yKey || 'Y'
let _zKey = zKey || 'Z'
let _fKey = fKey || 'F'
let _iKey = iKey || 'I'
let _jKey = jKey || 'J'
let _kKey = kKey || 'K'
if (checkVal(x)) {
val += ` ${_xkey}${x}`
}
if (checkVal(y)) {
val += ` ${_yKey}${y}`
}
if (checkVal(z)) {
val += ` ${_zKey}${z}`
}
if (checkVal(r)) {
val += ` R${r}`
} else {
if (checkVal(i)) {
val += ` ${_iKey}${i}`
}
if (checkVal(j)) {
val += ` ${_jKey}${j}`
}
if (checkVal(k)) {
val += ` ${_kKey}${k}`
}
}
if (checkVal(f)) {
val += ` ${_fKey}${f}`
}
return val
}
/** 主轴正转
*
* @param s 转速
*/
M03(s: any) {
let val: string = GCode.M03
if (checkVal(s)) {
val += ` S${s}`
}
return val
}
/**主轴反向转
* @param s 转速
*/
M04(s: any) {
let val: string = GCode.M04
if (checkVal(s)) {
val += ` S${s}`
}
return val
}
/**主轴停止 */
M05() {
let val: string = GCode.M05
return val
}
/**换刀指令 */
M06(t: String) {
let val: string = GCode.M06
if (checkVal(t)) {
val += ` T${t}`
}
return val
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
import type { Big_bang, xbang } from './bang'
import { RectOptimizeMachine } from './RectOptimizeMachine'
import {Worker} from "worker_threads"
const ctx: Worker = self as any
ctx.addListener('message', async (event) => {
let m = new RectOptimizeMachine()
m.CallBack = async (best, fit, arg, info) => {
ctx.postMessage([best, fit, arg, info])
}
if (event.data.type == 'start') {
/**
* blockList 小板列表
* boardList 大板(N个元素前N-1个元素表示余料板且余料板须为矩形第N个元素表示大板)
* boardCount 余料板数量(bigBang中前N-1个元素对应的数量如果bigBang中只有一个元素即只有大板没有余料板则为空数组)
* optimizeTimes 新优化次数
* isDoubleFaceBlockFirst 双面加工的小板是否优先排入
* gap 排版缝隙 = 开料刀直径 + 缝隙
* gzpb 规则排版
* isDoubleFaceBlockInRemain 余料板是否排入双面加工的小板
*/
let [blockList, boardList, boardCount, optimizeTimes, isDoubleFaceBlockFirst, gap, gzpb, isDoubleFaceBlockInRemain] = (event.data.data) as [xbang[], Big_bang[], number[], number, boolean, number, boolean, boolean]
m.Start(blockList, boardList, boardCount, optimizeTimes, isDoubleFaceBlockFirst, gap, gzpb, isDoubleFaceBlockInRemain)
} else {
const info = {
type: 'isStop',
}
await m.Stop(info)
ctx.postMessage([[], null, null, info])
ctx?.terminate()
}
})
// ctx.addEventListener('message', async (event) => {
// let m = new RectOptimizeMachine()
// m.CallBack = async (best, fit, arg, info) => {
// ctx.postMessage([best, fit, arg, info])
// }
// if (event.data.type == 'start') {
// /**
// * blockList 小板列表
// * boardList 大板(N个元素前N-1个元素表示余料板且余料板须为矩形第N个元素表示大板)
// * boardCount 余料板数量(bigBang中前N-1个元素对应的数量如果bigBang中只有一个元素即只有大板没有余料板则为空数组)
// * optimizeTimes 新优化次数
// * isDoubleFaceBlockFirst 双面加工的小板是否优先排入
// * gap 排版缝隙 = 开料刀直径 + 缝隙
// * gzpb 规则排版
// * isDoubleFaceBlockInRemain 余料板是否排入双面加工的小板
// */
// let [blockList, boardList, boardCount, optimizeTimes, isDoubleFaceBlockFirst, gap, gzpb, isDoubleFaceBlockInRemain] = (event.data.data) as [xbang[], Big_bang[], number[], number, boolean, number, boolean, boolean]
// m.Start(blockList, boardList, boardCount, optimizeTimes, isDoubleFaceBlockFirst, gap, gzpb, isDoubleFaceBlockInRemain)
// } else {
// const info = {
// type: 'isStop',
// }
// await m.Stop(info)
// ctx.postMessage([[], null, null, info])
// ctx?.terminate()
// }
// })
export default {} as typeof Worker & (new () => Worker)

View File

@@ -0,0 +1,115 @@
export type Con = YH_bang[];//单块1220*2440的板结果 Container
export type Inv = Con[]; //个体:优化结果 包含多个大板 Individual
/**纹路类型 Positive=0正纹 Reverse=1反纹 CanReversal=2可翻转 */
export enum LineType
{
/**正纹 */
Positive = 0,
/**反纹 */
Reverse = 1,
/**可翻转 */
CanReversal = 2,
}
/**优化的大板 */
export interface YH_bang
{
/**板ID */
bangid: number;
/**纹路 */
line: LineType;
x: number;
y: number;
/**排版高 */
pbg: number;
/**排版宽 */
pbk: number;
ishb?: boolean;//是否参与合并的板
hb?: number[]; //合在并的板
isgr?: boolean; //是否关连
gr?: number[];//关联的板的集合
grid?: number; //跟别的板关联的ID
isbig?: boolean;//是否为合并的大板
isqg?: boolean;//是否被切掉的板
}
/**版面类型: Positive=0正面 Reverse=1反面 Arbitrary=2任意面 */
export enum ComposingType
{
/**正面 */
Positive = 0,
/**反面 */
Reverse = 1,
/**任意面 */
Arbitrary = 2
}
/**孔类型 None=0无 Positive=1正面 Reverse=2反面 Two=3正反 */
export enum HoleType
{
/**无 */
None = 0,
/**正面 */
Positive = 1,
/**反面 */
Reverse = 2,
/**正反 */
Two = 3
}
/** 小板 */
export interface xbang
{
/**长 */
l: number;
/**宽 */
w: number;
/**纹路 */
line: LineType;
/**排版面 */
face: ComposingType;
/**小板ID */
id: number;
/**小板编号 */
bno: string;
/**孔面: 0无孔 1正面有孔 2反面有孔 3正反面都有孔 */
holeFaceCount: HoleType;
/**是矩形 */
isRect?: boolean;
/**有孔 */
hasHole?: boolean;
/**false单面 true双面 */
isdtwosided?: boolean;
}
/** 大板 */
export interface Big_bang //待优化的板
{
l: number; //长
w: number; //宽
x: number; //x
y: number; //y
}
export enum BlockRegion
{
/** 左下 = 0 */
LEFT_BOTTOM = 0,
/** 右下 = 1 */
RIGHT_BOTTOM = 1,
/** 右上 = 2 */
RIGHT_TOP = 2,
/** 左上 = 3 */
LEFT_TOP = 3,
}
export class WorkerItemType {
w?: Worker
goodsId?: string | number
pm?: any
status?: 'start' | 'stop'
}

View File

@@ -0,0 +1 @@
# 新优化

View File

@@ -0,0 +1,13 @@
import { ConfigBase } from "../../src/models/config";
/** 基础功能1 -- 新优化 */
export class BlockPlace extends ConfigBase{
// 优化过程中的回调函数
callBack?:Function
constructor(){
super()
this.name = 'XXX'
this.version = '20250707'
}
}

View File

@@ -0,0 +1,132 @@
import { FaceType, PlaceBlock, PlaceBlockDetail, PlaceBorderContour, PlaceMaterial, PlaceStyle, SizeExpand } from "../confClass"
import { Arc2d, Curve2d, Line2d } from "./common/base/CAD"
import { PolylineHelper } from "./common/LayoutEngine/PolylineHelper"
import { KnifeHelper } from "./knifeHelper"
/** 小板明细 相关的计算 孔 造型 以及轮廓 */
export class BlockDetailHelperBase {
/** 造型轮廓(含封边),扣除封边, 变成开料坐标 */
resetModelContour(bd: PlaceBlockDetail) {
let ox = bd.offsetX
let oy = bd.offsetY
for (let m of bd.models) {
if (m.hasContour()) {
let ptsArr = m.originModeling.outline.map(e => e.pts)
for (let pt of ptsArr) {
// 23.8.5 发现矩形的挖穿轮廓坐标是不含封边的
pt.x -= ox
pt.y -= oy
}
}
}
}
// 原始轮廓,成品轮廓
createFinalBorder(bd: PlaceBlockDetail, block: PlaceBlock): Curve2d[] {
const orgPoints = bd.orgPoints
const orgBorderCurveList = new Array<Curve2d>()
if (orgPoints && orgPoints.length > 1) // 异形
{
const count = orgPoints.length
for (let i = 0; i < count; i++) {
const p0 = orgPoints[i]
const p1 = i == count - 1 ? orgPoints[0] : orgPoints[i + 1]
const sideHoleCount = bd.holeListSide.filter(t => t.faceId == i).length
if (p0.curve == 0) // 直线
{
const newLine = Line2d.New(p0.pointX, p0.pointY, p1.pointX, p1.pointY)
newLine.tagData = p0.sealSize
newLine.tagData2 = sideHoleCount
orgBorderCurveList.push(newLine)
p0.radius = 0
}
else // 曲线
{
const crc = Arc2d.New(p0.pointX, p0.pointY, p1.pointX, p1.pointY, p0.curve)
crc.tagData = p0.sealSize
crc.tagData2 = sideHoleCount
p0.radius = crc.m_Radius
orgBorderCurveList.push(crc)
}
}
}
else // 矩形板
{
const w = block.width
const l = block.length
const line0 = Line2d.New(0, 0, w, 0)
line0.tagData = block.sealBottom
line0.tagData2 = block.holeCountBottom() || 0
const line1 = Line2d.New(w, 0, w, l)
line1.tagData = block.sealRight
line1.tagData2 = block.holeCountRight() || 0
const line2 = Line2d.New(w, l, 0, l)
line2.tagData = block.sealTop
line2.tagData2 = block.holeCountTop() || 0
const line3 = Line2d.New(0, l, 0, 0)
line3.tagData = block.sealLeft
line3.tagData2 = block.holeCountLeft() || 0
orgBorderCurveList.push(line0)
orgBorderCurveList.push(line1)
orgBorderCurveList.push(line2)
orgBorderCurveList.push(line3)
}
return orgBorderCurveList
}
/** 创建 开料轮廓不含预铣 */
createOrgBorder(bd: PlaceBlockDetail): Curve2d[] {
const borders = new Array<Curve2d>()
if (bd.points && bd.points.length > 1) // 异形
{
const count = bd.points.length
for (let i = 0; i < count - 1; i++) // 异形点(无封边,起点终点 是重复的)
{
const p0 = bd.points[i]
const p1 = i == count - 1 ? bd.points[0] : bd.points[i + 1]
const sideHoleCount = bd.holeListSide.filter(t => t.faceId == i).length
if (p0.curve == 0) // 直线
{
const newLine = Line2d.New(p0.pointX, p0.pointY, p1.pointX, p1.pointY)
newLine.tagData = p0.sealSize
newLine.tagData2 = sideHoleCount
borders.push(newLine)
p0.radius = 0
}
else // 曲线
{
const crc = Arc2d.New(p0.pointX, p0.pointY, p1.pointX, p1.pointY, p0.curve)
crc.tagData = p0.sealSize
crc.tagData2 = sideHoleCount
p0.radius = crc.m_Radius
if (p0.radius < 2) {
p0.curve = 0
p0.radius = 0
}
borders.push(crc)
}
}
}
else // 矩形板
{
const w = bd.cutWidth
const l = bd.cutLength
const line0 = Line2d.New(0, 0, w, 0)
const line1 = Line2d.New(w, 0, w, l)
const line2 = Line2d.New(w, l, 0, l)
const line3 = Line2d.New(0, l, 0, 0)
borders.push(line0)
borders.push(line1)
borders.push(line2)
borders.push(line3)
}
return borders
}
}

View File

@@ -0,0 +1,4 @@
/** 小板相关的计算 */
export class BlockHelper{
}

View File

@@ -0,0 +1,392 @@
export class List<Item> extends Array<Item>
{
/** 返回符合条件的第一个元素 */
first(fn: (item: Item) => boolean): Item
{
if (this.length == 0)
return null
for (const item of this)
{
if (fn(item))
return item
}
return null
}
/** 返回符合条件的最后一元素 */
last(fn: (t: Item) => boolean): Item
{
if (this.length == 0)
return null
for (let i = this.length - 1; i >= 0; i--)
{
if (fn(this[i]))
return this[i]
}
return null
}
/** 最大值 */
max<T>(fn: (item: Item) => T): T
{
let maxV: T
for (const i of this)
{
let v = fn(i)
if (maxV == null)
{
maxV = fn(i)
}
else
{
if (v > maxV)
{
maxV = v
}
}
}
return maxV
}
/** 最小值 */
min<T>(fn: (item: Item) => T): T
{
let minV: T
for (const i of this)
{
let v = fn(i)
if (minV == null)
{
minV = fn(i)
}
else
{
if (v < minV)
{
minV = v
}
}
}
return minV
}
/** 累加 */
sum(fn: (item: Item) => number): number
{
let v: number = 0
for (const t of this)
{
v = v + fn(t)
}
return v
}
/** 平均值 */
avg(fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let sum = this.sum(fn)
return sum / this.length
}
/** 满足条件的元素数量 */
count(fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let c = 0
for (const item of this)
{
if (fn(item))
c = c + 1
}
return c
}
FindMax<T>(fn: (item: Item) => T): Item
{
return this.reduce((a: Item, b: Item): Item => fn(a) > fn(b) ? a : b)
}
}
export class ArrayExt
{
/** 返回满足条件的元素数量 */
static count<Item>(list: Item[], fn: (item: Item) => boolean): number
{
if (list.length == 0)
return 0
let c = 0
for (const item of list)
{
if (fn(item))
c = c + 1
}
return c
}
/** 移除 */
static remove<Item>(list: Item[], obj: Item)
{
let index = list.findIndex(t => t == obj)
if (index == -1)
return
list.splice(index, 1)
}
/** 返回符合条件的第一个元素 */
static first<Item>(list: Item[], fn: (item: Item) => boolean, orderFn1: (item: Item) => number = null, orderFn2: (item: Item) => number = null): Item
{
if (list.length == 0)
return null
if (orderFn1 == null)
{
for (const item of list)
{
if (fn(item))
return item
}
return null
}
let minValue1: number
let minValue2: number
let minItem: Item
for (const item of list)
{
if (fn(item) == false)
continue
let v1 = orderFn1(item)
let v2 = orderFn2 != null ? orderFn2(item) : 0
if (minValue1 == null || v1 < minValue1 || (v1 == minValue1 && v2 < minValue2))
{
minValue1 = v1
minValue2 = v2
minItem = item
}
}
return minItem
}
/** 返回符合条件的最后一元素 */
static last<Item>(list: Item[], fn: (t: Item) => boolean, orderFn1: (item: Item) => number = null, orderFn2: (item: Item) => number = null): Item
{
if (list.length == 0)
return null
if (orderFn1 == null)
{
for (let i = list.length - 1; i >= 0; i--)
{
if (fn(list[i]))
return list[i]
}
return null
}
//
let maxValue1: number
let maxValue2: number
let maxItem: Item
for (const item of list)
{
if (fn(item) == false)
continue
let v1 = orderFn1(item)
let v2 = orderFn2 ? orderFn2(item) : 0
if (maxValue1 == null || v1 > maxValue1 || (v1 == maxValue1 && v2 > maxValue2))
{
maxValue1 = v1
maxValue2 = v2
maxItem = item
}
}
return maxItem
}
/** 取最大值 */
static max<T1, T2>(list: T1[], fn: (item: T1) => T2, whereF: (item: T1) => boolean = null, defaultV: T2 = null): T2
{
let maxV: T2
for (const i of list)
{
if (whereF && whereF(i) == false)
continue
let v = fn(i)
if (maxV == undefined)
{
maxV = fn(i)
}
else
{
if (v > maxV)
{
maxV = v
}
}
}
if (maxV != undefined)
return maxV
return defaultV
}
/** 最小值 */
static min<Item, T>(list: Item[], fn: (item: Item) => T, whereF: (item: Item) => boolean = null, defaultV: T = null): T
{
let minV: T
for (const i of list)
{
if (whereF && whereF(i) == false)
continue
let v = fn(i)
if (minV == undefined)
{
minV = v
}
else
{
if (v < minV)
{
minV = v
}
}
}
if (minV != undefined)
return minV
return defaultV
}
/** 累加 */
static sum<Item>(list: Item[], fn: (item: Item) => number, wn?: (item: Item) => boolean): number
{
let v: number = 0
for (const t of list)
{
if (wn && wn(t) == false)
continue
v = v + fn(t)
}
return v
}
/** 平均值 */
static avg<Item>(list: Item[], fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let sum = ArrayExt.sum(list, fn)
return sum / this.length
}
/** 排序 */
static sortBy<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null)
{
if (fn2 == null)
return list.sort((a: Item, b: Item): number => fn(a) - fn(b))
else
return list.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(a) - fn2(b)) : fn(a) - fn(b))
}
/** 降序 排序 */
static sortByDescending<Item>(list: Item[], fn: (item: Item) => number)
{
list.sort((a: Item, b: Item): number => fn(b) - fn(a))
}
/** 排序成新的数组 */
static orderBy<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null): Item[]
{
let newList = list.concat([])
if (fn2 == null)
return newList.sort((a: Item, b: Item): number => fn(a) - fn(b))
else
return newList.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(a) - fn2(b)) : fn(a) - fn(b))
}
/** 降序成新的数组 */
static orderByDescending<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null): Item[]
{
let newList = list.concat([])
if (fn2 == null)
return list.sort((a: Item, b: Item): number => fn(b) - fn(a))
else
return list.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(b) - fn2(a)) : fn(b) - fn(a))
}
/** 分组 */
static groupBy<Item, gT>(list: Item[], fn: (item: Item) => gT): GroupItem<gT, Item>[]
{
let groups = new Array<GroupItem<gT, Item>>()
for (const item of list)
{
let key = fn(item)
let group = groups.find(t => t.key == key)
if (group == null)
{
group = new GroupItem(key)
groups.push(group)
}
group.push(item)
}
return groups
}
/**
* 选择
* let newObjectList = ArrayExt.Select(list,t=>({pA:t.name, pB:t.age + "_"+ t.month}) ) ;
*/
static Select<Item, rT>(list: Item[], fn: (item: Item) => rT): rT[]
{
let newList = new Array<rT>()
for (const t of list)
{
newList.push(fn(t))
}
return newList
}
/** 过来,并按顺序排序 */
static where<Item>(list: Item[], whereFn: (item: Item) => boolean, orderfn1: (item: Item) => number = null, orderfn2: (item: Item) => number = null): Item[]
{
let newList = list.filter(whereFn)
if (orderfn1 == null && orderfn2 == null)
return newList
return ArrayExt.sortBy(newList, orderfn1, orderfn2)
}
}
export class GroupItem<gT, Item>
{
key: gT
get count() { return this.list.length }
list: Item[]
constructor(k: gT)
{
this.key = k
this.list = []
}
push(d: Item)
{
this.list.push(d)
}
}
/**
* 对排序好的数组进行去重操作
* @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
}

View File

@@ -0,0 +1,183 @@
import { Vector2 } from './Vector2.js'
import type { Point } from './Point.js'
export class Box2
{
min: Vector2
max: Vector2
constructor(min = new Vector2(+Number.POSITIVE_INFINITY, +Number.POSITIVE_INFINITY), max = new Vector2(Number.NEGATIVE_INFINITY, Number.NEGATIVE_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: Iterable<Point>): Box2
{
this.makeEmpty()
for (let p of points)
{
this.expandByPoint(p)
}
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 = +Number.POSITIVE_INFINITY
this.max.x = this.max.y = Number.NEGATIVE_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: Point): Box2
{
this.min.min(point)
this.max.max(point)
return this
}
expandByVector(vector: Vector2): Box2
{
this.min.sub(vector)
this.max.add(vector)
return this
}
expandByScalar(scalar: number): Box2
{
this.min.addScalar(-scalar)
this.max.addScalar(scalar)
return this
}
containsPoint(point: Vector2): boolean
{
if (point.x < this.min.x || point.x > this.max.x
|| point.y < this.min.y || point.y > this.max.y)
{
return false
}
return true
}
containsBox(box: Box2): boolean
{
if ((this.min.x <= box.min.x) && (box.max.x <= this.max.x)
&& (this.min.y <= box.min.y) && (box.max.y <= this.max.y))
{
return true
}
return false
}
getParameter(point: Vector2, result: Vector2 = new Vector2()): Vector2
{
// This can potentially have a divide by zero if the box
// has a size dimension of 0.
return result.set(
(point.x - this.min.x) / (this.max.x - this.min.x),
(point.y - this.min.y) / (this.max.y - this.min.y),
)
}
intersectsBox(box: Box2): boolean
{
// using 6 splitting planes to rule out intersections.
if (box.max.x < this.min.x || box.min.x > this.max.x
|| box.max.y < this.min.y || box.min.y > this.max.y)
{
return false
}
return true
}
clampPoint(point: Vector2, result: Vector2 = new Vector2()): Vector2
{
return result.copy(point).clamp(this.min, this.max)
}
private static _distanceToPoint_v1 = new Vector2()
distanceToPoint(point: Vector2): number
{
const v1 = Box2._distanceToPoint_v1
const clampedPoint = v1.copy(point).clamp(this.min, this.max)
return clampedPoint.sub(point).length()
}
intersect(box: Box2): Box2
{
this.min.max(box.min)
this.max.min(box.max)
return this
}
union(box: Box2): Box2
{
this.min.min(box.min)
this.max.max(box.max)
return this
}
translate(offset: Point): Box2
{
this.min.add(offset)
this.max.add(offset)
return this
}
equals(box: Box2): boolean
{
return box.min.equals(this.min) && box.max.equals(this.max)
}
};

View File

@@ -0,0 +1,25 @@
import * as clipperLib from 'js-angusj-clipper/web/index.js'
// nodejs style require
export const 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
clipperLib.NativeClipperLibRequestedFormat.WasmOnly,
).then((c) =>
{
clipperCpp.lib = c
res()
console.log('载入成功!')
})
})
}

View File

@@ -0,0 +1,31 @@
/**
* 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++]
}
}

View File

@@ -0,0 +1,255 @@
import type { PolylineProps } from 'cadapi'
import { Circle, Polyline, Polyline2Points } from 'cadapi'
import { EndType, JoinType } from 'js-angusj-clipper/web'
import type { Box3, Vector3 } from 'three'
import { Vector2 } from 'three'
import { clipperCpp } from '../ClipperCpp'
import type { Point } from '../Point'
import { Path, PathScale } from '../core/Path'
import type { IOffset } from './Simplify2'
import { SimplifyDouglasPeucker } from './Simplify2'
/** 内外接多边形 */
export function Circle2Points(circle: Circle, knifRadius: number, splitSize = 10, outside = false): Point[] {
let radius = circle.Radius
const an = Math.PI * 2 / splitSize
if (outside)
radius = radius / Math.cos(an / 2) + knifRadius
else
radius -= knifRadius
const cenP = circle.Center
const pts: Vector3[] = []
for (let i = 0; i < splitSize; i++)
pts.push(polar(cenP.clone(), an * i, radius))
return pts as Point[]
}
export function Curve2Path(curve: Polyline, outside = false): Path {
if (curve.IsClockWise)
curve.Reverse()
const w = new CurveWrap(curve, 3, outside)
return new Path(outside ? w.GetOutsidePoints() : w.GetInsidePoints())
}
export class CurveWrap {
BoundingBox: Box3
Area: number
SimplyPolyline: Polyline
SimplyOffset: IOffset
Used = false
Holes: CurveWrap[] = []
Points: Point[]
constructor(public Curve: Polyline | Circle, public KnifRadius: number, public IsOutside: boolean) {
this.BoundingBox = Curve.BoundingBox
if (Curve instanceof Polyline) {
const pts = Polyline2Points(Curve, IsOutside, 0)[1]
/**
* 精简算法SimplifyDouglasPeucker 会导致轮廓变大,
* 修改成直接取点 陈雄 QQ聊天记录 23.9.18
* 23.10.9 by lrx
*/
const [spts, offset] = SimplifyDouglasPeucker(pts, KnifRadius ** 2 + KnifRadius)
if (spts.length !== pts.length && spts.length > 2) {
this.SimplyOffset = offset
this.SimplyPolyline = Path2Polyline(spts)
this.Curve = this.SimplyPolyline// 保险起见,也更新它
this.Area = this.SimplyPolyline.Area
}
else// 此处更新多段线
{ this.Curve = Path2Polyline(pts) }
this.Points = spts
// 以下修改后的
// this.Curve = Path2Polyline(pts);
// this.Points = pts;
}
if (this.Area === undefined)
this.Area = this.Curve.Area
}
ContainsCurve(curve: CurveWrap): boolean {
if (this.SimplyPolyline)
return this.SimplyPolyline.PtInCurve(curve.Curve.StartPoint)
return this.Curve.PtInCurve(curve.Curve.StartPoint)
}
GetOutsidePoints(): Point[] {
if (this.Curve instanceof Circle) {
const pts = Circle2Points(this.Curve, this.KnifRadius, 10, true)
return pts
}
else {
const pl = this.SimplyPolyline || this.Curve
let offset = this.KnifRadius
if (this.SimplyOffset)
offset += this.SimplyOffset.positiveOffset
if (offset > 0) {
let pts = pl.GetStretchPoints() as Point[]
pts = clipperCpp.lib.offsetToPaths({
delta: offset * 1e4,
offsetInputs: [{ data: PathScale(pts, 1e4), joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
})[0]
try {
PathScale(pts, 1e-4)
}
catch {
console.log('err')
}
return pts
}
else { return this.Points }
}
}
GetInsidePoints(): Point[] {
return this.GetInsidePoints2(this.KnifRadius)
}
GetInsidePoints2(d: number): Point[] {
if (this.Curve instanceof Circle) {
const pts = Circle2Points(this.Curve, d, 10, false)
return pts
}
else {
const pl = this.SimplyPolyline || this.Curve
let offset = -d
if (this.SimplyOffset)
offset += this.SimplyOffset.negativeOffset
if (offset < -0.01) {
const pls = pl.GetOffsetCurves(offset)
if (pls.length)
return pls[0].GetStretchPoints()
}
else { return this.Points }
}
}
/** 引入Polyline 已经含刀半径, 获得精简后的点阵 */
GetOrgPoints(outside = true): Point[] {
if (this.Curve instanceof Circle) {
const pts = Circle2Points(this.Curve, 0, 10, outside)
return pts
}
else {
const pl = this.SimplyPolyline || this.Curve
let offset = 0
if (this.SimplyOffset)
offset += this.SimplyOffset.positiveOffset
if (offset > 0) {
let pts = pl.GetStretchPoints() as Point[]
pts = clipperCpp.lib.offsetToPaths({
delta: offset * 1e4,
offsetInputs: [{ data: PathScale(pts, 1e4), joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
})[0]
try {
PathScale(pts, 1e-4)
}
catch {
console.log('err')
}
return pts
}
else {
return this.Points
}
}
}
}
/** 多段线 转点整 已弃用整合到CAD api 23.11.2 */
// export function Polylin2Points(pl: Polyline, outside: boolean, knifRadius: number): [Polyline, Point[]]
// {
// let pts: Point[] = [];
// if (!outside) knifRadius = -knifRadius;
// if (pl.IsClockWise) pl.Reverse();
// for (let i = 0; i < pl.EndParam; i++)
// {
// pts.push(pl.GetPointAtParam(i));
// let bul = pl.GetBulgeAt(i);
// if (bul !== 0)
// {
// let arc = pl.GetCurveAtIndex(i) as Arc;
// let allAngle = arc.AllAngle;
// let arcLength = arc.Length;
// // let splitCount = Math.round(allAngle / 0.4);
// // if (arcLength < 300)
// // splitCount = 2;
// // else
// // splitCount = Math.max(arcLength / 200, splitCount,2);
// let minCount = Math.floor(allAngle * 4 / Math.PI);
// let splitCount = Math.round(allAngle / 0.4);
// if (arcLength < 300)
// splitCount = Math.max(2, minCount);
// else
// splitCount = Math.max(Math.floor(arcLength / 200), splitCount,2, minCount);
// let radius = arc.Radius;
// if (outside === bul > 0)
// radius = radius / Math.cos(allAngle / (splitCount * 2));
// let cp = arc.Center;
// for (let j = outside ? 0.5 : 0; j < splitCount; j++)
// {
// let a = arc.GetAngleAtParam(j * (1 / splitCount));
// let p = polar(cp.clone(), a, radius);
// pts.push(p);
// }
// }
// }
// if (knifRadius !== 0)
// {
// pts = clipperCpp.lib.offsetToPaths({
// delta: knifRadius * 1e4,
// offsetInputs: [{ data: PathScale(pts, 1e4), joinType: JoinType.Miter, endType: EndType.ClosedPolygon }]
// })[0];
// PathScale(pts, 1e-4);
// }
// return [pl, pts];
// }
export function Path2Polyline(path: Point[]): Polyline {
const pl = new Polyline()
pl.LineData = path.map((p) => {
return { pt: new Vector2(p.x, p.y), bul: 0 }
})
pl.CloseMark = true
return pl
}
export function Points2Polyline(pts: any[]): Polyline {
const lined: PolylineProps[] = []
const count = pts.length
for (let i = 0; i < count; i++) {
const p0 = pts[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: p0.bul })
}
const pls = new Polyline(lined)
pls.CloseMark = true
return pls
}
export function polar<T extends Vector2 | Vector3>(v: T, an: number, dis: number): T {
v.x += Math.cos(an) * dis
v.y += Math.sin(an) * dis
return v
}

View File

@@ -0,0 +1,369 @@
import type { PolylineProps } from 'cadapi'
import { CADFiler, Circle, Polyline, Status, VKnifToolPath, isTargetCurInOrOnSourceCur } from 'cadapi'
import type { Box3 } from 'three'
import { Vector2, Vector3 } from 'three'
import { arrayRemoveDuplicateBySort } from '../ArrayExt'
import type { Curve2d } from '../base/CAD'
import { Arc2d, Point2d, copyTextToClipboard } from '../base/CAD'
import { CurveWrap } from './Curves2Parts'
// import type { Curve2d } from '../../common/base/CAD'
export class PolylineHelper {
/** 创建闭合多段线 */
static create(pts: any[], closeMark = false): Polyline {
let lined: PolylineProps[] = []
let count = pts.length
for (let i = 0; i < count; i++) {
let p0 = pts[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: p0.bul || 0 })
}
let pls = new Polyline(lined)
pls.CloseMark = closeMark
return pls
}
static createByCurve2d(curs: Curve2d[], closeMark = true): Polyline {
let lined: PolylineProps[] = []
for (let cur of curs) {
let x = cur.StartPoint.m_X
let y = cur.StartPoint.m_Y
let bul = 0
if (cur instanceof Arc2d)
bul = cur.Bul || 0
lined.push({ pt: new Vector2(x, y), bul })
}
let pls = new Polyline(lined)
pls.CloseMark = true
return pls
}
static createByPts(pts: any[], buls: number[], closeMark = false): Polyline {
let plps: PolylineProps[] = []
let count = pts.length
for (let i = 0; i < count; i++) {
let p0 = pts[i]
plps.push({ pt: new Vector2(p0.x, p0.y), bul: buls[i] })
}
let pls = new Polyline(plps)
pls.CloseMark = closeMark
return pls
}
static getSimplePoints(pts: any[], offset: number): any[] {
let pl = PolylineHelper.create(pts)
pl.CloseMark = true
let cureW = new CurveWrap(pl, offset, true)
let pts2 = cureW.GetOutsidePoints()
arrayRemoveDuplicateBySort(pts2, (p1, p2) => (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) < 1e-2)
return pts2
}
static createByWidthLength(w: number, l: number): Polyline {
let plps: PolylineProps[] = []
plps.push({ pt: new Vector2(0, 0), bul: 0 })
plps.push({ pt: new Vector2(w, 0), bul: 0 })
plps.push({ pt: new Vector2(w, l), bul: 0 })
plps.push({ pt: new Vector2(0, l), bul: 0 })
let pls = new Polyline(plps)
pls.CloseMark = true
return pls
}
/** 多段线,添加位置移动 返回新多段线 */
static moveTo(pl: Polyline, x: number, y: number): Polyline {
let lindData = pl.LineData
let pos = pl.Position
let newPts: PolylineProps[] = []
for (let p of lindData) {
let nx = p.pt.x + pos.x + x
let ny = p.pt.y + pos.y + y
if (ny < 7.9) {
// console.log('修边小于 7.9????', ny)
}
let bul = p.bul
newPts.push({ pt: new Vector2(nx, ny), bul })
}
let npl = new Polyline(newPts)
npl.CloseMark = pl.CloseMark
return npl
}
/** 重设 多段线的几点 */
static resetPosition(pl: Polyline): Polyline {
let lindData = pl.LineData
let pos = pl.Position
let newPts: PolylineProps[] = []
for (let p of lindData) {
let nx = p.pt.x + pos.x
let ny = p.pt.y + pos.y
let bul = p.bul
newPts.push({ pt: new Vector2(nx, ny), bul })
}
let npl = new Polyline(newPts)
npl.CloseMark = pl.CloseMark
return npl
}
/** 获得v型刀走刀路径 */o
static getVModelPoints(pl: Polyline, depth: number, ang: number): any[] {
// let ang = Math.PI * (0.5 * angle) / 180 ;
let ps = []
let bx = pl.Position.x
let by = pl.Position.y
if (ang > 0.01) {
let rt = VKnifToolPath(pl, depth, ang / 2)
ps = rt.map((t) => { return { x: t.pt.x + bx, y: t.pt.y + by, z: t.pt.z, bul: t.bul, r: 0 } })
}
else {
ps = pl.LineData.map((t) => { return { x: t.pt.x + bx, y: t.pt.y + by, z: 0, bul: t.bul, r: 0 } })
}
for (let i = 0; i < ps.length; i++) {
let p = ps[i]
if (p.bul == 0)
continue
let p2 = (i == ps.length - 1 ? ps[0] : ps[i + 1])
let r = this.getArcRadius(p.x, p.y, p2.x, p2.y, p.bul)
p.r = r
}
return ps
}
static ConverPolyLin2Circle(polyline: Polyline, fuzz = 0.1): Circle | undefined {
let box = polyline.BoundingBox
let size = box.getSize(new Vector3())
if (!this.equaln(size.x, size.y, fuzz))// 盒子四方
return undefined
let circleLength = 2 * Math.PI * size.x
if (!this.equaln(circleLength, polyline.Length, fuzz * 2))
return undefined
let circleArea = Math.PI * size.x * size.x
if (!this.equaln(circleArea, polyline.Area, fuzz * 2))
return undefined
let r = size.x// 必须备份(因为我们要复用这个vector变量)
return new Circle(box.getCenter(size), r)
}
// 有问题
static getVModelPoints_offset(pl: Polyline, offset: number, depth: number, angle: number) {
let npl = offset == 0 ? pl : pl.GetOffsetCurves(offset)[0]
// if(offset != 0)
// {
// ClipboardTest.write2PolyLine(pl,npl);
// }
return PolylineHelper.getVModelPoints(npl, depth, angle)
}
static getArcRadius(x1: number, y1: number, x2: number, y2: number, bul: number): number {
let bul2 = Math.abs(bul)
let d = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) / 2
return 0.5 * d * (1 + bul2 ** 2) / bul2
}
// 圆 转 多段线
static cicleToPolyline(c: Circle): Polyline {
let arcs = c.GetSplitCurves([0, 0.5])
let pl = Polyline.FastCombine(arcs)
return pl
}
/** 判断多段线是否 重叠 */
static isIntersect(pl1: Polyline, pl2: Polyline): boolean {
let box1 = this.getBox(pl1)
let box2 = this.getBox(pl2)
if (!box1.intersectsBox(box2))
return false // 肯定不相交
let ipts = pl1.IntersectWith(pl2, 0)
if (ipts.length === 0) {
if (pl1.Area > pl2.Area)// 缓存面积
{
if (isTargetCurInOrOnSourceCur(pl1, pl2))
return true // 包含
}
else {
if (isTargetCurInOrOnSourceCur(pl2, pl1))
return true // 包含
}
return false
}
else {
return true // 有交点 一定有交集
}
}
// 多段线 圆弧合并
static mergeCurve(pl2: Polyline): Polyline {
const curves = pl2.Explode()
arrayRemoveDuplicateBySort(curves, (c1, c2) => {
return c1.Join(c2) === Status.True
})
return Polyline.FastCombine(curves)
}
/**
* pl2 包含在pl1 内
*
*/
// static isInside(pl1:Polyline,pl2:Polyline):boolean
// {
// let box1 = this.getBox(pl1);
// let box2 = this.getBox(pl2);
// if (!box1.intersectsBox(box2)) return false; //肯定不相交
// let ipts = pl1.IntersectWith(pl2, 0);
// if (ipts.length > 0) return true; //有交点 一定有交集
// }
/**
* 两片板的干涉检查
*
* @param pl1
* @param pls1_inner
* @param pls1_model
* @param pl2
* @param pls2_inner
* @param pls2_model
* @returns
*/
static isOverLap(pl1: Polyline, pls1_inner: Polyline[], pls1_model: Polyline[], pl2: Polyline, pls2_inner: Polyline[], pls2_model: Polyline[]) {
// 是否干涉, 被包含在造型洞,不算干涉
let isOverlap = this.boxIsOverlap(pl1, pls1_inner, pl2, pls2_inner)
if (isOverlap)
return true
// 造型 2v 刀路 是否干涉
for (let pl1_model of pls1_model) {
if (pl1_model.IntersectWith(pl2, 0).length > 0)
return true
for (let pl2_inner of pls2_inner) {
if (pl1_model.IntersectWith(pl2_inner, 0).length > 0)
return true
}
}
for (let pl2_model of pls2_model) {
if (pl2_model.IntersectWith(pl1, 0).length > 0)
return true
for (let pl1_inner of pls1_inner) {
if (pl2_model.IntersectWith(pl1_inner, 0).length > 0)
return true
}
}
return false
}
private static boxIsOverlap(pl1: Polyline, pls1_inner: Polyline[], pl2: Polyline, pls2_inner: Polyline[]) {
let box1 = this.getBox(pl1)
let box2 = this.getBox(pl2)
if (!box1.intersectsBox(box2))
return false // 肯定不相交
let ipts = pl1.IntersectWith(pl2, 0)
if (ipts.length > 0)
return true // 有交点 一定有交集
if (pl1.Area > pl2.Area)// 缓存面积
{
if (isTargetCurInOrOnSourceCur(pl1, pl2)) // pl1包含 pl2
{
for (let mpl of pls1_inner) // 如果pl1有造型洞包含pl2 则表示不干涉返回false
{
if (isTargetCurInOrOnSourceCur(mpl, pl2) == true)
return false
}
return true
}
}
else {
if (isTargetCurInOrOnSourceCur(pl2, pl1)) // pl2包含 pl1
{
for (let mpl of pls2_inner) // 如果pl2有造型洞包含pl1 则表示不干涉返回false
{
if (isTargetCurInOrOnSourceCur(mpl, pl1) == true)
return false
}
return true
}
}
return false
}
/** 判断 点是否在多段线内 */
static isPointInPolyline(pl1: Polyline, x: number, y: number): boolean {
return pl1.PtInCurve(new Vector3(x, y, 0))
}
static getBox(pl1: Polyline): Box3 {
if (!pl1.box_tp)
pl1.box_tp = pl1.BoundingBox
return pl1.box_tp as Box3
}
static getArea(pl1: Polyline): number {
if (!pl1.area_tp)
pl1.area_tp = pl1.Area
return pl1.area_tp as number
}
static getPath(pl: Polyline): Path2D {
let path = new Path2D()
let p0 = pl.LineData[0].pt
path.moveTo(p0.x, p0.y)
for (let i = 0; i < pl.LineData.length; i++) {
let p0 = pl.LineData[i].pt
let p1 = (i == pl.LineData.length - 1) ? pl.LineData[0].pt : pl.LineData[i + 1].pt
let bul = pl.LineData[i].bul
if (bul == 0) {
path.lineTo(p1.x, p1.y)
}
else {
let arc = new Arc2d(new Point2d(p0.x, p0.y), new Point2d(p1.x, p1.y), bul)
path.arc(arc.m_Center.m_X, arc.m_Center.m_Y, arc.m_Radius, arc.m_StartAngle, arc.m_EndAngle, bul < 0)
}
}
path.closePath()
return path
}
static equaln(v1: number, v2: number, fuzz = 1e-5) {
return Math.abs(v1 - v2) <= fuzz
}
static toClipboard(en: Polyline | any) {
let f = new CADFiler()
f.Write(1)// 实体个数
f.WriteObject(en)
copyTextToClipboard(f.ToString())
}
static getStrPLs(ens: any[]) {
if (ens.length == 0)
return ''
let f = new CADFiler()
f.Write(ens.length)// 实体个数
for (let en of ens)
f.WriteObject(en)
return f.ToString()
}
}

View File

@@ -0,0 +1,85 @@
import { Vector2 } from "../Vector2"
interface P {
x: number
y: number
}
export interface IOffset {
negativeOffset: number
positiveOffset: number
}
/** 点p到线段P1P2 的最短距离的平方,线段不延伸 */
function GetSqSegDist(p: P, p1: P, p2: P): number {
let x = p1.x
let y = p1.y
let dx = p2.x - x
let dy = p2.y - y
if (dx !== 0 || dy !== 0)// 不是0长度线
{
const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)
if (t > 1) {
x = p2.x
y = p2.y
}
else if (t > 0) {
x += dx * t
y += dy * t
}
}
dx = p.x - x
dy = p.y - y
return dx * dx + dy * dy
}
function CrossVector2(a: P, b: P)
{
return a.x * b.y - a.y * b.x
}
// Ramer-Douglas-Peucker algorithm
function SimplifyDPStep(points: P[], first: number, last: number, sqTolerance: number, simplified: P[], offset: IOffset): void {
let maxSqDist = 0
let index: number
const fp = points[first]
const lp = points[last]
for (let i = first + 1; i < last; i++) {
const p = points[i]
const sqDist = GetSqSegDist(p, fp, lp)
if (sqDist > maxSqDist) {
index = i
maxSqDist = sqDist
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1)
SimplifyDPStep(points, first, index, sqTolerance, simplified, offset)
simplified.push(points[index])
if (last - index > 1)
SimplifyDPStep(points, index, last, sqTolerance, simplified, offset)
}
else {
// 记录偏移
const v = new Vector2(lp.x - fp.x, lp.y - fp.y).normalize()
for (let i = first + 1; i < last; i++) {
const p = points[i]
const offsetDist = -CrossVector2(v, { x: p.x - fp.x, y: p.y - fp.y })
offset.positiveOffset = Math.max(offset.positiveOffset, offsetDist)
offset.negativeOffset = Math.min(offset.negativeOffset, offsetDist)
}
}
}
// Ramer-Douglas-Peucker 算法
export function SimplifyDouglasPeucker(points: P[], sqTolerance: number): [P[], IOffset] {
const last = points.length - 1
const simplified: P[] = [points[0]]
const offset: IOffset = { negativeOffset: 0, positiveOffset: 0 }
SimplifyDPStep(points, 0, last, sqTolerance, simplified, offset)
simplified.push(points[last])
return [simplified, offset]
}

View File

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

View File

@@ -0,0 +1,38 @@
/** 判断两个值是否相等 */
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 = (Array.isArray(arr)) ? 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
}

View File

@@ -0,0 +1,335 @@
import type { Point } from './Point.js'
/** 二维向量 */
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 (Number.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: Point): Vector2
{
this.x = Math.min(this.x, v.x)
this.y = Math.min(this.y, v.y)
return this
}
max(v: Point): 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; const 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); const 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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
export class StringFormat
{
/**
* 对Date的扩展将 Date 转化为指定格式的String
* 月(M)、日(d)、12小时(h)、24小时(H)、分(m)、秒(s)、周(E)、季度(q) 可以用 1-2 个占位符
* 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* eg:
* (new Date()).pattern("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
* (new Date()).pattern("yyyy-MM-dd E HH:mm:ss") ==> 2009-03-10 二 20:09:04
* (new Date()).pattern("yyyy-MM-dd EE hh:mm:ss") ==> 2009-03-10 周二 08:09:04
* (new Date()).pattern("yyyy-MM-dd EEE hh:mm:ss") ==> 2009-03-10 星期二 08:09:04
* (new Date()).pattern("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
*/
static Date(date: Date, fmt): string
{
let o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours() % 12 == 0 ? 12 : date.getHours() % 12, // 小时
'H+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S': date.getMilliseconds(), // 毫秒
}
let week = {
0: '/u65e5',
1: '/u4e00',
2: '/u4e8c',
3: '/u4e09',
4: '/u56db',
5: '/u4e94',
6: '/u516d',
}
if (/(y+)/.test(fmt))
{
fmt = fmt.replace(RegExp.$1, (`${date.getFullYear()}`).substr(4 - RegExp.$1.length))
}
if (/(E+)/.test(fmt))
{
fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length > 1) ? (RegExp.$1.length > 2 ? '/u661f/u671f' : '/u5468') : '') + week[`${date.getDay()}`])
}
for (let k in o)
{
if (new RegExp(`(${k})`).test(fmt))
{
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : ((`00${o[k]}`).substr((`${o[k]}`).length)))
}
}
return fmt
}
/** 返回数字的固定小数点 123.00 */
static number(value: number, bit: number): string
{
return value?.toFixed(bit).toString()
}
static toFixed(value: number, bit: number = 3): number
{
let tt = 10 ** bit
return Math.round(value * tt) / tt
}
static filterIllegalChar(str: string): string
{
// var pattern = new RegExp("[/:*?'<>|\\]");
// var rs = "";
// for (var i = 0; i < str.length; i++)
// {
// rs = rs + str.substr(i, 1).replace(pattern, '');
// }
// return rs;
let rt = str.replace(/\\/g, '').replace(/\:/g, '').replace(/\*/g, '').replace(/\?/g, '').replace(/\"/g, '').replace(/\</g, '').replace(/\>/g, '').replace(/\|/g, '').replace(/\'/g, '')
return rt
}
/** 文本格式化 */
static format(str: string, ...args: any[]): string
{
let data = args
let tmpl = str
for (const item of tmpl.matchAll(/\{(.+?)\}/g))
{
let parts = item[1].split(',').map(i => i.trim())
let index = Number(parts[0])
let arg = data[index]
let val = (arg || '').toString() // 默认
if (arg instanceof Date) // 日期
{
let fm = 'MM-dd HH;mm'
if (parts.length > 1)
{
fm = parts[1]
}
val = this.Date(arg, fm)
}
if (parts.length > 1 && parts[1][0] === '#')
{
// {2,#3} -> 数字 转成3位 001,...023,
val = val.padStart(Number(parts[1].substring(1)), '0')
}
tmpl = tmpl.replace(item[0], val)
}
return tmpl
}
/** 实现右对齐,右边填充 (25,4,'*') => '25**' */
static PadEnd(num: number, totalWidth: number, paddingChar: string): string
{
let str = num.toString()
return str.padEnd(totalWidth, paddingChar[0])
}
/** 实现右对齐,左边填充 (25,4,'0') => '0025' */
static PadStart(num: number, totalWidth: number, paddingChar: string): string
{
let str = num.toString()
return str.padStart(totalWidth, paddingChar[0])
}
}

View File

@@ -0,0 +1,381 @@
import { Box2 } from '../Box2.js'
import { clipperCpp } from '../ClipperCpp.js'
import type { NestFiler } from '../Filer.js'
import type { Point } from '../Point.js'
import { equaln } from '../Util.js'
import { Vector2 } from '../Vector2.js'
/**
* 轮廓路径类
* 可以求NFP和保存NFPCahce
* 因为NFP结果是按照最低点移动的,所以将点旋转后,按照盒子将点移动到0点.
*/
export class Path
{
Id: number
Points: Point[]
OutsideNFPCache: { [key: number]: Point[][] } = {}
InsideNFPCache: { [key: number]: Point[][] } = {}
constructor(public OrigionPoints?: Point[], rotation: number = 0)
{
if (OrigionPoints)
this.Init(OrigionPoints, rotation)
}
Origion: Path
// 点表在旋转后的原始最小点.使用这个点将轮廓移动到0点
OrigionMinPoint: Vector2
Rotation: number
Size: Vector2// 序列化
private Init(origionPoints: Point[], rotation: number)
{
this.Rotation = rotation
if (rotation === 0)
this.Points = origionPoints.map((p) => { return { ...p } })
else
{
let c = Math.cos(rotation)
let s = Math.sin(rotation)
let npts: Point[] = []
for (let p of origionPoints)
{
let x = p.x
let y = p.y
const x1 = x * c - y * s
const y1 = x * s + y * c
npts.push({ x: x1, y: y1 })
}
this.Points = npts
}
let box = new Box2()
let v2 = new Vector2()
for (let p of this.Points)
{
v2.x = p.x
v2.y = p.y
box.expandByPoint(v2)
}
this.OrigionMinPoint = box.min
this.Size = box.max.sub(box.min)
for (let p of this.Points)
{
p.x -= box.min.x
p.y -= box.min.y
}
}
GetNFPs(path: Path, outside: boolean): Point[][]
{
// 寻找内轮廓时,面积应该比本path小,这个判断移交给使用者自己判断
// if (!outside && this.Area < path.Area) return [];
let nfps = clipperCpp.lib.minkowskiSumPath(this.BigIntPoints, path.MirrorPoints, true)
// 必须删除自交,否则将会出错
nfps = clipperCpp.lib.simplifyPolygons(nfps)
nfps = nfps.filter((nfp) =>
{
let area = Area(nfp)
// if (area > 1) return outside;//第一个不一定是外轮廓,但是面积为正时肯定为外轮廓 (因为使用了简化多段线,所以这个代码已经不能有了)
if (Math.abs(area) < 10)
return false// 应该不用在移除这个了
let { x, y } = nfp[0]
if (outside)
{
if (this.Area > path.Area)
{
let p = { x: path.InPoint.x + x, y: path.InPoint.y + y }
if (p.x < 0 || p.y < 0 || p.x > this.BigSize.x || p.y > this.BigSize.y)
return true
let dir = clipperCpp.lib.pointInPolygon(p, this.BigIntPoints)
return dir === 0
}
else
{
let p = { x: this.InPoint.x - x, y: this.InPoint.y - y }
if (p.x < 0 || p.y < 0 || p.x > path.BigSize.x || p.y > path.BigSize.y)
return true
let dir = clipperCpp.lib.pointInPolygon(p, path.BigIntPoints)
return dir === 0
}
}
else
{
let p = { x: path.InPoint.x + x, y: path.InPoint.y + y }
if (p.x < 0 || p.y < 0 || p.x > this.BigSize.x || p.y > this.BigSize.y)
return false
let dir = clipperCpp.lib.pointInPolygon(p, this.BigIntPoints)
return dir === 1
}
})
return nfps
}
GetOutsideNFP(path: Path): Point[][]
{
let nfps = this.OutsideNFPCache[path.Id]
if (nfps)
return nfps
if (this.IsRect && path.IsRect)
{
let [ax, ay] = [this.Size.x * 1e4, this.Size.y * 1e4]
let [bx, by] = [path.Size.x * 1e4, path.Size.y * 1e4]
nfps = [[
{ x: -bx, y: -by },
{ x: ax, y: -by },
{ x: ax, y: ay },
{ x: -bx, y: ay },
]]
}
else
nfps = this.GetNFPs(path, true)
this.OutsideNFPCache[path.Id] = nfps
// 虽然有这种神奇的特性,但是好像并不会提高性能。
// path.OutsideNFPCache[this.id] = (this, nfps.map(nfp =>
// {
// return nfp.map(p =>
// {
// return { x: -p.x, y: -p.y };
// });
// }));
return nfps
}
GetInsideNFP(path: Path): Point[][]
{
if (path.Area > this.Area)
return
let nfp = this.InsideNFPCache[path.Id]
if (nfp)
return nfp
let nfps: Point[][]
if (this.IsRect)
{
let [ax, ay] = [this.Size.x * 1e4, this.Size.y * 1e4]
let [bx, by] = [path.Size.x * 1e4, path.Size.y * 1e4]
let l = ax - bx
let h = ay - by
const MinNumber = 200// 清理的数值是100,所以200是可以接受的, 200=0.020问题不大(过盈配合)
if (l < -MinNumber || h < -MinNumber)
return
if (l < MinNumber)
l = MinNumber
else
l += MinNumber
if (h < MinNumber)
h = MinNumber
else
h += MinNumber
nfps = [[
{ x: 0, y: 0 },
{ x: l, y: 0 },
{ x: l, y: h },
{ x: 0, y: h },
]]
}
else
nfps = this.GetNFPs(path, false)
if (path.Id !== undefined)
this.InsideNFPCache[path.Id] = nfps
return nfps
}
private _InPoint: Point
/**
* 用这个点来检测是否在Path内部
*/
private get InPoint()
{
if (this._InPoint)
return this._InPoint
let mp = { x: (this.Points[0].x + this.Points[1].x) / 2, y: (this.Points[0].y + this.Points[1].y) / 2 }
let normal = new Vector2(this.Points[1].x - this.Points[0].x, this.Points[1].y - this.Points[0].y).normalize()
// [normal.x, normal.y] = [normal.y, -normal.x];
mp.x -= normal.y
mp.y += normal.x
mp.x *= 1e4
mp.y *= 1e4
this._InPoint = mp
return mp
}
protected _BigIntPoints: Point[]
get BigIntPoints()
{
if (this._BigIntPoints)
return this._BigIntPoints
this._BigIntPoints = this.Points.map((p) =>
{
return {
x: Math.round(p.x * 1e4),
y: Math.round(p.y * 1e4),
}
})
return this._BigIntPoints
}
private _BigSize: Vector2
get BigSize()
{
if (this._BigSize)
return this._BigSize
this._BigSize = new Vector2(this.Size.x * 1e4, this.Size.y * 1e4)
return this._BigSize
}
protected _MirrorPoints: Point[]
get MirrorPoints()
{
if (!this._MirrorPoints)
this._MirrorPoints = this.BigIntPoints.map((p) =>
{
return { x: -p.x, y: -p.y }
})
return this._MirrorPoints
}
protected _BoundingBox: Box2
get BoundingBox()
{
if (!this._BoundingBox)
{
this._BoundingBox = new Box2(new Vector2(), this.Size)
}
return this._BoundingBox
}
protected _Area: number
get Area()
{
if (this._Area === undefined)
this._Area = Area(this.Points)
return this._Area
}
set Area(a: number)
{
this._Area = a
}
private _IsRect: boolean
get IsRect()
{
if (this._IsRect === undefined)
{
let s = this.BoundingBox.getSize(new Vector2())
this._IsRect = equaln(this.Area, s.x * s.y, 1)
}
return this._IsRect
}
ReadFile(file: NestFiler): void
{
let ver = file.Read()
this.Id = file.Read()
let arr = file.Read()
this.Points = []
for (let i = 0; i < arr.length; i += 2)
{
let p = { x: arr[i], y: arr[i + 1] }
this.Points.push(p)
}
this.Size = new Vector2(file.Read(), file.Read())
this._Area = file.Read()
let id = file.Read()
if (id !== -1)
{
this.Origion = id
this.Rotation = file.Read()
this.OrigionMinPoint = new Vector2(file.Read(), file.Read())
}
}
WriteFile(file: NestFiler): void
{
file.Write(1)// ver
file.Write(this.Id)
let arr: number[] = []
for (let p of this.Points)
arr.push(p.x, p.y)
file.Write(arr)
file.Write(this.Size.x)
file.Write(this.Size.y)
file.Write(this._Area)
if (this.Origion && this.Origion.Id)
{
// 如果有原始的id,则传递它,以便后续进行NFP复用.
file.Write(this.Origion.Id)
file.Write(this.Rotation)
file.Write(this.OrigionMinPoint.x)
file.Write(this.OrigionMinPoint.y)
}
else
file.Write(-1)
}
}
// 点表面积
export function Area(pts: Point[]): number
{
let cnt = pts.length
if (cnt < 3)
return 0
let a = 0
for (let i = 0, j = cnt - 1; i < cnt; ++i)
{
a += (pts[j].x + pts[i].x) * (pts[j].y - pts[i].y)
j = i
}
return -a * 0.5
}
/**
* 平移点表,返回新点表
*/
export function TranslatePath(pts: Point[], p: Point): Point[]
{
return pts.map((px) =>
{
return { x: p.x + px.x, y: p.y + px.y }
})
}
export function TranslatePath_Self(pts: Point[], mx: number, my: number): Point[]
{
for (let pt of pts)
{
pt.x += mx
pt.y += my
}
return pts
}
// 缩放点表,返回原始点表
export function PathScale(pts: Point[], scale: number): Point[]
{
for (let p of pts)
{
p.x *= scale
p.y *= scale
}
return pts
}

View File

@@ -0,0 +1,202 @@
import { _knifeType, Knife, SideModel } from "../confClass";
import { knifeData,knifeData1,knifeData2 } from "../demoKnives";
/** 刀库 管理刀具*/
export class KnifeHelper {
knifeList: Knife[]
constructor(_knifeList) {
this.knifeList.push(knifeData);
if (Array.isArray(_knifeList) && _knifeList.length >0 ) {
this.loadKnifeList(_knifeList)
}
}
loadKnifeList(_knifeList) {
this.knifeList = _knifeList || []
}
/** 根据刀名称获取造型刀 */
getModelKnifeByName(name: string): Knife | undefined {
return this.knifeList.find(t => t.knifeName.trim() == name.trim() && t.isModelingKnife());
}
/** 根据半径获取造型刀 */
getModelKnifeByRadius(radius: number, depth: number): Knife | undefined {
return this.knifeList.find(t => t.isModelingKnife() && Math.abs(t.diameter - 2 * radius) < 0.02 && t.length >= depth - 0.01);
}
/** 获取合适的开料刀作为造型刀 */
getFittedModelKnife(radius: number, depth: number): Knife | null {
const knives = this.knifeList.filter(t => t.isModelingKnife() && t.length > depth - 0.01 && t.diameter / 2 < radius - 0.02);
if (knives) {
knives.sort((a, b) => b.diameter - a.diameter); //按刀直径倒序排序
return knives[0];
}
return null;
}
/** 根据厚度获取开料刀 */
getKnifeByThickness(thickness: number): Knife | undefined {
return this.knifeList.find(t => t.length >= thickness);
}
/** 通用 找刀具 根据查询条件 */
getKnifeByParams(params: _knifeType) {
let knife: Knife | null = null
if (params) {
let tempKnifeList: Knife[] = [...this.knifeList] // []
let keys = Object.keys(params)
if (keys.length > 0) {
keys.forEach(key => {
if (Array.isArray(params[key]) && key == 'ability') {
// 进来的应该是ability 是数组 判断刀的能力
for (const arrItem of params[key]) {
let _knifeList = this.knifeList.filter(e => e.ability.includes(arrItem))
_knifeList.forEach(k => {
if (!this.KnifeIsInKnifeList(k, tempKnifeList)) {
tempKnifeList.push(k)
}
})
}
} else if (['string', 'number'].includes(typeof (params[key]))) {
if (params && params[key] && typeof (params[key]) == 'number') {
if (key == 'length') {
tempKnifeList = tempKnifeList.filter(e => e[key] >= params[key])
} else {
tempKnifeList = tempKnifeList.filter(e => e[key] == params[key])
}
}
}
});
if (tempKnifeList.length > 0) {
knife = tempKnifeList[0]
}
} else {
console.log('传入的查询条件 没有参数')
}
}
return knife
}
/** 判断 刀是否在 刀列表内
* true 存在
* false 不存在
*/
KnifeIsInKnifeList(_knife: Knife, _knifeList: Knife[]) {
let k = _knifeList.find(e => _knife.axleId == e.axleId
&& _knife.diameter == e.diameter
&& _knife.knifeId == e.knifeId)
if (k) {
return true
} else {
return false
}
}
getSideModelKnife(model: SideModel): Knife | undefined | null {
if (model.knifeName && model.knifeName.trim() != '') {
return this.getModelKnifeByName(model.knifeName.trim());
}
//获取刀半径(直径)和长度与造型定义的刀半径和深度一致的刀
const knife = this.getSideModelKnifeByRadius(model.knifeRadius, model.depth, model.direction);
if (knife) return knife;
//获取刀半径(直径)小于造型定义的刀半径、长度不小于造型深度的刀
return this.getFittedSideModelKnife(model.knifeRadius, model.depth, model.direction);
}
/** 获取合适的开料刀作为造型刀 */
getFittedSideModelKnife(radius: number, depth: number, direction): Knife | null {
let processFaceArr: number[] = [];
processFaceArr.push(5)
switch (direction) {
case 0:
processFaceArr.push(4)
break;
case 1:
processFaceArr.push(2)
break;
case 2:
processFaceArr.push(3)
break;
case 3:
processFaceArr.push(1)
break;
case 10:
break;
case 11:
break;
case 12:
break;
case 13:
break;
default:
break;
}
const knives = this.knifeList.filter(t => t.isModelingKnife() && t.length > depth - 0.01 && t.diameter / 2 < radius - 0.02 && processFaceArr.includes(t.processFace));
if (knives) {
knives.sort((a, b) => b.diameter - a.diameter); //按刀直径倒序排序
return knives[0];
}
return null;
}
/** 根据半径获取造型刀 */
// direction 0 下 1右 2上 3左 10 左下斜 11 右下斜 12右上斜 13 左上斜
// processFace: number // 加工面 2024 11 21 : 正面 0 反面 1 左侧面 2 右侧面 3 上侧面 4 下侧面 5 任意 6
getSideModelKnifeByRadius(radius: number, depth: number, direction): Knife | undefined {
let processFaceArr: number[] = [];
processFaceArr.push(5)
switch (direction) {
case 0:
processFaceArr.push(4)
break;
case 1:
processFaceArr.push(2)
break;
case 2:
processFaceArr.push(3)
break;
case 3:
processFaceArr.push(1)
break;
case 10:
break;
case 11:
break;
case 12:
break;
case 13:
break;
default:
break;
}
let knife = this.knifeList.find(t => t.isModelingKnife() && Math.abs(t.diameter - 2 * radius) < 0.02 && t.length >= depth - 0.01 && processFaceArr.includes(t.processFace))
return knife;
}
}

View File

@@ -0,0 +1,2 @@
# 基础功能目录
该目录下的功能函数尽量不包含业务逻辑

View File

@@ -0,0 +1,262 @@
import { ProcessorModule } from "../../src/device";
import { confItem, PlaceBlock, PlaceBlockDetail, PlaceMaterial } from "../confClass";
import { Big_bang, ComposingType, LineType, WorkerItemType, xbang } from "../handleAbility/RectOptimizeWorker/bang";
import resData from './res1.json'
import {Worker} from 'worker_threads'
/** 模块 新优化
*
* input 入参
*
* ProcessorModule<any, any>这里的any 可以自定义 根据组件需求
*/
export const RectOptimizeMachineModule: ProcessorModule<any, any> = {
moduleName: "RectOptimizeMachine",
config: {
workerList: [],
placeStyle: 1,
},
setConfig(config) {
this.config = { ...this.config, ...config };
},
before(input) {
// console.log(`优化前要做的事情`, input);
},
process(input, next, context) {
let { blockList, materialList } = input
let bList: any = []
let pm = materialList[0]
blockList.map(e => {
if (e.goodsId == pm.goodsId) {
bList[e.blockNo] = e
// let detail = blockDetailList.find(x => x.blockId == e.blockId)
// // 处理可能出现的异常 --没有板明细
// if (!Reflect.has(bList[e.blockNo], 'blockDetail')) {
// bList[e.blockNo].blockDetail = new PlaceBlockDetail(detail)
// }
// bList[e.blockNo].isTurnFaceToPlace = !this.getDoFace(bList[e.blockNo], this.processMode())
// 是否翻面后续处理
bList[e.blockNo].isTurnFaceToPlace = true
if (Array.isArray(pm.blockList)) {
pm.blockList.push(e)
} else {
pm.blockList = [e]
}
}
})
pm.cutBorder = this.cutBoardBorder
pm.cutKnifeGap = this.blockKnifeLineSpacing
/** 小板 */
let bans: xbang[] = []
// 实际开料大板的列表
let big_Bang: Big_bang[] = []
let big_BangSL: number[] = []
let border = this.cutBoardBorder
let borderOff = (pm.diameter + pm.cutKnifeGap) / 2
// 余料板 以及实际开料大板
for (const cuttingBoard of pm.remainBoardList) {
big_Bang.push({ w: cuttingBoard.width - border * 2 + borderOff * 2, l: cuttingBoard.length - border * 2 + borderOff * 2, x: border - borderOff, y: border - borderOff })
big_BangSL.push(cuttingBoard.count || 999)
}
// big_Bang = []
// big_BangSL = []
// 母板 兜底的
big_Bang.push({ w: pm.width - border * 2 + borderOff * 2, l: pm.length - border * 2 + borderOff * 2, x: border - borderOff, y: border - borderOff })
// 生成小板
for (let key in bList) {
let b = bList[key]
let bid = b.blockNo
let width = b.placeFullWidth
let length = b.placeFullLength
let line = toLine(b.texture)
bans.push({
l: length,
w: width,
line,
face: toface(b),
id: bid,
bno: b.blockNo,
holeFaceCount: 3,
isRect: !b.isUnRegular,
hasHole: false,
isdtwosided: true,
})
}
let bestCount = 0
if (bans.length == 0) // 没有板了
{
if (context.CallBack) {
let best = []
let yl: Big_bang[] = []
let fit = 0
let resObj = {
data: { bList, best, yl, fit, bans, width: pm.width, length: pm.length, bestCount: bestCount++, pm },
info: {
times: -1,
type: 'noBan'
}
}
context.CallBack(resObj)
}
return
}
let xyhcs = 50
if (bans.length > 1000) { xyhcs = 40 }
else if (bans.length > 1500) { xyhcs = 30 }
else if (bans.length > 2000) { xyhcs = 25 }
else if (bans.length > 4000) { xyhcs = 20 }
else if (bans.length > 6000) { xyhcs = 15 }
else if (bans.length > 10000) { xyhcs = 10 }
else if (bans.length > 15000) { xyhcs = 5 }
else if (bans.length > 20000) { xyhcs = 1 }
let isDoubleFaceBlockFirst = this.isDoubleFaceBlockFirst // 双面加工排前面
let gap = this.blockKnifeLineSpacing
// this.bestfit = 0
/** 这里要做个多线程 测试环境下 这部分有问题 暂时略过 */
for (let j = 0; j < 1; j++) {
// let w = new Worker(new URL('../handleAbility/RectOptimizeWorker/RectOptimizeWorker.worker', import.meta.url), { type: 'module' })
let w= new Worker('../handleAbility/RectOptimizeWorker/RectOptimizeWorker.worker')
const data = {
type: 'start',
data: [bans, big_Bang, big_BangSL, xyhcs, isDoubleFaceBlockFirst, gap, this.isRectPlace, this.isDoubleFaceBlockInRemain],
}
let item: WorkerItemType = {
w: w,
goodsId: pm.goodsId,
pm,
status: 'start'
}
if (this.config.workerList.findIndex(e => e.goodsId == item.goodsId) == -1) {
this.config.workerList.push(item)
}
let workItem = this.config.workerList.find(e => e.goodsId == pm.goodsId)
if (workItem && workItem != undefined) {
workItem.w?.postMessage(data)
workItem.w.onmessage = async (d) => {
let [best, yl, fit, info] = d.data as [any[], Big_bang[], number, any]
switch (info.type) {
case 'loop':
let resObj = {
data: { bList, best, yl, fit, bans, width: pm.width, length: pm.length, bestCount: bestCount++, pm },
info
}
if (context.CallBack) {
context.CallBack(resObj)
}
break;
case 'stop':
console.error('stop =》dataHandleBase', info, this.config.workerList)
this.terminateWorker({ goodsId: pm.goodsId })
break;
case 'isStop':
// console.error('isStop', info)
break;
default:
break;
}
}
}
}
if (context.CallBack) {
context.CallBack(resData)
}
return next ? next(input) : input;
},
after(result) {
console.log(`优化后要做的事情`,);
},
onError(error) {
console.log('出错了哦', error);
}
};
function toLine(texture): LineType {
if (texture == 0)
return LineType.Positive
if (texture == 1)
return LineType.CanReversal
return LineType.Reverse
}
/** 小板加工面Positive=0正面 Reverse=1反面 Arbitrary=2任意 */
function toface(block: PlaceBlock): ComposingType {
let turnF = block.isTurnFaceToPlace
// if (this.isTurnFaceToPlace)
if (true)
turnF = !turnF
if (!turnF)
return ComposingType.Positive
return ComposingType.Reverse
}
/** 配置列表 */
export const confList: confItem[] = [
{
key: 'isCutProcess',
label: '开料机(雕刻机)加工',
value: true
},
{
key: 'isCutAndCNCProcess',
label: '开料机CNC组合',
value: false
},
{
key: 'isCustomized',
label: '定制加工',
value: false
},
{
key: 'cutBoardBorder',
label: '总修边宽度',
value: 3
},
{
key: 'blockKnifeLineSpacing',
label: '刀路间距',
value: 0
},
{
key: 'isDoubleFaceBlockFirst',
label: '双面开料优先排版',
value: true
},
{
key: 'isRectPlace',
label: '新优化规则排版',
value: false
},
{
// yuLiaoBoardDo2FaceBlock
key: 'isDoubleFaceBlockInRemain',
label: '余料板允许排入双面加工的小板',
value: true
},
]

File diff suppressed because it is too large Load Diff

248
src/device.ts Normal file
View File

@@ -0,0 +1,248 @@
// 回调函数类型定义
type ProcessCallback<T, R> = (
input: T,
next: (input: T) => R | Promise<R>,
context?: any
) => R | Promise<R>;
// 模块配置类型
type ModuleConfig = Record<string, any>;
// 回调函数类型
type ModuleCallback<T, R> = (result: R, input?: T) => void | Promise<void>;
// 扩展后的功能模块接口
export interface ProcessorModule<T, R> {
// 主处理函数(可回调)
process?: ProcessCallback<T, R>;
// 直接是处理函数(无回调)
handle?: (input: T, next?: (input: T) => R | Promise<R>, context?: Record<string, any>) => R | Promise<R>;
// 模块名称(用于标识和排序)
moduleName?: string;
// 模块配置
config?: ModuleConfig;
getConfigList?: () => ModuleConfig
// 设置配置的方法
setConfig?: (config: ModuleConfig) => void;
// 前置回调(在模块处理前执行)
before?: ModuleCallback<T, R>;
// 后置回调(在模块处理后执行)
after?: ModuleCallback<T, R>;
// 错误处理回调
onError?: (error: unknown, input?: T) => void | Promise<void>;
}
export interface Processor<T, R> {
// 注册模块
use(module: ProcessorModule<T, R> | ProcessorModule<T, R>[]): this;
// 调整模块顺序
reorderModules(moduleNames: string[]): this;
// 执行处理流程
process(input: T): Promise<R>;
// 获取当前模块列表
getModules(): ProcessorModule<T, R>[];
// 新增方法:更新模块配置
updateModuleConfig(moduleName: string, config: ModuleConfig): this;
}
// 处理器集合接口
export interface ProcessorCollection<T, R> {
// 注册处理器
registerProcessor(name: string, processor: Processor<T, R>): this;
// 切换当前处理器
useProcessor(name: string): Processor<T, R>;
// 获取处理器
getProcessor(name: string): Processor<T, R> | undefined;
}
/** 流程管理器 处理器内组件的执行流程管理器
*
* 负责管理 组件执行顺序 和执行模块
*/
export class StepControllerProcessor<T, R> implements Processor<T, R> {
private modules: ProcessorModule<T, R>[] = [];
private modulesMap = new Map<string, ProcessorModule<T, R>>();
use(module: ProcessorModule<T, R> | ProcessorModule<T, R>[]): this {
const modules = Array.isArray(module) ? module : [module];
modules.forEach(m => {
if (m.moduleName) {
this.modulesMap.set(m.moduleName, m);
}
this.modules.push(m);
});
return this;
}
reorderModules(moduleNames: string[]): this {
const orderedModules = moduleNames
.map(name => this.modulesMap.get(name))
.filter(Boolean) as ProcessorModule<T, R>[];
const remainingModules = this.modules.filter(
m => !m.moduleName || !moduleNames.includes(m.moduleName)
);
this.modules = [...orderedModules, ...remainingModules];
return this;
}
updateModuleConfig(moduleName: string, config: ModuleConfig): this {
const module = this.modulesMap.get(moduleName);
if (module && module.setConfig) {
module.setConfig(config);
} else if (module) {
module.config = { ...module.config, ...config };
}
return this;
}
private async executeModule(
module: ProcessorModule<T, R>,
input: T,
next: (input: T) => Promise<R>,
context: Record<string, any>
): Promise<R> {
try {
// 执行前置回调
if (module.before) {
await module.before(input, input);
}
// 执行主处理逻辑(支持两种风格)
let result: R;
if (module.process) {
// 回调风格
const processResult = module.process(input, next, context);
result = processResult instanceof Promise ? await processResult : processResult;
} else if (module.handle) {
// 传统风格
const handleResult = module.handle(input, next, context);
result = handleResult instanceof Promise ? await handleResult : handleResult;
} else {
// 默认直接调用 next
result = await next(input);
}
// 执行后置回调
if (module.after) {
await module.after(result, input);
}
return result;
} catch (error) {
// 执行错误处理
if (module.onError) {
await module.onError(error, input);
// 即使出错也继续流程(除非抛出)
return await next(input);
}
throw error;
}
}
async process(input: T): Promise<R> {
if (this.modules.length === 0) {
throw new Error("No modules registered");
}
let currentIndex = 0;
const modules = this.modules;
const context: Record<string, any> = {};
const executeNext = async (currentInput: T): Promise<R> => {
const currentModule = modules[currentIndex++];
if (!currentModule) {
return currentInput as unknown as R;
}
// 创建 next 函数
const next = async (nextInput: T): Promise<R> => {
return executeNext(nextInput);
};
return this.executeModule(currentModule, currentInput, next, context);
};
return executeNext(input);
}
// async process(input: T): Promise<R> {
// if (this.modules.length === 0) {
// throw new Error("No modules registered");
// }
// let currentIndex = 0;
// const modules = this.modules;
// const context: Record<string, any> = {}; // 共享上下文
// const executeNext = async (currentInput: T): Promise<R> => {
// const currentModule = modules[currentIndex++];
// if (!currentModule) {
// return currentInput as unknown as R;
// }
// try {
// // 执行前置回调
// if (currentModule.before) {
// await currentModule.before(currentInput, currentInput);
// }
// // 执行主处理函数
// const next = async (nextInput: T): Promise<R> => {
// return executeNext(nextInput);
// };
// let result: R;
// const processResult = currentModule.process(currentInput, next, context);
// if (processResult instanceof Promise) {
// result = await processResult;
// } else {
// result = processResult;
// }
// // 执行后置回调
// if (currentModule.after) {
// await currentModule.after(result, currentInput);
// }
// return result;
// } catch (error) {
// // 执行错误处理
// if (currentModule.onError) {
// await currentModule.onError(error, currentInput);
// } else {
// throw error; // 如果没有错误处理,则向上抛出
// }
// // 根据错误处理结果决定是否继续
// return currentInput as unknown as R;
// }
// };
// return executeNext(input);
// }
getModules(): ProcessorModule<T, R>[] {
return [...this.modules];
}
}

View File

@@ -2,6 +2,4 @@ export * from './base';
export * from './parsers';
export * from './models/config';
export * from './models/knife';
export * from './models/file';
export * from './models/processors/rectLayout';
export * from './models/processors/cutOrder'
export * from './models/file';

View File

@@ -1,27 +0,0 @@
export interface IPoint { x: number, y: number; }
/**
* 加工数据
*/
export interface IProcessingItem {
/**
* 加工点数组
*/
pts: IPoint[];
/**
* 凸度数组
*/
buls: number[];
/**
* 加工深度
*/
depth: number;
/**
* 半径
*/
radius: number;
/**
* 刀ID
*/
knifeId?: number;
}

View File

@@ -1,57 +0,0 @@
/**
* 开料顺序(新)所使用的类型
*
*/
import { ConfigBase } from "../config";
/** 处理器入参 开料顺序(新)
* @author lx
*
* 注20250730 暂无配置 留个位置
*/
export class CutorderConfig extends ConfigBase {
// [key:string] : any
}
/**
* 处理器输入数据源 开料顺序(新)
*/
export type CutOrderInput = {
/**开料大板 宽 */
boardWidth: number,
/** 开料大板 高 */
boardHeight: number,
/** 刀头大小(含修边) */
gap: number,
/** 小板数据集合 */
blocks: CutorderInputBlock[]
}
/**
* 处理器输出数据 开料顺序(新)
*/
export type CutorderOutput = {
blocks: CutorderoutputBlock[]
}
/**
* 小板类型 输入
*/
export type CutorderInputBlock = {
/** 小板唯一标识 */
id: string | number,
/** 排版长 */
length: number,
/** 排版宽 */
width: number,
/** 板件坐标X */
x: number,
/** 板件坐标y */
y: number,
}
/**
* 小板类型 输出
*/
export type CutorderoutputBlock = CutorderInputBlock & {
cutOrder: number
}

View File

@@ -1,130 +0,0 @@
/**
* @file 矩形板件布局优化处理器使用的数据模型,包含优化输入、输出、大板、小板以及各类枚举
* @todo 目前仅适配了矩形优化,后续还需要对数据结构进行扩展
* @since 0.1.8
* @author CZY
*/
import { ConfigBase } from "../config";
export interface RectLayoutProcInput {
/** 小板列表 */
blocks: RectLayoutBlock[];
/** 余料大板列表,可选,余料大板将会被优先优化,当余料大板被用尽时,则会使用配置中的大板尺寸进行优化 */
scrapBoards?: Array<{
/** 余料大板 */
board: RectLayoutBoard,
/** 大板张数 */
count: number;
}>;
}
export class RectLayoutProcConfig extends ConfigBase {
/** 大板高度 */
boardHeight: number = 1220;
/** 大板宽度 */
boardWidth: number = 2440;
/** 优化迭代次数 */
iterations: number = 50;
/** 双面加工优先排版 */
doubleSidedFirst: boolean = false;
/** 刀路间隙 */
gap: number = 0;
/** 运行标识 (其实改成方法更合适*/
_runFlag: 'running' | 'stopped' | 'terminated' = 'running';
/** 当出现优化结果时,进行回调 */
_onMessage?: (e: LayoutResult) => void;
}
export type RectLayoutProcOutput = never;
/** 矩形优化小板输入 */
export interface RectLayoutBlock {
/** 小板ID */
id: number;
/** 长 */
length: number;
/** 宽 */
width: number;
/** 纹路类型 */
waveType: WaveType;
/** 排版面类型 */
composingType: ComposingType;
/** 孔洞类型 */
holeType: HoleType;
/** 是否为矩形板 */
isRect?: boolean;
/** 是否需要双面加工 */
isdtwosided?: boolean;
}
/** 矩形优化大板输入 */
export interface RectLayoutBoard {
length: number;
width: number;
}
/** 纹路类型 */
export enum WaveType {
/** 正纹 */
Positive = 0,
/** 反纹 */
Reverse = 1,
/** 可翻转 */
CanReversal = 2,
}
/** 排版面 */
export enum ComposingType {
/** 正面 */
Positive = 0,
Reverse = 1, //反面
Arbitrary = 2 //任意
}
/** 孔类型 */
export enum HoleType {
/** 没有孔 */
None = 0,
/** 正面 */
Positive = 1,
/** 反面 */
Reverse = 2,
/** 正反皆有 */
Two = 3
}
/** 布局大板 */
export interface LayoutResultBoard {
id: string;
/** 大板高度 */
boardLength: number;
/** 大板宽度 */
boardWidth: number;
}
/** 布局小板 */
export interface LayoutResultBlock {
id: string;
/** x坐标 */
x: number;
/** y坐标 */
y: number;
/** 高度 */
length: number;
/** 宽度 */
width: number;
/** 纹路类型 */
waveType: WaveType;
}
/** 优化布局结果 */
export type LayoutResult = {
/** 大板列表 */
boards: LayoutResultBoard[],
/** 小板列表其一维与boards长度对应二维为小板列表 */
blocks: LayoutResultBlock[][],
/** 优化中被使用的余料大板,这个列表中的每一个元素代表使用了一片该规格的大板 */
usedScrapBoard: LayoutResultBoard[];
};

View File

@@ -0,0 +1,392 @@
export class List<Item> extends Array<Item>
{
/** 返回符合条件的第一个元素 */
first(fn: (item: Item) => boolean): Item
{
if (this.length == 0)
return null
for (const item of this)
{
if (fn(item))
return item
}
return null
}
/** 返回符合条件的最后一元素 */
last(fn: (t: Item) => boolean): Item
{
if (this.length == 0)
return null
for (let i = this.length - 1; i >= 0; i--)
{
if (fn(this[i]))
return this[i]
}
return null
}
/** 最大值 */
max<T>(fn: (item: Item) => T): T
{
let maxV: T
for (const i of this)
{
let v = fn(i)
if (maxV == null)
{
maxV = fn(i)
}
else
{
if (v > maxV)
{
maxV = v
}
}
}
return maxV
}
/** 最小值 */
min<T>(fn: (item: Item) => T): T
{
let minV: T
for (const i of this)
{
let v = fn(i)
if (minV == null)
{
minV = fn(i)
}
else
{
if (v < minV)
{
minV = v
}
}
}
return minV
}
/** 累加 */
sum(fn: (item: Item) => number): number
{
let v: number = 0
for (const t of this)
{
v = v + fn(t)
}
return v
}
/** 平均值 */
avg(fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let sum = this.sum(fn)
return sum / this.length
}
/** 满足条件的元素数量 */
count(fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let c = 0
for (const item of this)
{
if (fn(item))
c = c + 1
}
return c
}
FindMax<T>(fn: (item: Item) => T): Item
{
return this.reduce((a: Item, b: Item): Item => fn(a) > fn(b) ? a : b)
}
}
export class ArrayExt
{
/** 返回满足条件的元素数量 */
static count<Item>(list: Item[], fn: (item: Item) => boolean): number
{
if (list.length == 0)
return 0
let c = 0
for (const item of list)
{
if (fn(item))
c = c + 1
}
return c
}
/** 移除 */
static remove<Item>(list: Item[], obj: Item)
{
let index = list.findIndex(t => t == obj)
if (index == -1)
return
list.splice(index, 1)
}
/** 返回符合条件的第一个元素 */
static first<Item>(list: Item[], fn: (item: Item) => boolean, orderFn1: (item: Item) => number = null, orderFn2: (item: Item) => number = null): Item
{
if (list.length == 0)
return null
if (orderFn1 == null)
{
for (const item of list)
{
if (fn(item))
return item
}
return null
}
let minValue1: number
let minValue2: number
let minItem: Item
for (const item of list)
{
if (fn(item) == false)
continue
let v1 = orderFn1(item)
let v2 = orderFn2 != null ? orderFn2(item) : 0
if (minValue1 == null || v1 < minValue1 || (v1 == minValue1 && v2 < minValue2))
{
minValue1 = v1
minValue2 = v2
minItem = item
}
}
return minItem
}
/** 返回符合条件的最后一元素 */
static last<Item>(list: Item[], fn: (t: Item) => boolean, orderFn1: (item: Item) => number = null, orderFn2: (item: Item) => number = null): Item
{
if (list.length == 0)
return null
if (orderFn1 == null)
{
for (let i = list.length - 1; i >= 0; i--)
{
if (fn(list[i]))
return list[i]
}
return null
}
//
let maxValue1: number
let maxValue2: number
let maxItem: Item
for (const item of list)
{
if (fn(item) == false)
continue
let v1 = orderFn1(item)
let v2 = orderFn2 ? orderFn2(item) : 0
if (maxValue1 == null || v1 > maxValue1 || (v1 == maxValue1 && v2 > maxValue2))
{
maxValue1 = v1
maxValue2 = v2
maxItem = item
}
}
return maxItem
}
/** 取最大值 */
static max<T1, T2>(list: T1[], fn: (item: T1) => T2, whereF: (item: T1) => boolean = null, defaultV: T2 = null): T2
{
let maxV: T2
for (const i of list)
{
if (whereF && whereF(i) == false)
continue
let v = fn(i)
if (maxV == undefined)
{
maxV = fn(i)
}
else
{
if (v > maxV)
{
maxV = v
}
}
}
if (maxV != undefined)
return maxV
return defaultV
}
/** 最小值 */
static min<Item, T>(list: Item[], fn: (item: Item) => T, whereF: (item: Item) => boolean = null, defaultV: T = null): T
{
let minV: T
for (const i of list)
{
if (whereF && whereF(i) == false)
continue
let v = fn(i)
if (minV == undefined)
{
minV = v
}
else
{
if (v < minV)
{
minV = v
}
}
}
if (minV != undefined)
return minV
return defaultV
}
/** 累加 */
static sum<Item>(list: Item[], fn: (item: Item) => number, wn?: (item: Item) => boolean): number
{
let v: number = 0
for (const t of list)
{
if (wn && wn(t) == false)
continue
v = v + fn(t)
}
return v
}
/** 平均值 */
static avg<Item>(list: Item[], fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let sum = ArrayExt.sum(list, fn)
return sum / this.length
}
/** 排序 */
static sortBy<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null)
{
if (fn2 == null)
return list.sort((a: Item, b: Item): number => fn(a) - fn(b))
else
return list.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(a) - fn2(b)) : fn(a) - fn(b))
}
/** 降序 排序 */
static sortByDescending<Item>(list: Item[], fn: (item: Item) => number)
{
list.sort((a: Item, b: Item): number => fn(b) - fn(a))
}
/** 排序成新的数组 */
static orderBy<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null): Item[]
{
let newList = list.concat([])
if (fn2 == null)
return newList.sort((a: Item, b: Item): number => fn(a) - fn(b))
else
return newList.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(a) - fn2(b)) : fn(a) - fn(b))
}
/** 降序成新的数组 */
static orderByDescending<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null): Item[]
{
let newList = list.concat([])
if (fn2 == null)
return list.sort((a: Item, b: Item): number => fn(b) - fn(a))
else
return list.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(b) - fn2(a)) : fn(b) - fn(a))
}
/** 分组 */
static groupBy<Item, gT>(list: Item[], fn: (item: Item) => gT): GroupItem<gT, Item>[]
{
let groups = new Array<GroupItem<gT, Item>>()
for (const item of list)
{
let key = fn(item)
let group = groups.find(t => t.key == key)
if (group == null)
{
group = new GroupItem(key)
groups.push(group)
}
group.push(item)
}
return groups
}
/**
* 选择
* let newObjectList = ArrayExt.Select(list,t=>({pA:t.name, pB:t.age + "_"+ t.month}) ) ;
*/
static Select<Item, rT>(list: Item[], fn: (item: Item) => rT): rT[]
{
let newList = new Array<rT>()
for (const t of list)
{
newList.push(fn(t))
}
return newList
}
/** 过来,并按顺序排序 */
static where<Item>(list: Item[], whereFn: (item: Item) => boolean, orderfn1: (item: Item) => number = null, orderfn2: (item: Item) => number = null): Item[]
{
let newList = list.filter(whereFn)
if (orderfn1 == null && orderfn2 == null)
return newList
return ArrayExt.sortBy(newList, orderfn1, orderfn2)
}
}
export class GroupItem<gT, Item>
{
key: gT
get count() { return this.list.length }
list: Item[]
constructor(k: gT)
{
this.key = k
this.list = []
}
push(d: Item)
{
this.list.push(d)
}
}
/**
* 对排序好的数组进行去重操作
* @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
}

View File

@@ -0,0 +1,25 @@
import * as clipperLib from 'js-angusj-clipper/web/index.js'
// nodejs style require
// 多边形和线条裁剪及偏移库
export const 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
clipperLib.NativeClipperLibRequestedFormat.WasmOnly,
).then((c) =>
{
clipperCpp.lib = c
res()
console.log('载入成功!')
})
})
}

View File

@@ -0,0 +1,369 @@
import type { PolylineProps } from 'cadapi'
import { CADFiler, Circle, Polyline, Status, VKnifToolPath, isTargetCurInOrOnSourceCur } from 'cadapi'
import type { Box3 } from 'three'
import { Vector2, Vector3 } from 'three'
import { arrayRemoveDuplicateBySort } from './ArrayExt'
import type { Curve2d } from './common/base/CAD'
import { Arc2d, Point2d, copyTextToClipboard } from './common/base/CAD'
import { CurveWrap } from './common/LayoutEngine/Curves2Parts'
// import type { Curve2d } from '../../common/base/CAD'
export class PolylineHelper {
/** 创建闭合多段线 */
static create(pts: any[], closeMark = false): Polyline {
let lined: PolylineProps[] = []
let count = pts.length
for (let i = 0; i < count; i++) {
let p0 = pts[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: p0.bul || 0 })
}
let pls = new Polyline(lined)
pls.CloseMark = closeMark
return pls
}
static createByCurve2d(curs: Curve2d[], closeMark = true): Polyline {
let lined: PolylineProps[] = []
for (let cur of curs) {
let x = cur.StartPoint.m_X
let y = cur.StartPoint.m_Y
let bul = 0
if (cur instanceof Arc2d)
bul = cur.Bul || 0
lined.push({ pt: new Vector2(x, y), bul })
}
let pls = new Polyline(lined)
pls.CloseMark = true
return pls
}
static createByPts(pts: any[], buls: number[], closeMark = false): Polyline {
let plps: PolylineProps[] = []
let count = pts.length
for (let i = 0; i < count; i++) {
let p0 = pts[i]
plps.push({ pt: new Vector2(p0.x, p0.y), bul: buls[i] })
}
let pls = new Polyline(plps)
pls.CloseMark = closeMark
return pls
}
static getSimplePoints(pts: any[], offset: number): any[] {
let pl = PolylineHelper.create(pts)
pl.CloseMark = true
let cureW = new CurveWrap(pl, offset, true)
let pts2 = cureW.GetOutsidePoints()
arrayRemoveDuplicateBySort(pts2, (p1, p2) => (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) < 1e-2)
return pts2
}
static createByWidthLength(w: number, l: number): Polyline {
let plps: PolylineProps[] = []
plps.push({ pt: new Vector2(0, 0), bul: 0 })
plps.push({ pt: new Vector2(w, 0), bul: 0 })
plps.push({ pt: new Vector2(w, l), bul: 0 })
plps.push({ pt: new Vector2(0, l), bul: 0 })
let pls = new Polyline(plps)
pls.CloseMark = true
return pls
}
/** 多段线,添加位置移动 返回新多段线 */
static moveTo(pl: Polyline, x: number, y: number): Polyline {
let lindData = pl.LineData
let pos = pl.Position
let newPts: PolylineProps[] = []
for (let p of lindData) {
let nx = p.pt.x + pos.x + x
let ny = p.pt.y + pos.y + y
if (ny < 7.9) {
// console.log('修边小于 7.9????', ny)
}
let bul = p.bul
newPts.push({ pt: new Vector2(nx, ny), bul })
}
let npl = new Polyline(newPts)
npl.CloseMark = pl.CloseMark
return npl
}
/** 重设 多段线的几点 */
static resetPosition(pl: Polyline): Polyline {
let lindData = pl.LineData
let pos = pl.Position
let newPts: PolylineProps[] = []
for (let p of lindData) {
let nx = p.pt.x + pos.x
let ny = p.pt.y + pos.y
let bul = p.bul
newPts.push({ pt: new Vector2(nx, ny), bul })
}
let npl = new Polyline(newPts)
npl.CloseMark = pl.CloseMark
return npl
}
/** 获得v型刀走刀路径 */o
static getVModelPoints(pl: Polyline, depth: number, ang: number): any[] {
// let ang = Math.PI * (0.5 * angle) / 180 ;
let ps = []
let bx = pl.Position.x
let by = pl.Position.y
if (ang > 0.01) {
let rt = VKnifToolPath(pl, depth, ang / 2)
ps = rt.map((t) => { return { x: t.pt.x + bx, y: t.pt.y + by, z: t.pt.z, bul: t.bul, r: 0 } })
}
else {
ps = pl.LineData.map((t) => { return { x: t.pt.x + bx, y: t.pt.y + by, z: 0, bul: t.bul, r: 0 } })
}
for (let i = 0; i < ps.length; i++) {
let p = ps[i]
if (p.bul == 0)
continue
let p2 = (i == ps.length - 1 ? ps[0] : ps[i + 1])
let r = this.getArcRadius(p.x, p.y, p2.x, p2.y, p.bul)
p.r = r
}
return ps
}
static ConverPolyLin2Circle(polyline: Polyline, fuzz = 0.1): Circle | undefined {
let box = polyline.BoundingBox
let size = box.getSize(new Vector3())
if (!this.equaln(size.x, size.y, fuzz))// 盒子四方
return undefined
let circleLength = 2 * Math.PI * size.x
if (!this.equaln(circleLength, polyline.Length, fuzz * 2))
return undefined
let circleArea = Math.PI * size.x * size.x
if (!this.equaln(circleArea, polyline.Area, fuzz * 2))
return undefined
let r = size.x// 必须备份(因为我们要复用这个vector变量)
return new Circle(box.getCenter(size), r)
}
// 有问题
static getVModelPoints_offset(pl: Polyline, offset: number, depth: number, angle: number) {
let npl = offset == 0 ? pl : pl.GetOffsetCurves(offset)[0]
// if(offset != 0)
// {
// ClipboardTest.write2PolyLine(pl,npl);
// }
return PolylineHelper.getVModelPoints(npl, depth, angle)
}
static getArcRadius(x1: number, y1: number, x2: number, y2: number, bul: number): number {
let bul2 = Math.abs(bul)
let d = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) / 2
return 0.5 * d * (1 + bul2 ** 2) / bul2
}
// 圆 转 多段线
static cicleToPolyline(c: Circle): Polyline {
let arcs = c.GetSplitCurves([0, 0.5])
let pl = Polyline.FastCombine(arcs)
return pl
}
/** 判断多段线是否 重叠 */
static isIntersect(pl1: Polyline, pl2: Polyline): boolean {
let box1 = this.getBox(pl1)
let box2 = this.getBox(pl2)
if (!box1.intersectsBox(box2))
return false // 肯定不相交
let ipts = pl1.IntersectWith(pl2, 0)
if (ipts.length === 0) {
if (pl1.Area > pl2.Area)// 缓存面积
{
if (isTargetCurInOrOnSourceCur(pl1, pl2))
return true // 包含
}
else {
if (isTargetCurInOrOnSourceCur(pl2, pl1))
return true // 包含
}
return false
}
else {
return true // 有交点 一定有交集
}
}
// 多段线 圆弧合并
static mergeCurve(pl2: Polyline): Polyline {
const curves = pl2.Explode()
arrayRemoveDuplicateBySort(curves, (c1, c2) => {
return c1.Join(c2) === Status.True
})
return Polyline.FastCombine(curves)
}
/**
* pl2 包含在pl1 内
*
*/
// static isInside(pl1:Polyline,pl2:Polyline):boolean
// {
// let box1 = this.getBox(pl1);
// let box2 = this.getBox(pl2);
// if (!box1.intersectsBox(box2)) return false; //肯定不相交
// let ipts = pl1.IntersectWith(pl2, 0);
// if (ipts.length > 0) return true; //有交点 一定有交集
// }
/**
* 两片板的干涉检查
*
* @param pl1
* @param pls1_inner
* @param pls1_model
* @param pl2
* @param pls2_inner
* @param pls2_model
* @returns
*/
static isOverLap(pl1: Polyline, pls1_inner: Polyline[], pls1_model: Polyline[], pl2: Polyline, pls2_inner: Polyline[], pls2_model: Polyline[]) {
// 是否干涉, 被包含在造型洞,不算干涉
let isOverlap = this.boxIsOverlap(pl1, pls1_inner, pl2, pls2_inner)
if (isOverlap)
return true
// 造型 2v 刀路 是否干涉
for (let pl1_model of pls1_model) {
if (pl1_model.IntersectWith(pl2, 0).length > 0)
return true
for (let pl2_inner of pls2_inner) {
if (pl1_model.IntersectWith(pl2_inner, 0).length > 0)
return true
}
}
for (let pl2_model of pls2_model) {
if (pl2_model.IntersectWith(pl1, 0).length > 0)
return true
for (let pl1_inner of pls1_inner) {
if (pl2_model.IntersectWith(pl1_inner, 0).length > 0)
return true
}
}
return false
}
private static boxIsOverlap(pl1: Polyline, pls1_inner: Polyline[], pl2: Polyline, pls2_inner: Polyline[]) {
let box1 = this.getBox(pl1)
let box2 = this.getBox(pl2)
if (!box1.intersectsBox(box2))
return false // 肯定不相交
let ipts = pl1.IntersectWith(pl2, 0)
if (ipts.length > 0)
return true // 有交点 一定有交集
if (pl1.Area > pl2.Area)// 缓存面积
{
if (isTargetCurInOrOnSourceCur(pl1, pl2)) // pl1包含 pl2
{
for (let mpl of pls1_inner) // 如果pl1有造型洞包含pl2 则表示不干涉返回false
{
if (isTargetCurInOrOnSourceCur(mpl, pl2) == true)
return false
}
return true
}
}
else {
if (isTargetCurInOrOnSourceCur(pl2, pl1)) // pl2包含 pl1
{
for (let mpl of pls2_inner) // 如果pl2有造型洞包含pl1 则表示不干涉返回false
{
if (isTargetCurInOrOnSourceCur(mpl, pl1) == true)
return false
}
return true
}
}
return false
}
/** 判断 点是否在多段线内 */
static isPointInPolyline(pl1: Polyline, x: number, y: number): boolean {
return pl1.PtInCurve(new Vector3(x, y, 0))
}
static getBox(pl1: Polyline): Box3 {
if (!pl1.box_tp)
pl1.box_tp = pl1.BoundingBox
return pl1.box_tp as Box3
}
static getArea(pl1: Polyline): number {
if (!pl1.area_tp)
pl1.area_tp = pl1.Area
return pl1.area_tp as number
}
static getPath(pl: Polyline): Path2D {
let path = new Path2D()
let p0 = pl.LineData[0].pt
path.moveTo(p0.x, p0.y)
for (let i = 0; i < pl.LineData.length; i++) {
let p0 = pl.LineData[i].pt
let p1 = (i == pl.LineData.length - 1) ? pl.LineData[0].pt : pl.LineData[i + 1].pt
let bul = pl.LineData[i].bul
if (bul == 0) {
path.lineTo(p1.x, p1.y)
}
else {
let arc = new Arc2d(new Point2d(p0.x, p0.y), new Point2d(p1.x, p1.y), bul)
path.arc(arc.m_Center.m_X, arc.m_Center.m_Y, arc.m_Radius, arc.m_StartAngle, arc.m_EndAngle, bul < 0)
}
}
path.closePath()
return path
}
static equaln(v1: number, v2: number, fuzz = 1e-5) {
return Math.abs(v1 - v2) <= fuzz
}
static toClipboard(en: Polyline | any) {
let f = new CADFiler()
f.Write(1)// 实体个数
f.WriteObject(en)
copyTextToClipboard(f.ToString())
}
static getStrPLs(ens: any[]) {
if (ens.length == 0)
return ''
let f = new CADFiler()
f.Write(ens.length)// 实体个数
for (let en of ens)
f.WriteObject(en)
return f.ToString()
}
}

View File

@@ -0,0 +1,85 @@
import { Vector2 } from './Vector2'
interface P {
x: number
y: number
}
export interface IOffset {
negativeOffset: number
positiveOffset: number
}
/** 点p到线段P1P2 的最短距离的平方,线段不延伸 */
function GetSqSegDist(p: P, p1: P, p2: P): number {
let x = p1.x
let y = p1.y
let dx = p2.x - x
let dy = p2.y - y
if (dx !== 0 || dy !== 0)// 不是0长度线
{
const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)
if (t > 1) {
x = p2.x
y = p2.y
}
else if (t > 0) {
x += dx * t
y += dy * t
}
}
dx = p.x - x
dy = p.y - y
return dx * dx + dy * dy
}
function CrossVector2(a: P, b: P)
{
return a.x * b.y - a.y * b.x
}
// Ramer-Douglas-Peucker algorithm 用于简化曲线或折线
function SimplifyDPStep(points: P[], first: number, last: number, sqTolerance: number, simplified: P[], offset: IOffset): void {
let maxSqDist = 0
let index: number
const fp = points[first]
const lp = points[last]
for (let i = first + 1; i < last; i++) {
const p = points[i]
const sqDist = GetSqSegDist(p, fp, lp)
if (sqDist > maxSqDist) {
index = i
maxSqDist = sqDist
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1)
SimplifyDPStep(points, first, index, sqTolerance, simplified, offset)
simplified.push(points[index])
if (last - index > 1)
SimplifyDPStep(points, index, last, sqTolerance, simplified, offset)
}
else {
// 记录偏移
const v = new Vector2(lp.x - fp.x, lp.y - fp.y).normalize()
for (let i = first + 1; i < last; i++) {
const p = points[i]
const offsetDist = -CrossVector2(v, { x: p.x - fp.x, y: p.y - fp.y })
offset.positiveOffset = Math.max(offset.positiveOffset, offsetDist)
offset.negativeOffset = Math.min(offset.negativeOffset, offsetDist)
}
}
}
// Ramer-Douglas-Peucker 算法 用于简化曲线或折线
export function SimplifyDouglasPeucker(points: P[], sqTolerance: number): [P[], IOffset] {
const last = points.length - 1
const simplified: P[] = [points[0]]
const offset: IOffset = { negativeOffset: 0, positiveOffset: 0 }
SimplifyDPStep(points, 0, last, sqTolerance, simplified, offset)
simplified.push(points[last])
return [simplified, offset]
}

View File

@@ -0,0 +1,126 @@
export class StringFormat
{
/**
* 对Date的扩展将 Date 转化为指定格式的String
* 月(M)、日(d)、12小时(h)、24小时(H)、分(m)、秒(s)、周(E)、季度(q) 可以用 1-2 个占位符
* 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* eg:
* (new Date()).pattern("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
* (new Date()).pattern("yyyy-MM-dd E HH:mm:ss") ==> 2009-03-10 二 20:09:04
* (new Date()).pattern("yyyy-MM-dd EE hh:mm:ss") ==> 2009-03-10 周二 08:09:04
* (new Date()).pattern("yyyy-MM-dd EEE hh:mm:ss") ==> 2009-03-10 星期二 08:09:04
* (new Date()).pattern("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
*/
static Date(date: Date, fmt): string
{
let o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours() % 12 == 0 ? 12 : date.getHours() % 12, // 小时
'H+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S': date.getMilliseconds(), // 毫秒
}
let week = {
0: '/u65e5',
1: '/u4e00',
2: '/u4e8c',
3: '/u4e09',
4: '/u56db',
5: '/u4e94',
6: '/u516d',
}
if (/(y+)/.test(fmt))
{
fmt = fmt.replace(RegExp.$1, (`${date.getFullYear()}`).substr(4 - RegExp.$1.length))
}
if (/(E+)/.test(fmt))
{
fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length > 1) ? (RegExp.$1.length > 2 ? '/u661f/u671f' : '/u5468') : '') + week[`${date.getDay()}`])
}
for (let k in o)
{
if (new RegExp(`(${k})`).test(fmt))
{
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : ((`00${o[k]}`).substr((`${o[k]}`).length)))
}
}
return fmt
}
/** 返回数字的固定小数点 123.00 */
static number(value: number, bit: number): string
{
return value?.toFixed(bit).toString()
}
static toFixed(value: number, bit: number = 3): number
{
let tt = 10 ** bit
return Math.round(value * tt) / tt
}
static filterIllegalChar(str: string): string
{
// var pattern = new RegExp("[/:*?'<>|\\]");
// var rs = "";
// for (var i = 0; i < str.length; i++)
// {
// rs = rs + str.substr(i, 1).replace(pattern, '');
// }
// return rs;
let rt = str.replace(/\\/g, '').replace(/\:/g, '').replace(/\*/g, '').replace(/\?/g, '').replace(/\"/g, '').replace(/\</g, '').replace(/\>/g, '').replace(/\|/g, '').replace(/\'/g, '')
return rt
}
/** 文本格式化 */
static format(str: string, ...args: any[]): string
{
let data = args
let tmpl = str
for (const item of tmpl.matchAll(/\{(.+?)\}/g))
{
let parts = item[1].split(',').map(i => i.trim())
let index = Number(parts[0])
let arg = data[index]
let val = (arg || '').toString() // 默认
if (arg instanceof Date) // 日期
{
let fm = 'MM-dd HH;mm'
if (parts.length > 1)
{
fm = parts[1]
}
val = this.Date(arg, fm)
}
if (parts.length > 1 && parts[1][0] === '#')
{
// {2,#3} -> 数字 转成3位 001,...023,
val = val.padStart(Number(parts[1].substring(1)), '0')
}
tmpl = tmpl.replace(item[0], val)
}
return tmpl
}
/** 实现右对齐,右边填充 (25,4,'*') => '25**' */
static PadEnd(num: number, totalWidth: number, paddingChar: string): string
{
let str = num.toString()
return str.padEnd(totalWidth, paddingChar[0])
}
/** 实现右对齐,左边填充 (25,4,'0') => '0025' */
static PadStart(num: number, totalWidth: number, paddingChar: string): string
{
let str = num.toString()
return str.padStart(totalWidth, paddingChar[0])
}
}

View File

@@ -0,0 +1,42 @@
import { ConfigBase } from "./models/config";
/**
* 加工处理器上下文
*/
export abstract class ProcessorContext<TInput,TOutput,TConfig extends ConfigBase>{
/**
* 输入数据
*/
public input?:TInput;
/**
* 合并配置文件与临时输入参
*/
public params?:TConfig;
/**
* 输出数据
*/
public output?:TOutput;
}
/**
* 处理器基类
*/
export abstract class ProcessorBase<TInput,TOutput,TConfig extends ConfigBase> {
public abstract get name():string;
public abstract get version(): string;
public abstract exec(context:ProcessorContext<TInput,TOutput,TConfig>):Promise<void>|void
}
export interface resultInfo {
code: ResCodeType;
data?: any;
success?: boolean;
message?: string;
}
export enum ResCodeType{
SUCCESS = 1,
ERROR = 0,
WARNING = -1
}

View File

@@ -0,0 +1,187 @@
/**
* 删除数组中指定的元素,返回数组本身
* @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
}
/** 原地更新数组,注意这个函数并不会比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 (let 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()
}
/** 数组2元素合并到数组1末尾 */
// 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
}
/** 查找数组中最大的元素 */
export function arrayMax<T>(arr: T[], f: (item: T) => number = a => (a as unknown as number)): [T, number]
{
let max = Number.NEGATIVE_INFINITY
let maxIndex = -1
for (let i = 0; i < arr.length; i++)
{
let item = arr[i]
let v = f(item)
if (v > max)
{
maxIndex = i
max = v
}
}
return [arr[maxIndex], maxIndex]
}

View File

@@ -0,0 +1,38 @@
import type { PlaceBlock } from "../confClass"
/** 拖拉的小板顶点 */
export class BlockBorderPoint
{
/** 板 */
block: PlaceBlock
/** 原坐标x */
x: number
/** 原坐标y */
y: number
/** 大板坐标X */
placeX: number
/** 大板坐标Y */
placeY: number
/** 拖拉点距切割后板的坐标偏移x */
bx = 0
/** 拖拉点距切割后板的坐标偏移x */
by = 0
/** 在placed线框中的序号 */
curveIndex: number
/** 在板的位置: 10:基点(左下点);1:右下角;2:右上角;3:左上角; -1:表示异形非顶点 areaID */
posId: number
constructor(block: PlaceBlock, x: number, y: number, index: number, posId: number, bx = 0, by = 0)
{
this.block = block
this.x = x
this.y = y
this.placeX = (block ? block.placeX : 0) + x
this.placeY = (block ? block.placeY : 0) + y
this.curveIndex = index
this.posId = posId
this.bx = bx
this.by = by
}
}

View File

@@ -0,0 +1,66 @@
import { Arc2d,Point2d } from "./base/CAD"
import type { PlaceMaterial } from "../confClass"
export function getMaterialSealEdge(pm: PlaceMaterial)
{
let ext = 30 // 每条边 浪费的长度
let fbs = []
for (let block of pm.blockList)
{
if (!block.isUnRegular)
{
pushLength(block.sealLeft, block.length)
pushLength(block.sealRight, block.length)
pushLength(block.sealBottom, block.width)
pushLength(block.sealTop, block.width)
}
else
{
for (let i = 0; i < block.orgPoints.length; i++)
{
let p1 = block.orgPoints[i]
if (Math.abs(p1.sealSize) < 0.001)
continue
let j = i + 1
if (j == block.orgPoints.length)
j = 0
let p2 = block.orgPoints[j]
let len = 0
if (p1.curve == 0)
{
len = Math.sqrt((p1.pointX - p2.pointX) * (p1.pointX - p2.pointX) + (p1.pointY - p2.pointY) * (p1.pointY - p2.pointY))
}
else
{
let arc = new Arc2d(new Point2d(p1.pointX, p1.pointY), new Point2d(p2.pointX, p2.pointY), p1.curve)
len = Math.abs(arc.m_Radius * arc.m_AllAngle)
}
pushLength(p1.sealSize, len)
}
}
}
let rlfbs = []
// 转换成米
for (let key in fbs)
{
let rt = fbs[key]
rt.l = Math.ceil(rt.l / 100) / 10
rlfbs.push(rt)
}
return rlfbs
function pushLength(fb: number, len: number)
{
if (Math.abs(fb) < 0.001)
return//
let str = fb.toFixed(2)
let sul = fbs[str]
if (sul == null)
{
sul = { t: fb, l: 0 }
fbs[str] = sul
}
sul.l += len + ext
}
}

View File

@@ -0,0 +1,183 @@
import { Vector2 } from './Vector2.js'
import type { Point } from '../common/Vector2.js'
export class Box2
{
min: Vector2
max: Vector2
constructor(min = new Vector2(+Number.POSITIVE_INFINITY, +Number.POSITIVE_INFINITY), max = new Vector2(Number.NEGATIVE_INFINITY, Number.NEGATIVE_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: Iterable<Point>): Box2
{
this.makeEmpty()
for (let p of points)
{
this.expandByPoint(p)
}
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 = +Number.POSITIVE_INFINITY
this.max.x = this.max.y = Number.NEGATIVE_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: Point): Box2
{
this.min.min(point)
this.max.max(point)
return this
}
expandByVector(vector: Vector2): Box2
{
this.min.sub(vector)
this.max.add(vector)
return this
}
expandByScalar(scalar: number): Box2
{
this.min.addScalar(-scalar)
this.max.addScalar(scalar)
return this
}
containsPoint(point: Vector2): boolean
{
if (point.x < this.min.x || point.x > this.max.x
|| point.y < this.min.y || point.y > this.max.y)
{
return false
}
return true
}
containsBox(box: Box2): boolean
{
if ((this.min.x <= box.min.x) && (box.max.x <= this.max.x)
&& (this.min.y <= box.min.y) && (box.max.y <= this.max.y))
{
return true
}
return false
}
getParameter(point: Vector2, result: Vector2 = new Vector2()): Vector2
{
// This can potentially have a divide by zero if the box
// has a size dimension of 0.
return result.set(
(point.x - this.min.x) / (this.max.x - this.min.x),
(point.y - this.min.y) / (this.max.y - this.min.y),
)
}
intersectsBox(box: Box2): boolean
{
// using 6 splitting planes to rule out intersections.
if (box.max.x < this.min.x || box.min.x > this.max.x
|| box.max.y < this.min.y || box.min.y > this.max.y)
{
return false
}
return true
}
clampPoint(point: Vector2, result: Vector2 = new Vector2()): Vector2
{
return result.copy(point).clamp(this.min, this.max)
}
private static _distanceToPoint_v1 = new Vector2()
distanceToPoint(point: Vector2): number
{
const v1 = Box2._distanceToPoint_v1
const clampedPoint = v1.copy(point).clamp(this.min, this.max)
return clampedPoint.sub(point).length()
}
intersect(box: Box2): Box2
{
this.min.max(box.min)
this.max.min(box.max)
return this
}
union(box: Box2): Box2
{
this.min.min(box.min)
this.max.max(box.max)
return this
}
translate(offset: Point): Box2
{
this.min.add(offset)
this.max.add(offset)
return this
}
equals(box: Box2): boolean
{
return box.min.equals(this.min) && box.max.equals(this.max)
}
};

View File

@@ -0,0 +1,25 @@
import * as clipperLib from 'js-angusj-clipper/web/index.js'
// nodejs style require
export const 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
clipperLib.NativeClipperLibRequestedFormat.WasmOnly,
).then((c) =>
{
clipperCpp.lib = c
res()
console.log('载入成功!')
})
})
}

View File

@@ -0,0 +1,62 @@
interface Vec2 { x: number; y: number }
export type CompareVectorFn = (v1: Vec2, v2: Vec2) => number
const comparePointCache: Map<string, CompareVectorFn> = new Map()
const ALLKEY = ['x', 'X', 'y', 'Y', 'z', 'Z']
const KEY = ['x', 'y', 'z']
/**
* 构建返回一个用来排序的函数.根据key创建排序规则.
*
* 当key = "xyz" 时,点集按 x从小到大,y从小到大 z从小到大
* key = "X" 时,点集按 x从大到小
* 以此类推.
*
* 例子:
* let pts:Vector3[] =...;
* pts.sort(comparePoint("x")); //x从小到大排序
* pts.sort(comparePoint("zX")); //z从小到大 x从大到小
*
* @export
* @param {string} sortKey
*/
export function ComparePoint(sortKey: string): CompareVectorFn
{
if (comparePointCache.has(sortKey))
return comparePointCache.get(sortKey)
let sortIndex = []
for (let char of sortKey)
{
let index = ALLKEY.indexOf(char)
let i2 = index / 2
let ci = Math.floor(i2)
sortIndex.push([KEY[ci], i2 > ci ? 1 : -1])
}
let compareFunction = (v1: Vec2, v2: Vec2): number =>
{
if (!v1)
return -1
if (!v2)
return 1
for (let s of sortIndex)
{
let vv1 = v1[s[0]]
let vv2 = v2[s[0]]
if (vv1 === vv2)
continue
if (vv2 > vv1)
return s[1]
else return -s[1]
}
return 0
}
comparePointCache.set(sortKey, compareFunction)
return compareFunction
}

View File

@@ -0,0 +1,9 @@
import convexHull from 'monotone-convex-hull-2d'
import type { Point } from '../common/Vector2'
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])
}

View File

@@ -0,0 +1,31 @@
/**
* 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++]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,365 @@
import { BlockHelper } from './BlockHelper.js'
import { BlockSizePlus } from './BlockSizePlus.js'
import { resetPlaceBoard } from './PlaceBase.js'
import { NcCustomized } from '@/imes/biz/NcCustomized.js'
import { FaceType, HoleType, HoleArrange, PlaceBlock, PlaceMaterial, PlaceBoard } from '../../confClass.js'
/** 计算大板中所有的小板的正反面孔造型数量,让正面加工时间接近 比例 30% */
export async function TurnOverWithDoneRate(pm: PlaceMaterial, cncData: boolean, doneRate: number, sp_h: number, sp_m: number) {
let i = 0
console.time(`${pm.fullName} full use time`)
for (let pb of pm.boardList) {
// if (pb.IsOddmengt) continue;
i++
// console.log(`${pm.FullName} bid=${i} 开始计算`);
await TurnOverWithDoneRate_pb(pm, pb, cncData, doneRate, sp_h, sp_m, i)
}
console.timeEnd(`${pm.fullName} full use time`)
}
/** 计算大板中所有的小板的正反面孔造型数量,让正面加工时间接近 比例 30% */
export async function TurnOverWithDoneRate_pb(pm: PlaceMaterial, pb: PlaceBoard, useCncData: boolean, doneRate: number, sp_h: number, sp2: number, i) {
// 孔 0.5秒/个
// 造型, 16秒/米 , 0.016秒/mm
let sp_m = sp2 * 0.001
let rate = doneRate * 0.01
// 一. 先求出大板所有小板,当前正反面加工时间,是否可翻转
let tm: any = [] //
let t1_z = 0 // 不可翻 正面加工时间 合计
let t1_f = 0 // 不可翻 反面加工时间 合计
let t2_z = 0 // 可翻 正面加工时间 合计
let t2_f = 0 // 可翻 反面加工时间 合计
let t_full = 0 // 所有加工时间 合计
let t_throngh = 0// 挖穿的造型的时间,
for (let block of pb.blockList) {
let tc = 0 // 挖穿;
let tz = 0 // 当前加工面 时间
let tf = 0 // 反面 时间
let holes = block.holeListFaceA
let models = block.modelListFaceA
if (useCncData) // 使用cnc 数据
{
holes = block.isTurnOver ? block.holeListOrgFaceB.filter(t => t.isCutting == false) : block.holeListOrgFaceA.filter(t => t.isCutting == false)
models = block.isTurnOver ? block.modelListOrgFaceB.filter(t => t.isCutting == false) : block.modelListOrgFaceA.filter(t => t.isCutting == false)
}
tz += holes.length * sp_h // 正面孔时间
for (let model of models) {
let len = 0
for (let i = 0; i < model.pointList.length - 1; i++) {
let p0 = model.pointList[i]
let p1 = model.pointList[i + 1]
if (p0 == null || p1 == null)
continue
len += Math.sqrt((p0.pointX - p1.pointX) ** 2 + (p0.pointY - p1.pointY) ** 2)
}
if (model.depth >= block.thickness - 0.001) {
tc += len * sp_m
}
else {
tz += len * sp_m
}
}
let holes2 = block.holeListFaceB
let models2 = block.modelListFaceB
if (useCncData) // 使用cnc 数据
{
holes2 = block.isTurnOver ? block.holeListOrgFaceA.filter(t => t.isCutting == false) : block.holeListOrgFaceB.filter(t => t.isCutting == false)
models2 = block.isTurnOver ? block.modelListOrgFaceB.filter(t => t.isCutting == false) : block.modelListOrgFaceB.filter(t => t.isCutting == false)
}
tf += holes2.length * sp_h
for (let model of models2) {
let len = 0
for (let i = 0; i < model.pointList.length - 1; i++) {
let p0 = model.pointList[i]
let p1 = model.pointList[i + 1]
if (p0 == null || p1 == null)
continue
len += Math.sqrt((p0.pointX - p1.pointX) ** 2 + (p0.pointY - p1.pointY) ** 2)
}
tf += len * sp_m
}
if (tz + tc == 0 && tf == 0)
continue // 没有加工
let canTurn = true
if (block.isUnRegular)
canTurn = false // 异形不能翻
if (canTurn && block.placeHole != 2)
canTurn = false // 非可翻转的
if (canTurn && hasChildBlock(block, pb.blockList))
canTurn = false// 里面有小板,不能翻
t_throngh += tc
t_full += (tc + tz + tf)
if (canTurn == false) // 不能翻
{
t1_z += tz
t1_f += tf
}
else // 可以翻 ,
{
if (Math.abs(tz - tf) > sp_h) // 翻面效果很差, 时间小于一个孔 ,翻不翻无所谓,
{
tm.push({ bno: block.blockNo, z: tz, f: tf, dis: tz - tf, child: [] })
}
t2_z += tz
t2_f += tf
}
}
// 二. 计算 不可翻的小板 正面加工 与理论值 相差多少
let t2_L = t_full * rate - t1_z - t_throngh // 剩下加工正面的时间 理论值 ( 理论值- 当前正面加工时间,就是剩下可翻的板 正面加工时间 )
let tz: any = t1_z + t_throngh + t2_z // 当前正面时间
let t2_dis = t2_z - t2_L // 理论值与 当前剩下正面加工时间的差值 ,应该要节省的时间总
// 正面加工时间 比例 接近预定于的比例 。 则不再计算
if (Math.abs(100 * t2_dis / t_full) < 3)
return { tz, fz: t_full - tz, full: t_full }
// 三.接下来的问题就是在 tm 中 找出 翻面加工省时 合计 最接近 t2_dis 的集合出来.
let best_list: any = [] // 最优解 需要翻面的
let best_v = t2_dis // 差值最大 默认当前
// 四.将tm 转换成 最多16个可翻面 的数组
tm.sort((a, b) => a.dis - b.dis) // 按小到大排序
// 精简 tm //将tm中 翻面效果 相反的 且大的 剔除掉
if (tm.length > 16) {
if (t2_dis > 0) {
for (let i = tm.length - 1; i >= 0; i--) {
if (tm[i].dis < -t2_dis) {
tm.splice(i, 1)
}
}
}
else {
for (let i = tm.length - 1; i >= 0; i--) {
if (tm[i].dis > -t2_dis) {
tm.splice(i, 1)
}
}
}
}
if (tm.length == 0)
return { tz, fz: t_full - tz, full: t_full }
let blocks: any = []
for (let b of pb.blockList) {
blocks[b.blockNo] = b
}
// 可翻转小板集合,精简
tm = smallerBlocks(tm)
let rt = await getBestTurnBlocks(pm, i, tm, best_v, t_full)
best_list = rt.result
best_v = rt.v
// 四. 将最优解中的小板 翻面
for (let m of best_list) {
tz -= m.dis
let bs: any = []
let block: any = blocks[m.bno]
bs.push(block)
for (let no of m.child) {
bs.push(blocks[no])
}
for (let block of bs) {
let orgStyle = block.placeStyle
let newStyle = BlockHelper.getTurnedPlaceStyle(orgStyle, 0)
let orgPlaceX = block.placeX - block.placeOffX
let orgPlaceY = block.placeY - block.placeOffY
let offset = BlockSizePlus.getOffDis(block, newStyle)
let newPlaceX = orgPlaceX + offset.x
let newPlaceY = orgPlaceY + offset.y
block.placeX = newPlaceX
block.placeY = newPlaceY
block.placeOffX = offset.x
block.placeOffY = offset.y
// 修改小板的放置方式
BlockHelper.resetPlaceStyle(block, newStyle)
}
}
// 重设pb
if (best_list.length > 0)
resetPlaceBoard(pm, pb)
return { tz, fz: t_full - tz, full: t_full }
}
/** 获取加工面processMode: 0开料机加工 1开料机CNC组合加工 2定制加工 */
export function getDoFace(block: PlaceBlock, processMode: number = 0): boolean {
// 模式0: 开料机处理排钻, 造型, 采用大孔面作为正面的模式. 正面多加工, cnc多加工
if (processMode == 0) {
// 先考虑 设计开料面
if (block.placeHole == HoleArrange.FRONT)
return true
if (block.placeHole == HoleArrange.BACK)
return false
// 造型单面 作为开料面
if (block.modelCountFront() > 0 && block.modelCountBack() == 0)
return true
if (block.modelCountFront() == 0 && block.modelCountBack() > 0)
return false
// 优先考虑 大孔面 多孔面
return getHoleFaceMore(block)
}
// 造型单面 作为开料面
if (block.modelCountFront() > 0 && block.modelCountBack() == 0)
return true
if (block.modelCountFront() == 0 && block.modelCountBack() > 0)
return false
return true
}
/** 大孔/孔多作为正面 */
function getHoleFaceMore(block: PlaceBlock): boolean {
if (!block.blockDetail) {
console.log('getHoleFaceMore: blockDetail is undefind', block)
return true
}
// 优先考虑 大孔面
let bigHole = block.blockDetail.holes.find(t => t.holeType == HoleType.BIG_HOLE)
if (bigHole)
return bigHole.face == FaceType.FRONT
// 多孔面
if (block.blockDetail.holeListFaceA.length > block.blockDetail.holeListFaceB.length)
return true
if (block.blockDetail.holeListFaceA.length < block.blockDetail.holeListFaceB.length)
return false
return true
}
// 大孔/孔多 作为正面
function getHoleFace_more(block: PlaceBlock): boolean {
// 优先考虑 大孔面
let bigHole = block.holes().find(t => t.holeType == HoleType.BIG_HOLE)
if (bigHole)
return bigHole.face == FaceType.FRONT
// 多孔面
if (block.holeCountFront > block.holeCountBack)
return true
if (block.holeCountFront < block.holeCountBack)
return false
return true
}
// 非大孔/孔少 作为正面
function getHoleFace_less(block: PlaceBlock): boolean {
// 优先考虑 大孔面
let bigHole = block.holes().find(t => t.holeType == HoleType.BIG_HOLE)
if (bigHole)
return bigHole.face != FaceType.FRONT
// 少孔面
if (block.holeCountFront > block.holeCountBack)
return false
if (block.holeCountFront < block.holeCountBack)
return true
return true
}
// 可翻转小板集合,精简
function smallerBlocks(tm: any[]): any[] {
if (tm.length <= 30)
return tm
let time = 1
if (tm.length > 40)
time = 2
if (tm.length > 60)
time = 3
if (tm.length > 80)
time = 4
if (tm.length > 100)
time = 5
if (tm.length > 120)
time = 10
let newtm: any = []
let newLength = Math.ceil(tm.length / time)
for (let t = 0; t < newLength; t++) {
let block0 = tm[t * time]
block0.child = [] // bno: block.BlockNo, z: t1, f: t2, dis: t1 - t2
for (let i = 1; i < time; i++) {
let j = t * time + i
if (j >= tm.length)
break
let blockNext = tm[j]
block0.child.push(blockNext.bno)
block0.z += blockNext.z
block0.f += blockNext.f
block0.dis += blockNext.dis
}
newtm.push(block0)
}
return newtm
}
async function getBestTurnBlocks(pm, pid, tm: any[], best_v, t_full) {
let result = []
let result_v = best_v
let disRate = 111111
return new Promise<any>((resolve, reject) => {
let num1 = 1
let num2 = 2 ** tm.length - 1
let isOK = false
// const worker = new Worker(new URL('./FaceDoTimeWorker', import.meta.url))
// worker.onmessage = (res) =>
// {
// let tmp = res.data.tmp
// let v = res.data.v
// let rate = res.data.rate
// result = tmp
// result_v = v
// disRate = rate
// if (res.data.ok == 1)
// {
// worker.terminate() // 关闭
// resolve({ result, v: result_v })
// isOK = true
// }
// }
// worker.postMessage({ num1, num2, blocks: tm, disTime: best_v, fullTime: t_full })
// Sleep(3000).then(() =>
// {
// if (isOK)
// return
// resolve({ result, v: result_v })
// // console.log(`${pm.FullName} bid=${pid} ${tm.length} 超时3秒`);
// })
})
}
/** 判断block 里头有没有小板 */
function hasChildBlock(block: PlaceBlock, blocks: PlaceBlock[]): boolean {
for (let b of blocks) {
if (b.blockNo == block.blockNo)
continue
if (b.placeX > block.placeX && b.placeX + b.placeWidth < block.placeX + block.placeWidth && b.placeY > block.placeY && b.placeY + b.placeLength < block.placeY + block.placeLength)
return true
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,300 @@
import { isTargetCurInOrOnSourceCur } from 'cadapi'
import type { Polyline } from 'cadapi'
import { PolylineHelper } from '../../common/LayoutEngine/PolylineHelper.js'
import { PlaceBlock,PlaceBoard,PlaceMaterial } from '../../confClass.js'
import { BlockPlus } from './BlockPlus.js'
// 小板干涉判断等算法
/** 判断小板干涉情况 */
export function checkOverlapInBoard(pm: PlaceMaterial, pb: PlaceBoard,config:any) {
if (pb.blockList.length == 0)
return
const blocks = pb.blockList.slice(0).sort((a, b) => a.cutOrder - b.cutOrder)
let {
overlapGap= 0.05
} = config
// 初始化
for (const t of blocks) {
t.isOverlap = false
t.isOutBoard = false
t.pl_border = null // 开料后轮廓(待预铣,同刀辅助 ,不含刀半径) ,内缩一个干涉容差/2
t.pl_cutBorder = null // 走刀轮廓
t.pl_modelSpaces = null
t.pl_modelsOut = null
}
const r = pm.diameter / 2
// 干涉容差
let _overlapGap = overlapGap + 0.002
if (_overlapGap < 0)
_overlapGap = 0
const borderOff = pm.cutBorder - _overlapGap / 2 - 0.001
// console.log('大板的尺寸 -- borderOff', pm.cutBorder, overlapGap / 2 - 0.001)
const minx = borderOff
const maxX = pb.width - borderOff
const miny = borderOff
const maxY = pb.length - borderOff
// 余料异形大板 的轮廓
let boardBorders
if (pb.isAdnormal()) {
if (pb.polyline == null) {
pb.polyline = PolylineHelper.create(pb.points, true)
}
if (_overlapGap > 0) // 有干涉容差, 变大
{
boardBorders = pb.polyline.GetOffsetCurves(_overlapGap)[0]
}
else {
boardBorders = pb.polyline
}
}
for (let i = 0; i < blocks.length; i++) {
const block1 = blocks[i]
// 1.是否超出大板外
if (checkOutOfBoard(block1, boardBorders, minx, maxX, miny, maxY, _overlapGap)) {
console.log('超出大板外', block1.blockNo)
block1.isOverlap = true
}
// 2.检查板 铣刀路径,是否与前面板的铣刀路径 相干涉
for (let p = 0; p < i; p++) {
const block2 = blocks[p]
const isOverlap = isOverlap_block(block1, block2, r, _overlapGap)
if (isOverlap) {
console.log('铣刀路径问题 检查板 铣刀路径,与前面板的铣刀路径 相干涉', block1.blockNo, block1.blockId, block2.blockNo, block2.blockId)
block1.isOverlap = true
block2.isOverlap = true
}
}
}
// 初始化
for (const t of blocks) {
t.pl_border = null // 开料后轮廓(待预铣,同刀辅助 ,不含刀半径) ,内缩一个干涉容差/2
t.pl_cutBorder = null // 走刀轮廓
t.pl_modelSpaces = null // 内部轮廓
t.pl_modelsOut = null // 造型外扩 轮廓
}
}
/** 判断板是否超出大板外 p0:大板可用最低点,p1:大板可用最高点 */
function checkOutOfBoard(b: PlaceBlock, pl_board: Polyline, minx, maxx, miny, maxy, overlapGap): boolean {
// 判断小板的开料轮廓
const pl_block = getBorder(b, overlapGap)
// 异形的余料大板
if (pl_board) {
// 小板面积比大板大 , 干涉
if (pl_board.Area < pl_block.Area) {
console.log('小板面积比大板大 , 干涉')
return true
}
// 盒子 不干涉,小板在外面
if (pl_board.BoundingBox.intersectsBox(pl_block.BoundingBox) == false) {
console.log('盒子 不干涉,小板在外面')
return true
}
// 大盒子,没有包含 小盒子, 肯定在大板外 ,
if (pl_board.BoundingBox.containsBox(pl_block.BoundingBox) == false) {
console.log('大盒子,没有包含 小盒子, 肯定在大板外 ,')
return true
}
// 轮廓有干涉
if (pl_block.IntersectWith(pl_board, 0).length > 0) {
console.log('轮廓有干涉')
return true
}
// 大轮廓 包含 小轮廓
if (isTargetCurInOrOnSourceCur(pl_board, pl_block) == false) {
console.log('大轮廓 包含 小轮廓 ')
return true
}
return false
}
else // 矩形板
{
// console.log('打印所有的矩形板=>', b.blockNo, pl_block)
if (pl_block.BoundingBox.min.x < minx)
{
console.log('矩形板 小板最小X 小于限定的 minx ')
return true
}
if (pl_block.BoundingBox.min.y < miny)
{
console.log('矩形板 小板最小y 小于限定的 miny ', b.blockNo, pl_block.BoundingBox.min.y, miny)
return true
}
if (pl_block.BoundingBox.max.x > maxx)
{
console.log('矩形板 小板最大x 小于限定的 maxx ')
return true
}
if (pl_block.BoundingBox.max.y > maxy)
{
console.log('矩形板 小板最大y 大于限定的 maxy ')
return true
}
return false
}
}
/** 判断两板是否重叠 */
function isOverlap_block(block1: PlaceBlock, block2: PlaceBlock, r: number, overlapGap: number): boolean {
if (block1.boardId != block2.boardId)
{
console.log('板材都不一样',block1.boardId ,block2.boardId);
return false // 两板不在同一个大板上
}
// 判断 b1,b2 开料是否干涉, <包括 造型洞内>
if (isOverLap_border(block1, block2, r, overlapGap)) {
return true
}
// 判断b1造型走刀 干涉到 b2。
if (isOverlap_model(block1, block2, r, overlapGap)) {
return true
}
// 判断b2造型走刀 干涉到 b1。
if (isOverlap_model(block2, block1, r, overlapGap)) {
return true
}
return false
}
/** 开料轮廓是否干涉 */
function isOverLap_border(block1: PlaceBlock, block2: PlaceBlock, r: number, overlapGap: number): boolean {
let b1 = block1
let b2 = block2
let pl_b1 = getCutBorder(b1, r, overlapGap)
let pl_b2 = getCutBorder(b2, r, overlapGap)
// 盒子没有交集 ,不干涉
if (pl_b1.BoundingBox.intersectsBox(pl_b2.BoundingBox) == false)
return false
// 盒子有交集 ,有可能干涉
// 走刀轮廓有干涉点 肯定干涉了
if (pl_b1.IntersectWith(pl_b2, 0).length > 0) {
// console.log('走刀轮廓有干涉点 肯定干涉了')
return true
}
// 强制 b1 比 b2 大
if (pl_b1.Area < pl_b2.Area) {
[b1, b2] = [b2, b1];
[pl_b1, pl_b2] = [pl_b2, pl_b1]
}
// b1 ,包含 b2
if (isTargetCurInOrOnSourceCur(pl_b1, pl_b2)) {
// b1 的挖穿空间
const pls_inner1 = getModelSpaces(b1, r, overlapGap)
// b2 的开料轮廓
const pl_border2 = getBorder(b2, overlapGap)
for (const pl_1 of pls_inner1) {
// 挖穿造型 包含 b2 , 则不干涉
if (pl_1.Area > pl_border2.Area && isTargetCurInOrOnSourceCur(pl_1, pl_border2))
return false
}
return true
}
return false
}
/** 判断block1 的造型走刀 是否干涉到 block2 */
function isOverlap_model(block1: PlaceBlock, block2: PlaceBlock, r: number, overlapGap: number): boolean {
// 外扩造型线
const pls_1 = getModelsOuter(block1, r, overlapGap)
const pl_2 = getCutBorder(block2, r, overlapGap)
for (const pl of pls_1) {
if (pl.IntersectWith(pl_2, 0).length > 0)
return true
}
return false
}
/** 开料轮廓(待预铣,同刀辅助 ,不含刀半径) ,内缩一个干涉容差/2 */
function getBorder(b: PlaceBlock, overlapGap: number): Polyline {
let pl_border = b.pl_border
// if (pl_border == null) {
const borders = BlockPlus.getBorder_sameKnife(b)
let pl = BlockPlus.borderToPolyline(borders)
pl = pl.GetOffsetCurves(-overlapGap / 2)[0]
pl_border = PolylineHelper.moveTo(pl, b.placeX, b.placeY)
// b.pl_border = pl_border
// }
return pl_border
}
/** 走刀轮廓(待预铣,同刀辅助 , 刀半径) ,内缩一个干涉容差/2 */
function getCutBorder(b: PlaceBlock, r: number, overlapGap: number): Polyline {
let pl_cutBorder = b.pl_cutBorder
if (pl_cutBorder == null) {
const border = BlockPlus.getCutLines_sameKnife(b)
pl_cutBorder = BlockPlus.borderToPolyline(border)
pl_cutBorder = pl_cutBorder.GetOffsetCurves(-overlapGap / 2)[0]
pl_cutBorder = PolylineHelper.moveTo(pl_cutBorder, b.placeX, b.placeY)
b.pl_cutBorder = pl_cutBorder
}
return pl_cutBorder
}
/** 挖穿造型空间<扣除:刀直径 - 容差/2> */
function getModelSpaces(b: PlaceBlock, r: number, overlapGap: number): Polyline[] {
let pl_models = b.pl_modelSpaces
if (pl_models == null) {
let spaces = BlockPlus.getBorders_inner(b)
let rs = BlockPlus.getBorders_inner_r(b)
pl_models = []
if (spaces.length > 0 && spaces.length == rs.length) {
for (let i = 0; i < spaces.length; i++) {
let pl = BlockPlus.borderToPolyline(spaces[i])
// 内侧 刀直径 < 造型刀,开料刀 最大值 >
const off = Math.max(r, rs[i]) * 2 - overlapGap / 2
pl = pl.GetOffsetCurves(-off)[0]
if (pl == null)
continue
pl = PolylineHelper.moveTo(pl, b.placeX, b.placeY)
pl_models.push(pl)
}
}
b.pl_modelSpaces = pl_models
}
return pl_models
}
/** 外扩造型 多段线 */
function getModelsOuter(b: PlaceBlock, r: number, overlapGap: number): Polyline[] {
let pl_models = b.pl_modelsOut
if (pl_models == null) {
pl_models = []
const pls_org = BlockPlus.getOutModelBorders(b)
for (const pl of pls_org) {
const npl = PolylineHelper.moveTo(pl, b.placeX, b.placeY)
pl_models.push(npl)
}
b.pl_modelsOut = pl_models
}
return pl_models
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,473 @@
import type { Line2d } from '../../common/base/CAD.js'
import { PlaceStyle,PlaceBlock } from '../../confClass.js'
/** 有造型需要板外下刀, 特别计算要外偏的偏移值, 在排版的时候要算进开料尺寸 */
export class BlockSizePlus
{
/** 分析板外尺寸偏移:1 预洗, 同刀辅助开料,出板造型刀, 2V刀路 */
static analySizeOff(block: PlaceBlock, sysConfig: any)
{
// //1.预铣 预铣值先设置=1
// this.analyePreCut(block,sysConfig);
// //2.同刀辅助
// this.analyeSameKnifeToHelpCut(block, sysConfig);
// //3.造型刀超出板外
// this.analyeModelKnifeR(block);
// //3.二维刀路 板外下刀
// this.analye2VModels(block);
}
/** 分析1: 预铣尺寸扩展, 分析每条异形边预铣标识 */
static analyePreCut(block: PlaceBlock, sysConfig: any)
{
// 先默认 预洗值为1 ,真正使用时 * 真正的预洗值
// let bd = block.SaleBlockDetail;
// if (bd.preCutSizeOutOff) return;
// if(sysConfig.PreCutValue <= 0) //没有启动预铣
// {
// bd.preCutSizeOutOff = new SizeOutOff();
// return ;
// }
// let width = block.Width;
// let length = block.Length;
// let zp = 0;
// let yp = 0;
// let sp = 0;
// let xp = 0;
// if (block.IsUnRegular == false) //矩形板
// {
// if (block.BorderLeft > 0.001) zp = 1;
// if (block.BorderRight > 0.001) yp = 1;
// if (block.BorderUpper > 0.001) sp = 1;
// if (block.BorderUnder > 0.001) xp = 1;
// }
// else //异形板
// {
// //1 判断异形边 预洗
// for (let i = 0; i < bd.OrgPoints.length; i++)
// {
// let s = i - 1;
// let j = i + 1;
// if (s == -1) s = bd.OrgPoints.length - 1;
// if (j == bd.OrgPoints.length) j = 0;
// let p0 = bd.OrgPoints[s];
// let p1 = bd.OrgPoints[i];
// let p2 = bd.OrgPoints[j];
// p1.needPreCut = checkYX(p0,p1,p2);
// setFX(p1,p2);
// }
// //2 如果是同一水平线,或垂直线的, 有一条不能预铣,所有都不能预铣
// // //底
// // let pts = bd.OrgPoints.filter(t=>t['fx']==0 && equal(t.PointY,0));
// // let noPreCut = pts.some(t=>!t.needPreCut);
// // if(noPreCut) pts.forEach(t=>t.needPreCut = false);
// // xp = pts.some(t=>t.needPreCut) ? 1:0;
// // //右
// // pts = bd.OrgPoints.filter(t=>t['fx']==1 && equal(t.PointX,width));
// // noPreCut = pts.some(t=>!t.needPreCut);
// // if(noPreCut) pts.forEach(t=>t.needPreCut = false);
// // yp = pts.some(t=>t.needPreCut) ? 1:0;
// // //上
// // pts = bd.OrgPoints.filter(t=>t['fx']==2 && equal(t.PointY,length));
// // noPreCut = pts.some(t=>!t.needPreCut);
// // if(noPreCut) pts.forEach(t=>t.needPreCut = false);
// // sp = pts.some(t=>t.needPreCut) ? 1:0;
// // //左
// // pts = bd.OrgPoints.filter(t=>t['fx']==3 && equal(t.PointX,0));
// // noPreCut = pts.some(t=>!t.needPreCut);
// // if(noPreCut) pts.forEach(t=>t.needPreCut = false);
// // zp = pts.some(t=>t.needPreCut) ? 1:0;
// //3 .内部有缺角的边 ,不能预铣
// //底
// let pts = bd.OrgPoints.filter(t=>t['fx']==0 && equal(t.PointY,0));
// if(pts.length > 1) pts.forEach(t=>t.needPreCut = false); //底的边,有多条线段,表示内部有缺角,全部不能预铣
// xp = pts.some(t=>t.needPreCut) ? 1:0;
// //右
// pts = bd.OrgPoints.filter(t=>t['fx']==1 && equal(t.PointX,width));
// if(pts.length > 1) pts.forEach(t=>t.needPreCut = false);
// yp = pts.some(t=>t.needPreCut) ? 1:0;
// //上
// pts = bd.OrgPoints.filter(t=>t['fx']==2 && equal(t.PointY,length));
// if(pts.length > 1) pts.forEach(t=>t.needPreCut = false);
// sp = pts.some(t=>t.needPreCut) ? 1:0;
// //左
// pts = bd.OrgPoints.filter(t=>t['fx']==3 && equal(t.PointX,0));
// if(pts.length > 1) pts.forEach(t=>t.needPreCut = false);
// zp = pts.some(t=>t.needPreCut) ? 1:0;
// //如果斜边 会影响
// //3 再计算 扩展 有预铣边的斜线 起点或终点 在这边上的,这边就要扩展
// for(let i = 0 ; i < bd.OrgPoints.length ;i++)
// {
// let j = i + 1;
// if (j == bd.OrgPoints.length) j = 0;
// let p1 = bd.OrgPoints[i];
// let p2 = bd.OrgPoints[j];
// if(p1.needPreCut == false) continue;
// //判断 起点
// if(p1['fx']==4 && xp == 0 && equal(p1.PointY,0)) xp = 1
// if(p1['fx']==5 && yp == 0 && equal(p1.PointX,width)) yp = 1
// if(p1['fx']==6 && sp == 0 && equal(p1.PointY,length)) sp = 1
// if(p1['fx']==7 && zp == 0 && equal(p1.PointX,0)) zp = 1
// //判断终点
// if(p1['fx']==4 && yp == 0 && equal(p2.PointX,width)) yp = 1
// if(p1['fx']==5 && sp == 0 && equal(p2.PointY,length)) sp = 1
// if(p1['fx']==6 && zp == 0 && equal(p2.PointX,0)) zp = 1
// if(p1['fx']==7 && xp == 0 && equal(p2.PointY,0)) xp = 1
// }
// }
// let sizePlus = new SizeOutOff();
// sizePlus.left = zp;
// sizePlus.right = yp;
// sizePlus.bottom = xp;
// sizePlus.top = sp;
// sizePlus.width = zp + yp;
// sizePlus.length = sp + xp;
// bd.preCutSizeOutOff = sizePlus;
// function checkYX(p0,p1,p2) //判断p1是否需要预铣
// {
// if (p1.Curve != 0) return false; //本身是圆弧
// if (p1.SealSize < 0.001) return false;// 本身不封边
// if (p0.Curve != 0) return false; //前一段是圆弧
// if (p2.Curve != 0) return false;//后一段是圆弧
// //p1.p2 只要有一点在板内,就不行
// let isIn1 = (p1.PointX > 0.001 && p1.PointX < width - 0.001 && p1.PointY > 0.001 && p1.PointY < length - 0.001);
// if (isIn1) return false;
// let isIn2 = (p2.PointX > 0.001 && p2.PointX < width - 0.001 && p2.PointY > 0.001 && p2.PointY < length - 0.001);
// if (isIn2) return false;
// return true; //需要预洗
// }
// function setFX(p1,p2) //设置p1的方向
// {
// let fx = -1; //向右 0,向上 1向左 2向下 3 ,右上 4,左上5,左下6右下 7
// if(p2.PointX > p1.PointX && equal(p2.PointY,p1.PointY))
// {
// fx = 0;
// }
// else if(p2.PointX < p1.PointX && equal(p2.PointY,p1.PointY))
// {
// fx = 2;
// }
// else if(p2.PointY > p1.PointY && equal(p2.PointX,p1.PointX))
// {
// fx = 1;
// }
// else if(p2.PointY < p1.PointY && equal(p2.PointX,p1.PointX))
// {
// fx = 3;
// }
// else if(p2.PointX > p1.PointX && p2.PointY > p1.PointY)
// {
// fx = 4;
// }
// else if(p2.PointX < p1.PointX && p2.PointY > p1.PointY)
// {
// fx = 5;
// }
// else if(p2.PointX < p1.PointX && p2.PointY < p1.PointY)
// {
// fx = 6;
// }
// else if(p2.PointX > p1.PointX && p2.PointY < p1.PointY)
// {
// fx = 7;
// }
// p1['fx'] = fx;
// }
}
// 分析2 同刀辅助
static analyeSameKnifeToHelpCut(block: PlaceBlock, sysconfig: any)
{
// let bDetail = block.SaleBlockDetail;
// let isSameKnifeToCut = sysconfig.UseSameKnifeToHelpCut && sysconfig.UseSameKnifeToHelpCutGap > 0;
// //未启动同刀辅助, 或 该小板 不需要同刀辅助
// if (isSameKnifeToCut == false || bDetail.isNeedHelpCut == false)
// {
// bDetail.sameKnifeToHelpCutGap = 0;
// bDetail.sameKnfieHelpOutOff = new SizeOutOff();
// }
// else
// {
// let gap = sysconfig.UseSameKnifeToHelpCutGap;
// bDetail.sameKnifeToHelpCutGap = gap;
// bDetail.sameKnfieHelpOutOff = new SizeOutOff({ left: gap, right: gap, under: gap, upper: gap });
// }
}
// 分析3 造型刀 超出板外
static analyeModelKnifeR(block: PlaceBlock)
{
// let bDetail = block.SaleBlockDetail;
// if (bDetail.modelKnifeOutOff) return;
// let outValue = (block.PlaceMetrial.CutDia + block.PlaceMetrial.CutGap) /2;
// let minX = -outValue;
// let maxX = bDetail.CuttingWidth + outValue;
// let minY = -outValue;
// let maxY = bDetail.CuttingLength + outValue;
// //求 造型点 最偏 值
// for (let model of bDetail.Models)
// {
// if(model.isVKnifeModel) continue;
// if(model.IsDo==false) continue;
// if(model.Depth > block.Thickness - 0.001) continue;
// let r = model.KnifeRadius;
// for (let mp of model.PointList)
// {
// if (mp.PointX - r < minX) minX = mp.PointX - r;
// if (mp.PointX + r > maxX) maxX = mp.PointX + r;
// if (mp.PointY - r < minY) minY = mp.PointY - r;
// if (mp.PointY + r > maxY) maxY = mp.PointY + r;
// }
// }
// let off = {left:0,right :0,upper:0,under:0};
// /**暂时屏蔽 造型外扩 */
// // if (minX < - outValue) off.left = (-minX) - outValue;
// // if (maxX > bDetail.CuttingWidth + outValue) off.right = maxX - bDetail.CuttingWidth - outValue;
// // if (minY < - outValue) off.bottom = (-minY) - outValue;
// // if (maxY > bDetail.CuttingLength + outValue) off.top = maxY - bDetail.CuttingLength - outValue;
// bDetail.modelKnifeOutOff = new SizeOutOff(off);
}
// 分析4 板外下刀 2v刀路
static analye2VModels(block: PlaceBlock)
{
// let blockDetail = block.SaleBlockDetail;
// if (blockDetail.vKnifeModelSizeOutOff) return;//已存在, 不用重复分析
// let sizePlus = new SizeOutOff();
// let cutR = block.PlaceMetrial.CutDia /2;
// let minX = - cutR;
// let maxX = block.CuttingWidth + cutR ;
// let minY = - cutR;
// let maxY = block.CuttingLength + cutR;
// for(let model of blockDetail.Models)
// {
// if(!model.IsDo || model.isVKnifeModel ==false ) continue;
// if(!model.VLines) continue;
// if(model.VLines.length == 0) continue;
// for(let vm of model.VLines)
// {
// let knifeR = vm.knifeRadius;
// let points = vm.points.map((t) =>
// {
// return {
// PointX: t.x,
// PointY: t.y,
// Radius: t.r,
// Depth: vm.depth - t.z,
// Curve: t.bul,
// };
// });
// let isOut = vm.points.some(t=>t.x - knifeR < -cutR)
// || vm.points.some(t=>t.x + knifeR > block.CuttingWidth + cutR)
// || vm.points.some(t=>t.y - knifeR < -cutR)
// || vm.points.some(t=>t.y + knifeR > block.CuttingLength + cutR);
// if(isOut ) //超出板外
// {
// vm['isOut'] = true;
// for (let mp of points)
// {
// if (mp.PointX - knifeR < minX) minX = mp.PointX - knifeR;
// if (mp.PointX + knifeR > maxX) maxX = mp.PointX + knifeR;
// if (mp.PointY - knifeR < minY) minY = mp.PointY - knifeR;
// if (mp.PointY + knifeR > maxY) maxY = mp.PointY + knifeR;
// }
// }
// }
// }
// if (minX < -cutR) sizePlus.left = -minX;
// if (maxX > block.CuttingWidth + cutR) sizePlus.right = maxX - block.CuttingWidth;
// if (minY < -cutR) sizePlus.bottom = - minY;
// if (maxY > block.CuttingLength + cutR) sizePlus.top = maxY - block.CuttingLength;
// sizePlus.width = sizePlus.left + sizePlus.right;
// sizePlus.length = sizePlus.bottom + sizePlus.top;
// sizePlus.hasDone = true;
// //blockDetail.vKnifeModelSizeOutOff = sizePlus;
// blockDetail.vKnifeModelSizeOutOff = new SizeOutOff();
}
/** 获得板件偏移值 */
static getOffDis(block: PlaceBlock, placeStyle?: PlaceStyle): any
{
// console.log('获得板件偏移值')
if (placeStyle == null || placeStyle == undefined)
{
placeStyle = block.placeStyle
}
let expandSize :any = block.sizeExpand()
let posOff = { x: 0, y: 0, left: 0, right: 0, top: 0, bottom: 0 }
if(expandSize){
switch (placeStyle)
{
case PlaceStyle.FRONT: // 正面
posOff.x = expandSize.left
posOff.y = expandSize.bottom
posOff.left = expandSize.left
posOff.right = expandSize.right
posOff.bottom = expandSize.bottom
posOff.top = expandSize.top
break
case PlaceStyle.FRONT_TURN_RIGHT: // 正面右转
posOff.x = expandSize.bottom
posOff.y = expandSize.right
posOff.left = expandSize.bottom
posOff.right = expandSize.top
posOff.bottom = expandSize.right
posOff.top = expandSize.left
break
case PlaceStyle.FRONT_TURN_BACK: // 正面后转
posOff.x = expandSize.right
posOff.y = expandSize.top
posOff.left = expandSize.right
posOff.right = expandSize.left
posOff.bottom = expandSize.top
posOff.top = expandSize.bottom
break
case PlaceStyle.FRONT_TURN_LEFT: // 正面左转
posOff.x = expandSize.top
posOff.y = expandSize.left
posOff.left = expandSize.top
posOff.right = expandSize.bottom
posOff.bottom = expandSize.left
posOff.top = expandSize.right
break
case PlaceStyle.BACK: // 反面
posOff.x = expandSize.right
posOff.y = expandSize.bottom
posOff.left = expandSize.right
posOff.right = expandSize.left
posOff.bottom = expandSize.bottom
posOff.top = expandSize.top
break
case PlaceStyle.BACK_TURN_RIGHT: // 反面右转
posOff.x = expandSize.bottom
posOff.y = expandSize.left
posOff.left = expandSize.bottom
posOff.right = expandSize.top
posOff.bottom = expandSize.left
posOff.top = expandSize.right
break
case PlaceStyle.BACK_TURN_BACK: // 反面后转
posOff.x = expandSize.left
posOff.y = expandSize.top
posOff.left = expandSize.left
posOff.right = expandSize.right
posOff.bottom = expandSize.top
posOff.top = expandSize.bottom
break
case PlaceStyle.BACK_TURN_LEFT: // 反面左转
posOff.x = expandSize.top
posOff.y = expandSize.right
posOff.left = expandSize.bottom
posOff.right = expandSize.bottom
posOff.bottom = expandSize.right
posOff.top = expandSize.left
break
default:
break
}
}
return posOff
}
// 设置板件的位置
static resetNewPlace(block: PlaceBlock)
{
let posOff = this.getOffDis(block)
block.placeOffX = posOff.x
block.placeOffY = posOff.y
block.placeX = block.placeX + block.placeOffX
block.placeY = block.placeY + block.placeOffY
}
static checkPreBorder(block: PlaceBlock, line: Line2d): boolean // 判断 开料刀路中的一条 line 是否需要预洗
{
let x1 = line.StartPoint.m_X
let y1 = line.StartPoint.m_Y
let x2 = line.EndPoint.m_X
let y2 = line.EndPoint.m_Y
if (block.isUnRegular == false) // 矩形
{
if (this.eqaul(x1, 0, 0.01) && this.eqaul(x1, x2))
return block.sealLeft > 0
if (this.eqaul(x1, block.cutWidth, 0.01) && this.eqaul(x1, x2))
return block.sealRight > 0
if (this.eqaul(y1, 0, 0.01) && this.eqaul(y1, y2))
return block.sealBottom > 0
if (this.eqaul(y1, block.cutLength, 0.01) && this.eqaul(y1, y2))
return block.sealTop > 0
return false
}
else // 异形
{
// 找出原始轮廓中 对应的边 , 是否有 预洗信息
for (let i = 0; i < block.orgPoints.length; i++)
{
let j = i + 1
if (j == block.orgPoints.length)
j = 0
if (block.orgPoints[i].curve != 0)
continue
let dis = block.orgPoints[i].sealSize
let w1 = block.orgPoints[i].pointX - (block.blockDetail?.offsetX || 0)
let v1 = block.orgPoints[i].pointY - (block.blockDetail?.offsetY || 0)
let w2 = block.orgPoints[j].pointX - (block.blockDetail?.offsetX || 0)
let v2 = block.orgPoints[j].pointY - (block.blockDetail?.offsetY || 0)
let dis1 = Math.sqrt((x1 - w1) * (x1 - w1) + (y1 - v1) * (y1 - v1))
if (dis1 > dis * 2)
continue
let dis2 = Math.sqrt((x2 - w2) * (x2 - w2) + (y2 - v2) * (y2 - v2))
if (dis2 < dis * 2)
return block.orgPoints[i].isPreCutRequired
}
return false
}
}
static equal2Point(p1, p2, dis = 0.001)
{
let x1 = p1.m_X
let y1 = p1.m_Y
let x2 = p2.m_X
let y2 = p2.m_Y
let len1 = (x1 - x2) * (x1 - x2)
let len2 = (y1 - y2) * (y1 - y2)
let len = Math.sqrt(len2 + len1)
return len < dis
}
static eqaul(a, b, dis = 0.001)
{
return Math.abs(a - b) < 0.001
}
}

View File

@@ -0,0 +1,266 @@
import type { PolylineProps } from 'cadapi'
import { Circle, Polyline, Polyline2Points } from 'cadapi'
import { EndType, JoinType } from 'js-angusj-clipper/web'
import type { Box3, Vector3 } from 'three'
import { Vector2 } from 'three'
import { clipperCpp } from '../ClipperCpp'
import type { Point } from '../Vector2'
import { Path, PathScale } from '../core/Path'
import type { IOffset } from './Simplify2'
import { SimplifyDouglasPeucker } from './Simplify2'
/** 内外接多边形 */
export function Circle2Points(circle: Circle, knifRadius: number, splitSize = 10, outside = false): Point[] {
let radius = circle.Radius
const an = Math.PI * 2 / splitSize
if (outside)
radius = radius / Math.cos(an / 2) + knifRadius
else
radius -= knifRadius
const cenP = circle.Center
const pts: Vector3[] = []
for (let i = 0; i < splitSize; i++)
pts.push(polar(cenP.clone(), an * i, radius))
return pts as Point[]
}
export function Curve2Path(curve: Polyline, outside = false): Path {
if (curve.IsClockWise)
curve.Reverse()
const w = new CurveWrap(curve, 3, outside)
return new Path(outside ? w.GetOutsidePoints() : w.GetInsidePoints())
}
export class CurveWrap {
BoundingBox: Box3
Area: number
SimplyPolyline?: Polyline
SimplyOffset?: IOffset
Used = false
Holes: CurveWrap[] = []
Points: Point[] = []
constructor(public Curve: Polyline | Circle, public KnifRadius: number, public IsOutside: boolean) {
this.BoundingBox = Curve.BoundingBox
this.Area = 0
if (Curve instanceof Polyline) {
const pts = Polyline2Points(Curve, IsOutside, 0)[1]
/**
* 精简算法SimplifyDouglasPeucker 会导致轮廓变大,
* 修改成直接取点 陈雄 QQ聊天记录 23.9.18
* 23.10.9 by lrx
*/
const [spts, offset] = SimplifyDouglasPeucker(pts, KnifRadius ** 2 + KnifRadius)
if (spts.length !== pts.length && spts.length > 2) {
this.SimplyOffset = offset
this.SimplyPolyline = Path2Polyline(spts)
this.Curve = this.SimplyPolyline// 保险起见,也更新它
this.Area = this.SimplyPolyline.Area
}
else// 此处更新多段线
{ this.Curve = Path2Polyline(pts) }
this.Points = spts
// 以下修改后的
// this.Curve = Path2Polyline(pts);
// this.Points = pts;
}
if (this.Area === undefined)
this.Area = this.Curve.Area
}
ContainsCurve(curve: CurveWrap): boolean {
if (this.SimplyPolyline)
return this.SimplyPolyline.PtInCurve(curve.Curve.StartPoint)
return this.Curve.PtInCurve(curve.Curve.StartPoint)
}
GetOutsidePoints(): Point[] {
if (this.Curve instanceof Circle) {
const pts = Circle2Points(this.Curve, this.KnifRadius, 10, true)
return pts
}
else {
const pl = this.SimplyPolyline || this.Curve
let offset = this.KnifRadius
if (this.SimplyOffset)
offset += this.SimplyOffset.positiveOffset
if (offset > 0) {
let pts = pl.GetStretchPoints() as Point[]
if (clipperCpp.lib && typeof clipperCpp.lib.offsetToPaths === 'function') {
const result = clipperCpp.lib.offsetToPaths({
delta: offset * 1e4,
offsetInputs: [{ data: PathScale(pts, 1e4), joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
});
pts = result && result[0] ? result[0] : [];
}
try {
PathScale(pts, 1e-4)
}
catch {
console.log('err')
}
return pts
}
else { return this.Points }
}
}
GetInsidePoints(): Point[] {
return this.GetInsidePoints2(this.KnifRadius)
}
GetInsidePoints2(d: number): Point[] {
if (this.Curve instanceof Circle) {
const pts = Circle2Points(this.Curve, d, 10, false)
return pts
}
else {
const pl = this.SimplyPolyline || this.Curve
let offset = -d
if (this.SimplyOffset)
offset += this.SimplyOffset.negativeOffset
if (offset < -0.01) {
const pls = pl.GetOffsetCurves(offset)
if (pls.length)
{
return pls[0].GetStretchPoints()
}else{
return []
}
}
else { return this.Points }
}
}
/** 引入Polyline 已经含刀半径, 获得精简后的点阵 */
GetOrgPoints(outside = true): Point[] {
if (this.Curve instanceof Circle) {
const pts = Circle2Points(this.Curve, 0, 10, outside)
return pts
}
else {
const pl = this.SimplyPolyline || this.Curve
let offset = 0
if (this.SimplyOffset)
offset += this.SimplyOffset.positiveOffset
if (offset > 0) {
let pts = pl.GetStretchPoints() as Point[]
if (clipperCpp.lib && typeof clipperCpp.lib.offsetToPaths === 'function') {
const result = clipperCpp.lib.offsetToPaths({
delta: offset * 1e4,
offsetInputs: [{ data: PathScale(pts, 1e4), joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
});
pts = result && result[0] ? result[0] : [];
}
try {
PathScale(pts, 1e-4)
}
catch {
console.log('err')
}
return pts
}
else {
return this.Points
}
}
}
}
/** 多段线 转点整 已弃用整合到CAD api 23.11.2 */
// export function Polylin2Points(pl: Polyline, outside: boolean, knifRadius: number): [Polyline, Point[]]
// {
// let pts: Point[] = [];
// if (!outside) knifRadius = -knifRadius;
// if (pl.IsClockWise) pl.Reverse();
// for (let i = 0; i < pl.EndParam; i++)
// {
// pts.push(pl.GetPointAtParam(i));
// let bul = pl.GetBulgeAt(i);
// if (bul !== 0)
// {
// let arc = pl.GetCurveAtIndex(i) as Arc;
// let allAngle = arc.AllAngle;
// let arcLength = arc.Length;
// // let splitCount = Math.round(allAngle / 0.4);
// // if (arcLength < 300)
// // splitCount = 2;
// // else
// // splitCount = Math.max(arcLength / 200, splitCount,2);
// let minCount = Math.floor(allAngle * 4 / Math.PI);
// let splitCount = Math.round(allAngle / 0.4);
// if (arcLength < 300)
// splitCount = Math.max(2, minCount);
// else
// splitCount = Math.max(Math.floor(arcLength / 200), splitCount,2, minCount);
// let radius = arc.Radius;
// if (outside === bul > 0)
// radius = radius / Math.cos(allAngle / (splitCount * 2));
// let cp = arc.Center;
// for (let j = outside ? 0.5 : 0; j < splitCount; j++)
// {
// let a = arc.GetAngleAtParam(j * (1 / splitCount));
// let p = polar(cp.clone(), a, radius);
// pts.push(p);
// }
// }
// }
// if (knifRadius !== 0)
// {
// pts = clipperCpp.lib.offsetToPaths({
// delta: knifRadius * 1e4,
// offsetInputs: [{ data: PathScale(pts, 1e4), joinType: JoinType.Miter, endType: EndType.ClosedPolygon }]
// })[0];
// PathScale(pts, 1e-4);
// }
// return [pl, pts];
// }
export function Path2Polyline(path: Point[]): Polyline {
const pl = new Polyline()
pl.LineData = path.map((p) => {
return { pt: new Vector2(p.x, p.y), bul: 0 }
})
pl.CloseMark = true
return pl
}
export function Points2Polyline(pts: any[]): Polyline {
const lined: PolylineProps[] = []
const count = pts.length
for (let i = 0; i < count; i++) {
const p0 = pts[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: p0.bul })
}
const pls = new Polyline(lined)
pls.CloseMark = true
return pls
}
export function polar<T extends Vector2 | Vector3>(v: T, an: number, dis: number): T {
v.x += Math.cos(an) * dis
v.y += Math.sin(an) * dis
return v
}

View File

@@ -0,0 +1,321 @@
import { BlockSizePlus } from './BlockSizePlus.js'
import { BlockHelper } from './BlockHelper.js'
import { BlockModel,PlaceBlock,PlaceBoard,PlaceMaterial } from '../../confClass.js'
/**
* 大板长边的两侧多少范围内 避免造型 ,如果出现,则那片小板掉头放置
* @param pm
* @param 大板边缘范围
* @face 大板面 反面 1 正面 0 ;双面 2
* @forCNC 针对的是cnc数据
* @isReverse 反转: 大板边缘尽量 安排造型
*/
export function DisPoseModelInBoardBorder(pm: PlaceMaterial, width: number, face: number, forCNC: number, isReverse: boolean)
{
if (width == 0)
return
for (const pb of pm.boardList)
{
if (pb.isAdnormal())
continue
if (pb.isLocked || pb.cutedType != 0)
continue
if (isReverse == false)
{
// 避免大板边出现 造型
DisPoseModelInBoardBorder_pb(pb, width, face, forCNC)
}
else
{
// 尽量 安排 造型在大板边
needModelInBoardBorder_pb(pb, width, face, forCNC)
}
}
}
/**
* 大板边缘 避免造型 如果出现,则那片小板掉头放置
*
* @param pb
* @param width
* @param face
* @param forCNC 针对的是cnc的加工
*/
function DisPoseModelInBoardBorder_pb(pb: PlaceBoard, width: number, face: number, forCNC: number)
{
const bw = pb.width
for (const block of pb.blockList)
{
if (block.isUnRegular)
continue
if (block.placeX + block.placeWidth < width)
continue // 小板整片板都在边缘, 转不转没意义
if (block.placeX > bw - width)
continue
// 如果内部有小板, 也不能掉头
if (hasChildBlock(block, pb.blockList))
continue
let isInBorder = false
const models = getModels(block, face, forCNC)
for (const model of models)
{
isInBorder = isInBoardBorder(block, model, bw, width)
if (isInBorder)
break
}
if (isInBorder == false)
continue
turnBlock(block)
}
}
/**
* 大板边缘 尽量安排造型
*
* @param pb
* @param width
* @param face
* @param forCNC
*/
function needModelInBoardBorder_pb(pb: PlaceBoard, width: number, face: number, forCNC: number)
{
const blocks_turn: any = []
const bw = pb.width
// 大板左边
const blocks_z = pb.blockList.filter(t => t.isUnRegular == false && t.placeX < width)
for (const block of blocks_z)
{
// 如果内部有小板, 也不能掉头
if (hasChildBlock(block, pb.blockList))
continue
// 造型
const models = getModels(block, face, forCNC)
let hasModelOnLeft = false // 左边有造型
let hasModelOnRight = false // 右边有造型
for (const model of models)
{
if (hasModelOnLeft == false && isInBoardLeft(block, model, width, false))
hasModelOnLeft = true
if (hasModelOnRight == false && isInBoardLeft(block, model, width, true))
hasModelOnRight = true
}
// 左边没有造型, 右边有造型,则掉头
if (hasModelOnLeft == false && hasModelOnRight)
{
blocks_turn.push(block)
}
}
// 大板右边
const blocks_y = pb.blockList.filter(t => t.isUnRegular == false && t.placeX + t.placeWidth > bw - width)
for (const block of blocks_y)
{
// 如果内部有小板, 也不能掉头
if (hasChildBlock(block, pb.blockList))
continue
// 造型
const models = getModels(block, face, forCNC)
let hasModelOnLeft = false // 小板左边有造型
let hasModelOnRight = false // 小板右边有造型
for (const model of models)
{
if (hasModelOnLeft == false && isInBoardRight(block, model, bw, width, true))
hasModelOnLeft = true
if (hasModelOnRight == false && isInBoardRight(block, model, bw, width, false))
hasModelOnRight = true
}
// 右边没有造型, 左边有造型,则掉头
if (hasModelOnLeft && hasModelOnRight == false)
{
blocks_turn.push(block)
}
}
// 翻转小板
for (const block of blocks_turn)
{
turnBlock(block)
}
}
/** 获取小板指定面造型 */
function getModels(block: PlaceBlock, face: number, forCNC: number): BlockModel[]
{
let models: any = []
if (face == 0) // 正面
{
models = block.modelListFaceA
if (forCNC == 1)
{
const ms_A = block.isTurnOver() == false ? block.modelListOrgFaceA() : block.modelListOrgFaceB()
models = ms_A.filter(t => t.isCutting == false).concat(block.modelListOrgTrough().filter(t => t.isCutting == false))
}
}
else if (face == 1) // 反面
{
models = block.modelListFaceB
if (forCNC == 1)
{
const ms_B = block.isTurnOver() == false ? block.modelListOrgFaceB() : block.modelListOrgFaceA()
models = ms_B.filter(t => t.isCutting == false)
}
}
else // 随意面
{
models = block.models().filter(t => t.isCutting != (forCNC == 1))
}
return models
}
/** 判断是否有造型出现在无法加工的区域 */
export function hasModelInBoardBorder(pb: PlaceBoard, width: number, face: number, forCNC: number)
{
pb.hasModelOnLeft = false
pb.hasModelOnRight = false
const bw = pb.width
for (const block of pb.blockList)
{
if (block.isUnRegular)
continue
if (block.placeX + block.placeWidth < width)
continue // 小板整片板都在边缘, 转不转没意义
if (block.placeX > bw - width)
continue
const models = getModels(block, face, forCNC)
if (pb.hasModelOnLeft == false)
{
for (const model of models)
{
let isLeft = false
for (const p of model.pointList)
{
const rp = BlockHelper.getPlaceXYInBoard(block, p.pointX, p.pointY)
if (rp.x < width)
{
isLeft = true
break
}
}
if (isLeft)
{
pb.hasModelOnLeft = true
break
}
}
}
if (pb.hasModelOnRight == false)
{
for (const model of models)
{
let isRight = false
for (const p of model.pointList)
{
const rp = BlockHelper.getPlaceXYInBoard(block, p.pointX, p.pointY)
if (rp.x > bw - width)
{
isRight = true
break
}
}
if (isRight)
{
pb.hasModelOnRight = true
break
}
}
}
if (pb.hasModelOnLeft && pb.hasModelOnRight)
return // 已经有结果了
}
}
/** 判断造型是否在大板的边缘 */
function isInBoardBorder(block: PlaceBlock, model: BlockModel, bw: number, width: number): boolean
{
for (const p of model.pointList)
{
const rp = BlockHelper.getPlaceXYInBoard(block, p.pointX, p.pointY)
if (rp.x < width || rp.x > bw - width)
return true
}
return false
}
/** 造型是否在大板左边 toTurn 翻转后 */
function isInBoardLeft(block: PlaceBlock, model: BlockModel, width: number, toTurn: boolean)
{
for (const p of model.pointList)
{
const rp = BlockHelper.getPlaceXYInBlock(block, p.pointX, p.pointY, false, false)
let rx = toTurn ? block.placeWidth - rp.x : rp.x
rx = rx + block.placeX
if (rx < width)
return true
}
return false
}
/** 造型 是否在大板 右边 toTurn 翻转后 */
function isInBoardRight(block: PlaceBlock, model: BlockModel, boardWidth: number, width: number, toTurn: boolean)
{
for (const p of model.pointList)
{
const rp = BlockHelper.getPlaceXYInBlock(block, p.pointX, p.pointY, false, false)
let rx = toTurn ? block.placeWidth - rp.x : rp.x
rx = rx + block.placeX
if (rx > boardWidth - width)
return true
}
return false
}
/** 判断block里有没有小板 */
function hasChildBlock(block: PlaceBlock, blocks: PlaceBlock[]): boolean
{
for (const b of blocks)
{
if (b.blockNo == block.blockNo)
continue
if (b.placeX > block.placeX && b.placeX + b.placeWidth < block.placeX + block.placeWidth && b.placeY > block.placeY && b.placeY + b.placeLength < block.placeY + block.placeLength)
return true
}
return false
}
function turnBlock(block: PlaceBlock)
{
// 有造型在大板边
const orgStyle = block.placeStyle
const newStyle = BlockHelper.getTurnedPlaceStyle(orgStyle, 2)
const orgPlaceX = block.placeX - block.placeOffX
const orgPlaceY = block.placeY - block.placeOffY
const offset = BlockSizePlus.getOffDis(block, newStyle)
// 左右上下,外扩尺寸不一样, 翻转有可能会导致 小板与其他小板 干涉。
if (offset.left != offset.right || offset.top != offset.bottom)
return
const newPlaceX = orgPlaceX + offset.x
const newPlaceY = orgPlaceY + offset.y
block.placeX = newPlaceX
block.placeY = newPlaceY
block.placeOffX = offset.x
block.placeOffY = offset.y
// 修改小板的放置方式
BlockHelper.resetPlaceStyle(block, newStyle)
}

View File

@@ -0,0 +1,94 @@
import { ArrayExt } from '../../common/base/ArrayExt.js';
import { PlaceBoard,PlaceMaterial } from '../../confClass.js';
import { hasModelInBoardBorder } from './DisposeModelInBoardBorder.js';
import { CutOrder } from '../cutorder/CutOrder.js';
// import { PlaceStore } from './PlaceStore.js';
/**
* 返回大板信息
* @param pm
* @param bid
*/
export function getPlaceBoard(pm: PlaceMaterial, bid: number): PlaceBoard
{
if (bid < pm.minBoardId) return null;
if (bid > pm.maxBoardId) return null;
let pb = pm.boardList.find(t => t.boardId == bid);
if (pb == null)
{
pb = new PlaceBoard(bid, pm.width, pm.length);
pb.blockList = pm.blockList.filter(t => t.boardId == bid);
resetPlaceBoard(pm, pb);
pm.boardList.push(pb);
}
return pb;
}
/**重设大板汇总 */
export function resetPlaceBoard(pm: PlaceMaterial, pb: PlaceBoard, blocks = null)
{
if (pm == null) return;
if (pb == null) return;
if (blocks != null)
{
pb.blockList = blocks;
}
pb.blockCount = pb.blockList.length;
pb.blockArea = ArrayExt.sum(pb.blockList, t => t.area);
pb.usageRate = 100 * pb.blockArea / pb.area;
//判断 有造型出现在无法加工的区域
// const sys = PlaceStore.sysConfig;
// if(sys && sys.boardBorderModelRange > 1 && sys.modelNearBoardBorder == false)
// {
// hasModelInBoardBorder(pb, sys.boardBorderModelRange, sys.boardBorderModelModeToFace, sys.boardBorderModelByMachine);
// }
// CutOrder.autoCalcCutOrder(pm, pb);
}
/**
* 重设板材 优化率等, 是否排序大板列表
* @param pm
* @param sortBoard
*/
export function resetPlaceMaterial(pm: PlaceMaterial, sortBoard = false)
{
if (pm.boardCount == 0)
{
pm.blockCount = 0;
pm.blockArea = 0;
pm.avgUsageRateAll = 0;
pm.avgUsageRateExcludeLastBoard = 0;
pm.usageRateLastBoard = 0;
pm.boardCountFlipFace = 0;
return;
}
pm.blockCount = pm.blockList.length;
pm.blockArea = ArrayExt.sum(pm.blockList, t => t.area);
if (pm.boardCount == 1)
{
pm.avgUsageRateAll = pm.blockArea;
pm.avgUsageRateExcludeLastBoard = pm.blockArea;
pm.usageRateLastBoard = pm.blockArea;
}
else
{
const size_last = getPlaceBoard(pm, pm.maxBoardId).blockArea;
pm.avgUsageRateAll = pm.blockArea / pm.boardCount;
pm.avgUsageRateExcludeLastBoard = (pm.blockArea - size_last) / (pm.boardCount - 1);
pm.usageRateLastBoard = size_last;
if (sortBoard) sortBoardPlace(pm);
}
pm.boardCountFlipFace = ArrayExt.count(pm.boardList, t => t.isTwoFaceProcessing);
}
/**
* 排序大板信息
* @param pm
*/
export function sortBoardPlace(pm: PlaceMaterial)
{
pm.boardList = ArrayExt.sortBy(pm.boardList, t => t.boardId);
}

View File

@@ -0,0 +1,359 @@
import type { PolylineProps } from 'cadapi'
import { CADFiler, Circle, Polyline, Status, VKnifToolPath, isTargetCurInOrOnSourceCur } from 'cadapi'
import type { Box3 } from 'three'
import { Vector2, Vector3 } from 'three'
import { arrayRemoveDuplicateBySort } from '../ArrayExt'
import type { Curve2d } from '../../common/base/CAD'
import { Arc2d, Point2d, copyTextToClipboard } from '../../common/base/CAD'
import { CurveWrap } from './Curves2Parts'
// import type { Curve2d } from '../../common/base/CAD'
export class PolylineHelper {
/** 创建闭合多段线 */
static create(pts: any[], closeMark = false): Polyline {
let lined: PolylineProps[] = []
let count = pts.length
for (let i = 0; i < count; i++) {
let p0 = pts[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: p0.bul || 0 })
}
let pls = new Polyline(lined)
pls.CloseMark = closeMark
return pls
}
static createByCurve2d(curs: Curve2d[]): Polyline {
let lined: PolylineProps[] = []
for (let cur of curs) {
let x = cur.StartPoint.m_X
let y = cur.StartPoint.m_Y
let bul = 0
if (cur instanceof Arc2d)
bul = cur.Bul || 0
lined.push({ pt: new Vector2(x, y), bul })
}
let pls = new Polyline(lined)
pls.CloseMark = true
return pls
}
static createByPts(pts: any[], buls: number[], closeMark = false): Polyline {
let plps: PolylineProps[] = []
let count = pts.length
for (let i = 0; i < count; i++) {
let p0 = pts[i]
plps.push({ pt: new Vector2(p0.x, p0.y), bul: buls[i] })
}
let pls = new Polyline(plps)
pls.CloseMark = closeMark
return pls
}
static getSimplePoints(pts: any[], offset: number): any[] {
let pl = PolylineHelper.create(pts)
pl.CloseMark = true
let cureW = new CurveWrap(pl, offset, true)
let pts2 = cureW.GetOutsidePoints()
arrayRemoveDuplicateBySort(pts2, (p1, p2) => (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) < 1e-2)
return pts2
}
static createByWidthLength(w: number, l: number): Polyline {
let plps: PolylineProps[] = []
plps.push({ pt: new Vector2(0, 0), bul: 0 })
plps.push({ pt: new Vector2(w, 0), bul: 0 })
plps.push({ pt: new Vector2(w, l), bul: 0 })
plps.push({ pt: new Vector2(0, l), bul: 0 })
let pls = new Polyline(plps)
pls.CloseMark = true
return pls
}
/** 多段线,添加位置移动 返回新多段线 */
static moveTo(pl: Polyline, x: number, y: number): Polyline {
let lindData = pl.LineData
let pos = pl.Position
let newPts: PolylineProps[] = []
for (let p of lindData) {
let nx = p.pt.x + pos.x + x
let ny = p.pt.y + pos.y + y
if (ny < 7.9) {
// console.log('修边小于 7.9????', ny)
}
let bul = p.bul
newPts.push({ pt: new Vector2(nx, ny), bul })
}
let npl = new Polyline(newPts)
npl.CloseMark = pl.CloseMark
return npl
}
/** 重设 多段线的几点 */
static resetPosition(pl: Polyline): Polyline {
let lindData = pl.LineData
let pos = pl.Position
let newPts: PolylineProps[] = []
for (let p of lindData) {
let nx = p.pt.x + pos.x
let ny = p.pt.y + pos.y
let bul = p.bul
newPts.push({ pt: new Vector2(nx, ny), bul })
}
let npl = new Polyline(newPts)
npl.CloseMark = pl.CloseMark
return npl
}
/** 获得v型刀走刀路径 */o
static getVModelPoints(pl: Polyline, depth: number, ang: number): any[] {
// let ang = Math.PI * (0.5 * angle) / 180 ;
let ps:any[] = []
let bx = pl.Position.x
let by = pl.Position.y
if (ang > 0.01) {
let rt = VKnifToolPath(pl, depth, ang / 2)
ps = rt.map((t) => { return { x: t.pt.x + bx, y: t.pt.y + by, z: t.pt.z, bul: t.bul, r: 0 } })
}
else {
ps = pl.LineData.map((t) => { return { x: t.pt.x + bx, y: t.pt.y + by, z: 0, bul: t.bul, r: 0 } })
}
for (let i = 0; i < ps.length; i++) {
let p = ps[i]
if (p.bul == 0)
continue
let p2 = (i == ps.length - 1 ? ps[0] : ps[i + 1])
let r = this.getArcRadius(p.x, p.y, p2.x, p2.y, p.bul)
p.r = r
}
return ps
}
static ConverPolyLin2Circle(polyline: Polyline, fuzz = 0.1): Circle | undefined {
let box = polyline.BoundingBox
let size = box.getSize(new Vector3())
if (!this.equaln(size.x, size.y, fuzz))// 盒子四方
return undefined
let circleLength = 2 * Math.PI * size.x
if (!this.equaln(circleLength, polyline.Length, fuzz * 2))
return undefined
let circleArea = Math.PI * size.x * size.x
if (!this.equaln(circleArea, polyline.Area, fuzz * 2))
return undefined
let r = size.x// 必须备份(因为我们要复用这个vector变量)
return new Circle(box.getCenter(size), r)
}
// 有问题
static getVModelPoints_offset(pl: Polyline, offset: number, depth: number, angle: number) {
let npl = offset == 0 ? pl : pl.GetOffsetCurves(offset)[0]
return PolylineHelper.getVModelPoints(npl, depth, angle)
}
static getArcRadius(x1: number, y1: number, x2: number, y2: number, bul: number): number {
let bul2 = Math.abs(bul)
let d = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) / 2
return 0.5 * d * (1 + bul2 ** 2) / bul2
}
// 圆 转 多段线
static cicleToPolyline(c: Circle): Polyline | undefined {
let arcs = c.GetSplitCurves([0, 0.5])
let pl = Polyline.FastCombine(arcs)
return pl
}
/** 判断多段线是否 重叠 */
static isIntersect(pl1: Polyline, pl2: Polyline): boolean {
let box1 = this.getBox(pl1)
let box2 = this.getBox(pl2)
if (!box1.intersectsBox(box2))
return false // 肯定不相交
let ipts = pl1.IntersectWith(pl2, 0)
if (ipts.length === 0) {
if (pl1.Area > pl2.Area)// 缓存面积
{
if (isTargetCurInOrOnSourceCur(pl1, pl2))
return true // 包含
}
else {
if (isTargetCurInOrOnSourceCur(pl2, pl1))
return true // 包含
}
return false
}
else {
return true // 有交点 一定有交集
}
}
// 多段线 圆弧合并
static mergeCurve(pl2: Polyline): Polyline|undefined {
const curves = pl2.Explode()
arrayRemoveDuplicateBySort(curves, (c1, c2) => {
return c1.Join(c2) === Status.True
})
return Polyline.FastCombine(curves)
}
/**
* 两片板的干涉检查
*
* @param pl1
* @param pls1_inner
* @param pls1_model
* @param pl2
* @param pls2_inner
* @param pls2_model
* @returns
*/
static isOverLap(pl1: Polyline, pls1_inner: Polyline[], pls1_model: Polyline[], pl2: Polyline, pls2_inner: Polyline[], pls2_model: Polyline[]) {
// 是否干涉, 被包含在造型洞,不算干涉
let isOverlap = this.boxIsOverlap(pl1, pls1_inner, pl2, pls2_inner)
if (isOverlap)
return true
// 造型 2v 刀路 是否干涉
for (let pl1_model of pls1_model) {
if (pl1_model.IntersectWith(pl2, 0).length > 0)
return true
for (let pl2_inner of pls2_inner) {
if (pl1_model.IntersectWith(pl2_inner, 0).length > 0)
return true
}
}
for (let pl2_model of pls2_model) {
if (pl2_model.IntersectWith(pl1, 0).length > 0)
return true
for (let pl1_inner of pls1_inner) {
if (pl2_model.IntersectWith(pl1_inner, 0).length > 0)
return true
}
}
return false
}
private static boxIsOverlap(pl1: Polyline, pls1_inner: Polyline[], pl2: Polyline, pls2_inner: Polyline[]) {
let box1 = this.getBox(pl1)
let box2 = this.getBox(pl2)
if (!box1.intersectsBox(box2))
return false // 肯定不相交
let ipts = pl1.IntersectWith(pl2, 0)
if (ipts.length > 0)
return true // 有交点 一定有交集
if (pl1.Area > pl2.Area)// 缓存面积
{
if (isTargetCurInOrOnSourceCur(pl1, pl2)) // pl1包含 pl2
{
for (let mpl of pls1_inner) // 如果pl1有造型洞包含pl2 则表示不干涉返回false
{
if (isTargetCurInOrOnSourceCur(mpl, pl2) == true)
return false
}
return true
}
}
else {
if (isTargetCurInOrOnSourceCur(pl2, pl1)) // pl2包含 pl1
{
for (let mpl of pls2_inner) // 如果pl2有造型洞包含pl1 则表示不干涉返回false
{
if (isTargetCurInOrOnSourceCur(mpl, pl1) == true)
return false
}
return true
}
}
return false
}
/** 判断 点是否在多段线内 */
static isPointInPolyline(pl1: Polyline, x: number, y: number): boolean {
return pl1.PtInCurve(new Vector3(x, y, 0))
}
static getBox(pl1: Polyline): Box3 {
if (!Reflect.has(pl1,'box_tp'))
{
Reflect.set(pl1,'box_tp',pl1.BoundingBox)
}
return pl1['box_tp'] as Box3
}
static getArea(pl1: Polyline): number {
if (!Reflect.has(pl1,'area_tp'))
{
Reflect.set(pl1,'area_tp',pl1.Area)
}
return pl1['area_tp'] as number
}
static getPath(pl: Polyline): Path2D {
let path = new Path2D()
let p0 = pl.LineData[0].pt
path.moveTo(p0.x, p0.y)
for (let i = 0; i < pl.LineData.length; i++) {
let p0 = pl.LineData[i].pt
let p1 = (i == pl.LineData.length - 1) ? pl.LineData[0].pt : pl.LineData[i + 1].pt
let bul = pl.LineData[i].bul
if (bul == 0) {
path.lineTo(p1.x, p1.y)
}
else {
let arc = new Arc2d(new Point2d(p0.x, p0.y), new Point2d(p1.x, p1.y), bul)
path.arc(arc.m_Center.m_X, arc.m_Center.m_Y, arc.m_Radius, arc.m_StartAngle, arc.m_EndAngle, bul < 0)
}
}
path.closePath()
return path
}
static equaln(v1: number, v2: number, fuzz = 1e-5) {
return Math.abs(v1 - v2) <= fuzz
}
static toClipboard(en: Polyline | any) {
let f = new CADFiler()
f.Write(1)// 实体个数
f.WriteObject(en)
copyTextToClipboard(f.ToString())
}
static getStrPLs(ens: any[]) {
if (ens.length == 0)
return ''
let f = new CADFiler()
f.Write(ens.length)// 实体个数
for (let en of ens)
f.WriteObject(en)
return f.ToString()
}
}

View File

@@ -0,0 +1,404 @@
import type { Polyline } from 'cadapi'
import { Polyline2Points } from 'cadapi'
import { Vector3 } from 'three'
import { ArrayExt } from '../base/ArrayExt.js'
import { Arc2d } from '../base/CAD.js'
import { arrayRemoveDuplicateBySort } from '../ArrayExt.js'
import { InitClipperCpp } from '../ClipperCpp.js'
import type { Vector2 } from '../Vector2.js'
import { Container } from '../core/Container.js'
import { NestCache } from '../core/NestCache.js'
import { ParseOddments } from '../core/ParseOddments.js'
import { Part } from '../core/Part.js'
import { Path } from '../core/Path.js'
import { PathGeneratorSingle } from '../core/PathGenerator.js'
import type { PlaceBlock,PlaceBoard,PlaceMaterial,BlockModel,RemainBlock} from '../../confClass.js'
import { BlockPlus } from './BlockPlus.js'
import { BlockSizePlus } from './BlockSizePlus.js'
import { BlockHelper } from './BlockHelper.js'
// import { PlaceStore } from '../../vo/order/PlaceStore.js'
import { PolylineHelper } from './PolylineHelper.js'
import { CurveWrap, Points2Polyline } from './Curves2Parts.js'
/** 自动生成余料板空间 */
export class RemainHelper {
squarePath: Path
canPutPaths: Path[]
offset = 3.5
initSquarePath() {
// let squareWidth = PlaceStore.sysConfig.scrapBlockSquare || 200;
// let widthMin = PlaceStore.sysConfig.srcapBlockWidthMin || 100;
// let widthMax = PlaceStore.sysConfig.scrapBlockWidthMax || 600;
let squareWidth = 200
let widthMin = 100
let widthMax = 600
this.squarePath = NestCache.CreatePath(60, 60, 0)
this.canPutPaths = [
NestCache.CreatePath(squareWidth, squareWidth, 0),
NestCache.CreatePath(widthMin, widthMax, 0),
NestCache.CreatePath(widthMax, widthMin, 0),
]
}
/** 分析所有余料空间 */
async analyzeAllRemainSpace(pm1: PlaceMaterial, pb1: PlaceBoard, preMillingSize: number) {
this.initSquarePath()
console.log('分析所有余料空间');
if (pm1 && pb1) // 当前板 先做
{
this.offset = (pm1.diameter + pm1.cutKnifeGap) / 2 + preMillingSize
pb1.isCreateRemainSpace = true
await this.analyzeScrapSpace(pm1, pb1)
}
let order = PlaceStore.order
for (let pm of order.materialList) {
this.offset = (pm.diameter + pm.cutKnifeGap ) / 2 + preMillingSize
for (let pb of pm.boardList) {
await this.analyzeScrapSpace(pm, pb)
}
}
}
/** 分析所有余料空间 */
async analyzeScrapSpace(pm: PlaceMaterial, board: PlaceBoard) {
if (board.isCreateRemainSpace == false)
return
await InitClipperCpp()
let cutBoardBorder = PlaceStore.sysConfig.cutBoardBorder;
let cutBoardBorderB = PlaceStore.sysConfig.cutBoardBorderB
if (board.isTwoFaceProcessing == false)
cutBoardBorderB = 0
let binPath: Path
if (board.points && board.points.length > 0) // 非矩形板
{
binPath = new Path(board.points)
}
else // 矩形板
{
binPath = new Path([{ x: cutBoardBorder, y: cutBoardBorder },
{ x: board.width - cutBoardBorderB, y: cutBoardBorder },
{ x: board.width - cutBoardBorderB, y: board.length - cutBoardBorderB },
{ x: cutBoardBorder, y: board.length - cutBoardBorderB },
])
}
binPath.Id = undefined
PathGeneratorSingle.RegisterId(binPath)
// 容器
let container = new Container(binPath)
for (let i = 0; i < board.blockList.length; i++) {
let block = board.blockList[i]
let part = this.toPart(block, binPath, this.offset * 2, i)
// 设置位置
part[0].PlacePosition = { x: (part[1].x + block.placeX + block.placeOffX - cutBoardBorder) * 1e4, y: (part[1].y + block.placeY + block.placeOffY - cutBoardBorder) * 1e4 }
container.PlacedParts.push(part[0])
}
// for (let i = 0; i < board.BlockList.length; i++)
// {
// let block = board.BlockList[i];
// let part = this.toPart(block, binPath, 0, board.BlockList.length + i);
// //设置位置
// part[0].PlacePosition = { x: (part[1].x + block.placeX - border) * 1e4, y: (part[1].y + block.placeY - border) * 1e4 };
// container.PlacedParts.push(part[0]);
// }
// let f = new NestFiler();
// f.Write(container.PlacedParts.length);
// for (let part of container.PlacedParts)
// f.Write(part.State.Contour.BigIntPoints);
// container.WriteFile(f);
// console.log(JSON.stringify(f._datas));
// FileZip.WriteFile(`${board.boardId}.container`, JSON.stringify(f._datas));
board.remainBlockList = [] // 清空
try {
let spaces = ParseOddments(container, binPath, this.offset, this.squarePath, this.canPutPaths)
for (let space of spaces) {
let sb = this.toRemainBlock(space)
sb.placeX = sb.placeX + cutBoardBorder
sb.placeY = sb.placeY + cutBoardBorder
board.remainBlockList.push(sb)
}
}
catch (err) {
// throw new Error(`计算余料空间异常:${pm.FullName} 第${board.boardId}片`);
console.log(`计算余料空间异常:${pm.fullName}${board.boardId}`, err)
throw new Error(`计算余料空间异常:${pm.fullName}${board.boardId}片;请保存优化联系售后工程师分析`)
}
// 分析小板内 造型孔洞 余料空间
for (let block of board.blockList) {
let childBlocks: PlaceBlock[]
// 造型孔
for (let m of block.models) {
if (m.depth < block.thickness - 0.05)
continue
if (m.hasContour == false)
continue
if (m.isCutting == false)
continue
if (!childBlocks) {
childBlocks = board.blockList.filter(t => t.placeX > block.placeX
&& t.placeX + t.placeWidth < block.placeX + block.placeWidth
&& t.placeY > block.placeY
&& t.placeY + t.placeLength < block.placeY + block.placeLength)
}
let spaces = await this.getModelSpace(block, m, childBlocks)
for (let space of spaces) {
let sb = this.toRemainBlock(space)
// sb.placeX += border;
// sb.placeY += border;
sb.placeX += block.placeX
sb.placeY += block.placeY
board.remainBlockList.push(sb)
}
}
}
board.isCreateRemainSpace = false
// console.log(`board id=${board.boardId} find scrap block count = ${board.remainBlockList.length}`);
// console.log(`${board.boardId} scraping ok`)
}
/** 获取造型空间 */
async getModelSpace(block: PlaceBlock, m: BlockModel, blocks: PlaceBlock[]): Promise<any[]> {
if (m.originModeling == null)
return []
let pts = m.originModeling.outline.pts
let buls = m.originModeling.outline.buls
if (pts == undefined) {
pts = m.originModeling.outline.map(e => e.pts)
}
if (buls == undefined) {
buls = m.originModeling.outline.map(e => e.buls)
}
// blocks = [];
// 起点
let pts_n = []
for (let i = 0; i < pts.length; i++) {
let p0 = BlockHelper.getPlaceXYInBlock(block, pts[i].x, pts[i].y, false, false)
pts_n.push({ x: p0.x, y: p0.y, bul: buls[i] })
}
// 如果反面,翻转方向,重设坐标
if (block.isTurnOver)
pts_n = this.reversePoint(pts_n, block.cutWidth)
// pts_n.forEach(t => { t.x += block.placeX; t.y += block.placeY; });
let polyLine
polyLine = Points2Polyline(pts_n)
let cicle = PolylineHelper.ConverPolyLin2Circle(polyLine)
if (cicle)
polyLine = cicle
let cureW2 = new CurveWrap(polyLine, this.offset, false)
let pts2 = cureW2.GetInsidePoints2(this.offset)
if (!pts2)
return []
let posx = 10000
let posy = 10000
for (let p of pts2) {
if (p.x < posx)
posx = p.x
if (p.y < posy)
posy = p.y
}
for (let p of pts2) {
p.x -= posx
p.y -= posy
}
let binPath = new Path(pts2)
if (binPath.Area < 15000)
return []
await InitClipperCpp()
binPath.Id = undefined
PathGeneratorSingle.RegisterId(binPath)
// 容器
let container = new Container(binPath)
let i = 0
for (let b of blocks) {
let part = this.toPart(b, binPath, this.offset, i)
if (part[0].State.Contour == null)
continue
part[0].Id = i++
// let isin = this.blockInBlock(block, b, polyLine);
// if (isin == false) continue;
// let part = this.toPart(b, binPath, this.offset, i++);
// 设置位置
part[0].PlacePosition = { x: (b.placeX - block.placeX - posx) * 1e4, y: (b.placeY - block.placeY - posy) * 1e4 }
container.PlacedParts.push(part[0])
}
try {
// let f = new NestFiler();
// f.Write(container.PlacedParts.length);
// for (let part of container.PlacedParts)
// f.Write(part.State.Contour.BigIntPoints);
// container.WriteFile(f);
// console.log(JSON.stringify(f._datas));
// FileZip.WriteFile(`${board.boardId}.container`, JSON.stringify(f._datas));
let spaces = ParseOddments(container, binPath, this.offset, this.squarePath, this.canPutPaths)
for (let space of spaces) {
space.Points.forEach((t) => { t.x += posx; t.y += posy })
}
return spaces
}
catch (err) {
console.log(`${block.blockNo}造型孔分析余料空间失败.`)
throw new Error(`${block.blockNo}造型孔分析余料空间失败.请保存优化联系售后工程师分析`)
}
// let spaces = ParseOddments(container, binPath, this.offset, this.squarePath, this.canPutPaths);
// for (let space of spaces)
// {
// space.Points.forEach(t => { t.x += posx; t.y += posy; });
// }
// return spaces;
}
/** 转配件 */
toPart(block: PlaceBlock, binPath: Path, offset: number, id: number): [Part, Vector2] {
let w = 10000
let binPath2 = new Path([{ x: -w, y: -w }, { x: w, y: -w }, { x: w, y: w }, { x: -w, y: w }])
let part = new Part()
part.Id = id
part.UserData = { bno: block.blockNo, area: block.area, isUnRegular: block.isUnRegular, width: block.cutWidth, length: block.cutLength }
let path: Path
let bPoint: Vector2
let sizeOff = BlockSizePlus.getOffDis(block) // 外扩偏移
let hasSizeOff = (sizeOff.left + sizeOff.right + sizeOff.top + sizeOff.bottom) > (block.placeMaterial.diameter * 2 + block.placeMaterial.cutKnifeGap * 2 + 0.01)
// 矩形且带板外不偏移
if (block.isUnRegular == false && hasSizeOff == false) {
path = NestCache.CreatePath(block.placeWidth + sizeOff.left + sizeOff.right, block.placeLength + sizeOff.top + sizeOff.bottom, offset)
bPoint = path.OrigionMinPoint
// 轮廓
part.Init2(path, binPath2, [0])
}
else // 异形
{
let pts = []
let cured = BlockPlus.getBorder_moving(block)
for (let l of cured) {
if (l instanceof Arc2d) {
pts.push({ x: l.StartPoint.m_X, y: l.StartPoint.m_Y, bul: l.Bul })
}
else {
pts.push({ x: l.StartPoint.m_X, y: l.StartPoint.m_Y, bul: 0 })
}
}
let polyLine
polyLine = Points2Polyline(pts)
polyLine.CloseMark = true
let cureW = new CurveWrap(polyLine, offset, true)
let pts2 = cureW.GetOutsidePoints()
arrayRemoveDuplicateBySort(pts, (p1, p2) => (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) < 1e-2)
path = new Path(pts2)
let cicle = PolylineHelper.ConverPolyLin2Circle(polyLine)
if (!cicle) {
let area = path.BoundingBox.area - path.Area
if (area < 15000 && pts.length > 6)
path = NestCache.CreatePath(block.placeWidth, block.placeLength, offset)
}
part.Init2(path, binPath2, [0])
// 不能放下,那么尝试不简化路径
if (!path.IsRect && !cicle && part.RotatedStates.length == 0) {
let pts2 = Polyline2Points(polyLine, true, offset)[1]
arrayRemoveDuplicateBySort(pts2, (p1, p2) => (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y) < 1e-2)
path = new Path(pts2)
part.Init2(path, binPath, [0])
}
// 轮廓
bPoint = path.OrigionMinPoint
}
if (hasSizeOff) {
bPoint.x = bPoint.x - sizeOff.left
bPoint.y = bPoint.y - sizeOff.bottom
}
return [part, bPoint]
}
/** 余料板空间 */
toRemainBlock(path: Path): RemainBlock {
let bx = path.OrigionMinPoint.x
let by = path.OrigionMinPoint.y
let points = path.Points
let ptsX = points.map((t) => { return t.x })
let ptsY = points.map((t) => { return t.y })
let x0 = ArrayExt.min(ptsX, t => t)
let y0 = ArrayExt.min(ptsY, t => t)
let x1 = ArrayExt.max(ptsX, t => t)
let y1 = ArrayExt.max(ptsY, t => t)
let sp = new RemainBlock(x0, y0, x1 - x0, y1 - y0)
let pts = points.map((t) => { return { x: t.x - x0, y: t.y - y0 } })
sp.placeX = bx + x0
sp.placeY = by + y0
sp.setPoints(pts)
return sp
}
/** 判断板是否在造型洞里头 */
blockInModelOfBlock(block: PlaceBlock, b: PlaceBlock, modlPl: Polyline, posX = 0, posY = 0): boolean {
let cured = BlockPlus.getBorder(b)
let pts = []
for (let l of cured) {
if (l instanceof Arc2d) {
pts.push({ x: l.StartPoint.m_X, y: l.StartPoint.m_Y, bul: l.Bul })
}
else {
pts.push({ x: l.StartPoint.m_X, y: l.StartPoint.m_Y, bul: 0 })
}
}
let polyLine = Points2Polyline(pts)
let cureW2 = new CurveWrap(polyLine, this.offset, false)
let pts2 = cureW2.GetInsidePoints2(this.offset)
// pts2 = pts2.map(t => { return { x: t.x + b.placeX - block.placeX - posX, y: t.y + b.placeY - block.placeY - posY }; });
for (let t of pts2) {
let x = t.x + b.placeX - block.placeX - posX
let y = t.y + b.placeY - block.placeY - posY
if (modlPl.PtInCurve(new Vector3(x, y, 0)) == false)
return false
}
return true
}
/** 反向点 */
private reversePoint(pts, w): any[] {
let newPts = []
for (let i = pts.length - 1; i >= 0; i--) {
let p = pts[i]
let x = p.x
let y = p.y
let j = i == 0 ? pts.length - 1 : i - 1
let bul = pts[j].bul
newPts.push({ x, y, bul })
}
return newPts
}
}

View File

@@ -0,0 +1,85 @@
import { Vector2 } from '../Vector2'
interface P {
x: number
y: number
}
export interface IOffset {
negativeOffset: number
positiveOffset: number
}
/** 点p到线段P1P2 的最短距离的平方,线段不延伸 */
function GetSqSegDist(p: P, p1: P, p2: P): number {
let x = p1.x
let y = p1.y
let dx = p2.x - x
let dy = p2.y - y
if (dx !== 0 || dy !== 0)// 不是0长度线
{
const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)
if (t > 1) {
x = p2.x
y = p2.y
}
else if (t > 0) {
x += dx * t
y += dy * t
}
}
dx = p.x - x
dy = p.y - y
return dx * dx + dy * dy
}
function CrossVector2(a: P, b: P)
{
return a.x * b.y - a.y * b.x
}
// Ramer-Douglas-Peucker algorithm
function SimplifyDPStep(points: P[], first: number, last: number, sqTolerance: number, simplified: P[], offset: IOffset): void {
let maxSqDist = 0
let index: number
const fp = points[first]
const lp = points[last]
for (let i = first + 1; i < last; i++) {
const p = points[i]
const sqDist = GetSqSegDist(p, fp, lp)
if (sqDist > maxSqDist) {
index = i
maxSqDist = sqDist
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1)
SimplifyDPStep(points, first, index, sqTolerance, simplified, offset)
simplified.push(points[index])
if (last - index > 1)
SimplifyDPStep(points, index, last, sqTolerance, simplified, offset)
}
else {
// 记录偏移
const v = new Vector2(lp.x - fp.x, lp.y - fp.y).normalize()
for (let i = first + 1; i < last; i++) {
const p = points[i]
const offsetDist = -CrossVector2(v, { x: p.x - fp.x, y: p.y - fp.y })
offset.positiveOffset = Math.max(offset.positiveOffset, offsetDist)
offset.negativeOffset = Math.min(offset.negativeOffset, offsetDist)
}
}
}
// Ramer-Douglas-Peucker 算法
export function SimplifyDouglasPeucker(points: P[], sqTolerance: number): [P[], IOffset] {
const last = points.length - 1
const simplified: P[] = [points[0]]
const offset: IOffset = { negativeOffset: 0, positiveOffset: 0 }
SimplifyDPStep(points, 0, last, sqTolerance, simplified, offset)
simplified.push(points[last])
return [simplified, offset]
}

View File

@@ -0,0 +1,773 @@
import { PolylineHelper } from '../../common/LayoutEngine/PolylineHelper.js'
import type { Curve2d } from '../../common/base/CAD.js'
import { Arc2d, CADExt, Line2d, Point2d } from '../../common/base/CAD.js'
import { equal } from '../../common/base/MathComm.js'
import { PlaceSpace,PlaceBlock,TextureType } from '../../confClass.js'
/** 生成矩形空间的相关算法 */
export class SpacePlus
{
/** 纹路0正文 1反纹 */
static texture :TextureType = 0 // = TextureType.NORMAL_TEXTURE // 正纹
/** 设置空间的方向,横向或竖向 */
static setSpaceStyle(block: PlaceBlock)
{
if (block.texture == TextureType.NORMAL_TEXTURE) // 正纹
{
this.texture = 0
}
else if (block.texture == TextureType.REVERSE_TEXTURE) // 反纹
{
this.texture = 1
}
else // 可翻转
{
if (block.cutWidth < block.cutLength)
{
this.texture = 0
}
else
{
this.texture = 1
}
}
}
/** 复制轮廓 */
static copyBorder(curves: Curve2d[]): Curve2d[]
{
const newCurves: Curve2d[] = []
for (let i = 0; i < curves.length; i++)
{
const curve = curves[i]
const sp = curve.StartPoint
const ep = curve.EndPoint
if (curve instanceof Arc2d)
{
const arc = new Arc2d(new Point2d(sp.m_X, sp.m_Y), new Point2d(ep.m_X, ep.m_Y), curve.Bul)
newCurves.push(arc)
}
else
{
// 后面会修改line里头的起点终点所以必须重新初始化, 确保不能影响原来的轮廓 border .
const newLine = new Line2d(new Point2d(sp.m_X, sp.m_Y), new Point2d(ep.m_X, ep.m_Y))
newCurves.push(newLine)
}
}
return newCurves
}
/** 从轮廓中分析空间 */
static borderToSpace(orgcurves: Curve2d[])
{
const curves = this.copyBorder(orgcurves)
// 1.新建线段列表 不影响源数据
// 弧线转成 直线
const lines: Line2d[] = []
for (let i = 0; i < curves.length;)
{
const curve = curves[i]
const sp = curve.StartPoint
const ep = curve.EndPoint
if (curve instanceof Arc2d)
{
const len = Math.abs(curve.m_Radius * curve.m_AllAngle) // 圆弧长度
if (len > 200) // 圆弧长大于 200 则拆分
{
const count = Math.ceil(len / 100)
const newArcs = CADExt.SplitArc(curve, count)
curves.splice(i, 1)
for (let j = newArcs.length - 1; j >= 0; j--)
{
curves.splice(i, 0, newArcs[j])
}
continue
}
else // 圆弧转换 直线
{
if (curve.Bul == 0)
{
curve.Parse()
}
const newLine = new Line2d(new Point2d(sp.m_X, sp.m_Y), new Point2d(ep.m_X, ep.m_Y))
// 外凸 直接转
if (curve.Bul >= 0)
{
newLine.canotSplit = true // 不能拆分
lines.push(newLine)
}
else // 内凹 算要算一下 弧形外接 矩形面积
{
const pts:any[] = []
pts.push({ x: curve.StartPoint.m_X, y: curve.StartPoint.m_Y, bul: curve.Bul })
pts.push({ x: curve.EndPoint.m_X, y: curve.EndPoint.m_Y, bul: 0 })
const pl = PolylineHelper.create(pts)
const box = pl.BoundingBox
this.setDirect(newLine)
const fx = newLine.fx
const newPoints :any[] = []
newPoints.push({ x: sp.m_X, y: sp.m_Y })
switch (fx)
{
case 0:
newPoints.push({ x: box.min.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.min.y })
break
case 1:
newPoints.push({ x: box.max.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.max.y })
break
case 2:
newPoints.push({ x: box.max.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.max.y })
break
case 3:
newPoints.push({ x: box.min.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.min.y })
break
case 4:
newPoints.push({ x: box.min.x, y: sp.m_Y })
newPoints.push({ x: box.min.x, y: box.max.y })
newPoints.push({ x: ep.m_X, y: box.max.y })
break
case 5:
newPoints.push({ x: sp.m_X, y: box.min.y })
newPoints.push({ x: box.min.x, y: box.min.y })
newPoints.push({ x: box.min.x, y: ep.m_Y })
break
case 6:
newPoints.push({ x: box.max.x, y: sp.m_Y })
newPoints.push({ x: box.max.x, y: box.min.y })
newPoints.push({ x: ep.m_X, y: box.min.y })
break
case 7:
newPoints.push({ x: sp.m_X, y: box.max.y })
newPoints.push({ x: box.max.x, y: box.max.y })
newPoints.push({ x: box.max.x, y: ep.m_Y })
break
default:
}
newPoints.push({ x: ep.m_X, y: ep.m_Y })
for (let n = 0; n < newPoints.length - 1; n++)
{
const m = n + 1
const newLine = new Line2d(new Point2d(newPoints[n].x, newPoints[n].y), new Point2d(newPoints[m].x, newPoints[m].y))
lines.push(newLine)
}
}
}
}
else
{
// 后面会修改line里头的起点终点所以必须重新初始化 ,确保不能影响原来的轮廓 border .
const newLine = new Line2d(new Point2d(sp.m_X, sp.m_Y), new Point2d(ep.m_X, ep.m_Y))
lines.push(newLine)
}
i++
}
// 2 轮廓需要够大
let minX = 1000
let minY = 1000
let maxX = -1000
let maxY = -1000
for (const line of lines)
{
if (line.StartPoint.m_X < minX)
minX = line.StartPoint.m_X
if (line.StartPoint.m_X > maxX)
maxX = line.StartPoint.m_X
if (line.StartPoint.m_Y < minY)
minY = line.StartPoint.m_Y
if (line.StartPoint.m_Y > maxY)
maxY = line.StartPoint.m_Y
if (line.EndPoint.m_X < minX)
minX = line.EndPoint.m_X
if (line.EndPoint.m_X > maxX)
maxX = line.EndPoint.m_X
if (line.EndPoint.m_Y < minY)
minY = line.EndPoint.m_Y
if (line.EndPoint.m_Y > maxY)
maxY = line.EndPoint.m_Y
}
if (maxX - minX < 50)
return // 太小了,就不要分析
if (maxY - minY < 50)
return
if ((maxX - minX) * (maxY - minY) < 10000)
return []
// 3.将长斜线 ,转成 多段
for (let i = 0; i < lines.length;)
{
const line = lines[i]
if (this.isXL(line))
{
if (line.canotSplit) // 不能拆分
{
}
else
{
const sp = line.StartPoint
const ep = line.EndPoint
if (line.m_Length > 300 && Math.abs(sp.m_X - ep.m_X) > 50 && Math.abs(sp.m_X - ep.m_X) > 50) // 够长,截断
{
const cx = (line.StartPoint.m_X + line.EndPoint.m_X) / 2
const cy = (line.StartPoint.m_Y + line.EndPoint.m_Y) / 2
line.EndPoint = new Point2d(cx, cy)
line.Parse()
const newLine = new Line2d(new Point2d(cx, cy), new Point2d(ep.m_X, ep.m_Y))
lines.splice(i + 1, 0, newLine)
continue
}
}
}
i++
}
// 3.斜线 -> 横平竖直
for (let i = 0; i < lines.length; i++)
{
this.turnToLine(lines, i)
}
// 6.将line 标识方向
for (let i = lines.length - 1; i >= 0; i--)
{
if (this.setDirect(lines[i]) == -1)
lines.splice(i, 1)
}
const tmpSpaces = []
// 7.开始分析空间
this.parseSpace(lines, tmpSpaces)
// 8.合并空间
this.mergeSpaces(tmpSpaces)
// 9 生成
const spaces: PlaceSpace[] = []
for (const tmp of tmpSpaces)
{
const space = PlaceSpace.create(tmp.x1, tmp.y1, tmp.x2, tmp.y2)
spaces.push(space)
}
return spaces
}
/** 闭合(逆时针)的线段内分析空间 */
private static parseSpace(lines: Line2d[], spaces: any[])
{
const childs = this.resetLines(lines)
if (childs.length > 1) // 分叉
{
for (const child of childs)
{
this.parseSpace(child, spaces)
}
return
}
// 没有分支,才开始真正计算
lines = childs[0]
if (lines.length < 4)
return // 至少有4条
/**
* 思路:在线段里头 找到一段连续且成矩形的3条直线即可组成一个矩形
* 然后去掉或缩短这3条直线再重新找
*/
let space
for (let i = 0; i < lines.length; i++)
{
space = this.createSpace(lines, i)
if (space)
break
}
// 没找到任何空间, 直接退出
if (!space)
return
// 找到了,继续往下找
spaces.push(space)
this.parseSpace(lines, spaces)
}
/** 分析空间大的保留,小的合并 */
static mergeSpaces(spaces: any[])
{
const minW = 50 // 空间最小宽度
const minL = 200
const avgW = 60
// 不损失 合并空间
for (let i = 0; i < spaces.length;)
{
const space1 = spaces[i]
let hasMerge = false // 是否合并一个空间
for (let j = i + 1; j < spaces.length;)
{
const space2 = spaces[j]
hasMerge = mergeSpace(space1, space2) // space2 是否被合并
if (hasMerge)
{
spaces.splice(j, 1)
break
}
j++
}
if (hasMerge)
break
i++
}
// 小空间 委屈合并
for (let i = 0; i < spaces.length;)
{
const space1 = spaces[i]
if (big(space1) == false) // 小空间,找别人合并
{
mergeSpace2(i)
spaces.splice(i, 1)// 删除小空间
continue
}
i++
}
/** 空间 大否 */
function big(space)
{
let w = space.x2 - space.x1
if (w < minW)
return false
let l = space.y2 - space.y1
if (l < minW)
return false
if (w > l)
[w, l] = [l, w]
if (w >= minW && l >= minL)
return true
if (w >= avgW && l >= avgW)
return true
return false
}
/** 不损失 合并空间 */
function mergeSpace(sp1, sp2)
{
// 上下 宽一样
if (equal(sp1.x1, sp2.x1) && equal(sp1.x2, sp2.x2) && (equal(sp1.y2, sp2.y1) || equal(sp2.y2, sp1.y1)))
{
sp1.y1 = Math.min(sp1.y1, sp2.y1)
sp1.y2 = Math.max(sp1.y2, sp2.y2)
return true
}
// 左右 高一样
if (equal(sp1.y1, sp2.y1) && equal(sp1.y2, sp2.y2) && (equal(sp1.x2, sp2.x1) || equal(sp2.x2, sp1.x1)))
{
sp1.x1 = Math.min(sp1.x1, sp2.x1)
sp1.x2 = Math.max(sp1.x2, sp2.x2)
return true
}
return false
}
/** 损失 合并空间,四周找一个空间,合并后效果最好的 */
function mergeSpace2(index: number)
{
const cur = spaces[index]
const canSpaces = []
for (let i = 0; i < spaces.length; i++)
{
if (i == index)
continue
const oth = spaces[i]
let x1, y1, x2, y2
// 右边的
if (equal(cur.x2, oth.x1) && cur.y1 < oth.y2 && cur.y2 > oth.y1)
{
x1 = cur.x1
y1 = Math.max(cur.y1, oth.y1)
x2 = oth.x2
y2 = Math.min(cur.y2, oth.y2)
}
// 左边的
else if (equal(cur.x1, oth.x2) && cur.y1 < oth.y2 && cur.y2 > oth.y1)
{
x1 = oth.x1
y1 = Math.max(cur.y1, oth.y1)
x2 = cur.x2
y2 = Math.min(cur.y2, oth.y2)
}
// 下边的
else if (equal(cur.y1, oth.y2) && cur.x1 < oth.x2 && cur.x2 > oth.x1)
{
x1 = Math.max(cur.x1, oth.x1)
y1 = oth.y1
x2 = Math.min(cur.x2, oth.x2)
y2 = cur.y2
}
// 上边的
else if (equal(cur.y2, oth.y1) && cur.x1 < oth.x2 && cur.x2 > oth.x1)
{
x1 = Math.max(cur.x1, oth.x1)
y1 = cur.y1
x2 = Math.min(cur.x2, oth.x2)
y2 = oth.y2
}
else
{
continue
}
// oth 原来面积
const size_org = (oth.x2 - oth.x1) * (oth.y2 - oth.y1)
const size_new = (x2 - x1) * (y2 - y1)
const size_plus = size_new - size_org
if (size_plus < 0)
continue // 合并 面积更小,就不要合并
const space = { x1, y1, x2, y2 }
canSpaces.push({ size_plus, space, i })
}
if (canSpaces.length == 0)
return // 没有可以合并的
// 按增大面积 排序
canSpaces.sort((a, b) => b.size_plus - a.size_plus)
// 取效果最好的。
const newSpace = canSpaces[0].space
// 替换 oth
spaces.splice(canSpaces[0].i, 1, newSpace)
}
}
/** 整理多段线 */
private static resetLines(lines: Line2d[]): Line2d[][]
{
for (let i = 0; i < lines.length;)
{
const lp = lines[i]
let n = i + 1
if (n == lines.length)
n = 0
const ln = lines[n]
if (lp.m_Length < 0.001) // 太短了,移除
{
lines.splice(i, 1)
i = 0
continue
}
if (lp.fx == ln.fx) // 同向,
{
lp.EndPoint = ln.EndPoint
lp.Parse()
lines.splice(n, 1)
i = 0
continue
}
if (lp.fx % 2 == ln.fx % 2) // 反向
{
lp.EndPoint = ln.EndPoint
lp.Parse()
this.setDirect(lp)
lines.splice(n, 1)
if ((lp.fx == 0 && lp.EndPoint.m_X < lp.StartPoint.m_X)
|| (lp.fx == 1 && lp.EndPoint.m_Y < lp.StartPoint.m_Y)
|| (lp.fx == 2 && lp.EndPoint.m_X > lp.StartPoint.m_X)
|| (lp.fx == 3 && lp.EndPoint.m_Y > lp.StartPoint.m_Y)) // 这很奇葩,该空间 不能分析,直接清除所有线段
{
lines.splice(0, lines.length)
return
}
i = 0
continue
}
i++
}
// 判断 有向下的线,与向上的线 重叠。 则要分开; 类似 与 管 的下面 2个口
let line0: Line2d
let line1: Line2d
let i = -1
let j = -1
let x = 0
for (i = 0; i < lines.length; i++)
{
if (lines[i].fx == 3)
{
x = lines[i].StartPoint.m_X
j = lines.findIndex(t => t.fx == 1 && t.StartPoint.m_X == x)
if (j >= 0)
{
line0 = lines[i]
line1 = lines[j]
break
}
}
}
if (!line0 || !line1)
return [lines] // 没找到
const rt = []
const si = Math.min(i, j)
const ei = Math.max(i, j)
const lines_s: Line2d[] = []
const lines_x: Line2d[] = []
for (let i = si + 1; i <= ei - 1; i++)
{
lines_s.push(lines[i])
}
for (let i = ei + 1; i <= lines.length + si - 1; i++)
{
const ri = i % lines.length
lines_x.push(lines[ri])
}
if (lines_s.length >= 3)
{
const pe = lines_s[lines_s.length - 1].EndPoint
const ps = lines_s[0].StartPoint
const newLine = new Line2d(new Point2d(pe.m_X, pe.m_Y), new Point2d(ps.m_X, ps.m_Y))
this.setDirect(newLine)
lines_s.push(newLine)
rt.push(lines_s)
}
if (lines_x.length >= 3)
{
const pe = lines_x[lines_x.length - 1].EndPoint
const ps = lines_x[0].StartPoint
const newLine = new Line2d(new Point2d(pe.m_X, pe.m_Y), new Point2d(ps.m_X, ps.m_Y))
this.setDirect(newLine)
lines_x.push(newLine)
rt.push(lines_x)
}
return rt
}
/** line如果是斜线转换成直线 */
private static turnToLine(lines: Line2d[], i: number)
{
if (!this.isXL(lines[i]))
return
const sp = lines[i].StartPoint
const ep = lines[i].EndPoint
const sx = sp.m_X
const sy = sp.m_Y
const ex = ep.m_X
const ey = ep.m_Y
let cx = 0
let cy = 0
// ↖ 换成 ←↑
if (ex < sx && ey > sy)
{
cx = ex
cy = sy
}
else if (ex < sx && ey < sy) // ↙ 换成 ←↓
{
cx = sx
cy = ey
}
else if (ex > sx && ey < sy) // ↘ 换成 →↓
{
cx = ex
cy = sy
}
else if (ex > sx && ey > sy) // ↗ 换成 →↑
{
cx = sx
cy = ey
}
if (cx == 0 && cy == 0)
return
const line1 = new Line2d(new Point2d(sx, sy), new Point2d(cx, cy))
const line2 = new Line2d(new Point2d(cx, cy), new Point2d(ex, ey))
lines.splice(i, 1, line1, line2)
}
/** 线段设置方向 */
private static setDirect(line: Line2d)
{
let fx = -1 // 向右 0,向上 1向左 2向下 3 ,右上 4,左上5,左下6右下 7
if (line.EndPoint.m_X > line.StartPoint.m_X && equal(line.EndPoint.m_Y, line.StartPoint.m_Y))
{
fx = 0
}
else if (line.EndPoint.m_X < line.StartPoint.m_X && equal(line.EndPoint.m_Y, line.StartPoint.m_Y))
{
fx = 2
}
else if (line.EndPoint.m_Y > line.StartPoint.m_Y && equal(line.EndPoint.m_X, line.StartPoint.m_X))
{
fx = 1
}
else if (line.EndPoint.m_Y < line.StartPoint.m_Y && equal(line.EndPoint.m_X, line.StartPoint.m_X))
{
fx = 3
}
else if (line.EndPoint.m_X > line.StartPoint.m_X && line.EndPoint.m_Y > line.StartPoint.m_Y)
{
fx = 4
}
else if (line.EndPoint.m_X < line.StartPoint.m_X && line.EndPoint.m_Y > line.StartPoint.m_Y)
{
fx = 5
}
else if (line.EndPoint.m_X < line.StartPoint.m_X && line.EndPoint.m_Y < line.StartPoint.m_Y)
{
fx = 6
}
else if (line.EndPoint.m_X > line.StartPoint.m_X && line.EndPoint.m_Y < line.StartPoint.m_Y)
{
fx = 7
}
line.fx = fx
return fx
}
/** 斜线 */
private static isXL(line: Curve2d) { return !equal(line.StartPoint.m_X, line.EndPoint.m_X) && !equal(line.StartPoint.m_Y, line.EndPoint.m_Y) }
/** 3条线方向持续 组成空间 */
private static createSpace(lines: Line2d[], i: number)
{
let j = i + 1
let n = i + 2
if (j >= lines.length)
j = j - lines.length
if (n >= lines.length)
n = n - lines.length
const line1 = lines[i]
const line2 = lines[j]
const line3 = lines[n]
const fx1 = line1.fx
if (line2.fx != (fx1 + 1) % 4)
return
if (line3.fx != (fx1 + 2) % 4)
return
// 安装板的纹路,进行开始计算空间,横的,还是竖的
if (fx1 % 2 != this.texture)
return
let x1, y1, x2, y2
let sp, ep
if (fx1 == 0)
{
x1 = Math.max(line1.StartPoint.m_X, line3.EndPoint.m_X)
y1 = line2.StartPoint.m_Y
x2 = line2.EndPoint.m_X
y2 = line2.EndPoint.m_Y
sp = new Point2d(x1, y1)
ep = new Point2d(x1, y2)
}
else if (fx1 == 1)
{
x1 = line2.EndPoint.m_X
y1 = Math.max(line1.StartPoint.m_Y, line3.EndPoint.m_Y)
x2 = line2.StartPoint.m_X
y2 = line2.StartPoint.m_Y
sp = new Point2d(x2, y1)
ep = new Point2d(x1, y1)
}
else if (fx1 == 2)
{
x1 = line2.EndPoint.m_X
y1 = line2.EndPoint.m_Y
x2 = Math.min(line1.StartPoint.m_X, line3.EndPoint.m_X)
y2 = line2.StartPoint.m_Y
sp = new Point2d(x2, y2)
ep = new Point2d(x2, y1)
}
else if (fx1 == 3)
{
x1 = line2.StartPoint.m_X
y1 = line2.StartPoint.m_Y
x2 = line2.EndPoint.m_X
y2 = Math.min(line1.StartPoint.m_Y, line3.EndPoint.m_Y)
sp = new Point2d(x1, y2)
ep = new Point2d(x2, y2)
}
else
{
return
}
// 在新空间内,不能出现其他的线段,
for (let o = 0; o < lines.length; o++)
{
if (o == i || o == j || o == n)
continue
const oth = lines[o]
if (oth.EndPoint.m_X > x1 + 0.001 && oth.EndPoint.m_X < x2 - 0.001 && oth.EndPoint.m_Y > y1 + 0.001 && oth.EndPoint.m_Y < y2 - 0.001)
return
}
// 缩短line1 line3
line1.EndPoint = sp
line1.Parse()
this.setDirect(line1)
line3.StartPoint = ep
line3.Parse()
this.setDirect(line3)
// 删除 line2 ,添加 新连接线
const newline = new Line2d(new Point2d(sp.m_X, sp.m_Y), new Point2d(ep.m_X, ep.m_Y))
this.setDirect(newline)
lines.splice(j, 1, newline)
const space = { x1, y1, x2, y2 }
return space
}
/** 反转 */
static reverseCurves(curves: Curve2d[]): Curve2d[]
{
const newCurs: Curve2d[] = []
for (let i = curves.length - 1; i >= 0; i--)
{
const cur = curves[i]
const sp = cur.StartPoint
const ep = cur.EndPoint
if (cur instanceof Arc2d)
{
const newArc = new Arc2d(ep, sp, -cur.Bul)
newCurs.push(newArc)
}
else
{
const newline = new Line2d(ep, sp)
newCurs.push(newline)
}
}
return newCurs
}
}

View File

@@ -0,0 +1,104 @@
import type { PolylineProps } from 'cadapi'
import { CADFiler, Polyline } from 'cadapi'
import { Vector2 } from 'three'
import { copyTextToClipboard } from '../base/CAD'
export class ClipboardTest {
public static writePolyline1(pl: Polyline, pts1) {
// pl 原图
// pts :偏移后的点
const lined: PolylineProps[] = []
const count = pts1.length
for (let i = 0; i < count; i++) {
const p0 = pts1[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: 0 })
}
const pl1 = new Polyline(lined)
pl1.CloseMark = true
const f = new CADFiler()
f.Clear()
f.Write(2)
f.WriteObject(pl)
f.WriteObject(pl1)
const test = JSON.stringify(f.Data)
// for (let pl of lined)
// f.WriteObject(pl)
copyTextToClipboard(test)
}
public static writePolyline2(pl: Polyline, pts1, pts2) {
// pl 原图
// pts :偏移后的点
const lined: PolylineProps[] = []
const count = pts1.length
for (let i = 0; i < count; i++) {
const p0 = pts1[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: 0 })
}
const pl1 = new Polyline(lined)
const lined2: PolylineProps[] = []
const count2 = pts2.length
for (let i = 0; i < count2; i++) {
const p0 = pts2[i]
lined2.push({ pt: new Vector2(p0.x, p0.y), bul: 0 })
}
const pl2 = new Polyline(lined2)
const f = new CADFiler()
f.Clear()
f.Write(3)
f.WriteObject(pl)
f.WriteObject(pl1)
f.WriteObject(pl2)
const test = JSON.stringify(f.Data)
// for (let pl of lined)
// f.WriteObject(pl)
copyTextToClipboard(test)
}
public static writeClipboard(pts) {
const lined: PolylineProps[] = []
const count = pts.length
for (let i = 0; i < count; i++) {
const p0 = pts[i]
lined.push({ pt: new Vector2(p0.x, p0.y), bul: 0 })
}
const pls = new Polyline(lined)
const f = new CADFiler()
f.Clear()
f.Write(1)
f.WriteObject(pls)
const test = JSON.stringify(f.Data)
// for (let pl of lined)
// f.WriteObject(pl)
copyTextToClipboard(test)
}
public static writePolyLine(pls) {
const f = new CADFiler()
f.Clear()
f.Write(1)
f.WriteObject(pls)
const test = JSON.stringify(f.Data)
copyTextToClipboard(test)
}
public static write2PolyLine(pls, pls2) {
const f = new CADFiler()
f.Clear()
f.Write(2)
f.WriteObject(pls)
f.WriteObject(pls2)
const test = JSON.stringify(f.Data)
copyTextToClipboard(test)
}
}

View File

@@ -0,0 +1,143 @@
import { PlaceStyle, PlaceBlock, PlaceBoard, BoardPosition } from "../confClass";
/** 靠板类 */
export class PlacePosition {
static turnPlacePosition(pb: PlaceBoard, newlocator: BoardPosition, config) {
const { placeOriginByBoardLocation } = config
if (placeOriginByBoardLocation == false) return;
if (pb.isAdnormal()) return; //余料板是余料板,不参与翻转
let width = pb.width;
let length = pb.length;
//右下角靠板
if (newlocator == BoardPosition.RIGHT_BOTTOM) {
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth;
let y = block.placeY;
let placeStyle = this.getPlaceStyleLeftRight(block);
block.placeX = x;
block.placeY = y;
block.placeStyle = placeStyle;
}
}
//右上角靠板
if (newlocator == BoardPosition.RIGHT_TOP) {
// console.log('BoardPosition=BoardPosition.RIGHT_TOP');
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth;
let y = length - block.placeLength - block.placeY;
let placeStyle = this.getPlaceStyleAcrossCorner(block);
block.placeX = x;
block.placeY = y;
block.placeStyle = placeStyle;
}
}
//左上角靠板
if (newlocator == BoardPosition.LEFT_TOP) {
for (let block of pb.blockList) {
let x = block.placeX;
let y = length - block.placeLength - block.placeY;
let placeStyle = this.getPlaceStyleTopBottom(block);
block.placeX = x;
block.placeY = y;
block.placeStyle = placeStyle;
}
}
}
/** 重置排版位置 */
static resetPlacePosition(pb: PlaceBoard, config) {
const { placeOriginByBoardLocation = false, boardLocation } = config
const newlocator: BoardPosition = boardLocation
if (placeOriginByBoardLocation == false) return;
if (newlocator == BoardPosition.LEFT_BOTTOM) return;
let width = pb.width;
let length = pb.length;
//右下角靠板
if (newlocator == BoardPosition.RIGHT_BOTTOM) {
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth;
let y = block.placeY;
let placeStyle = this.getPlaceStyleLeftRight(block);
block.placeX = x;
block.placeY = y;
block.placeStyle = placeStyle;
}
}
//右上角靠板
if (newlocator == BoardPosition.RIGHT_TOP) {
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth;
let y = length - block.placeLength - block.placeY;
let placeStyle = this.getPlaceStyleAcrossCorner(block);
block.placeX = x;
block.placeY = y;
block.placeStyle = placeStyle;
}
}
//左上角, 靠板
if (newlocator == BoardPosition.LEFT_TOP) {
for (let block of pb.blockList) {
let x = block.placeX;
let y = length - block.placeLength - block.placeY;
let placeStyle = this.getPlaceStyleTopBottom(block);
block.placeX = x;
block.placeY = y;
block.placeStyle = placeStyle;
}
}
}
/** 获得新的放置方式(左右翻) */
static getPlaceStyleLeftRight(block: PlaceBlock): PlaceStyle {
if (block.placeStyle == PlaceStyle.FRONT) return PlaceStyle.BACK;
if (block.placeStyle == PlaceStyle.FRONT_TURN_RIGHT) return PlaceStyle.BACK_TURN_LEFT;
if (block.placeStyle == PlaceStyle.FRONT_TURN_BACK) return PlaceStyle.BACK_TURN_BACK;
if (block.placeStyle == PlaceStyle.FRONT_TURN_LEFT) return PlaceStyle.BACK_TURN_RIGHT;
if (block.placeStyle == PlaceStyle.BACK) return PlaceStyle.FRONT;
if (block.placeStyle == PlaceStyle.BACK_TURN_LEFT) return PlaceStyle.FRONT_TURN_RIGHT;
if (block.placeStyle == PlaceStyle.BACK_TURN_BACK) return PlaceStyle.FRONT_TURN_BACK;
if (block.placeStyle == PlaceStyle.BACK_TURN_RIGHT) return PlaceStyle.FRONT_TURN_LEFT;
return PlaceStyle.FRONT;
}
/** 获得新的放置方式(上下翻) */
static getPlaceStyleTopBottom(block: PlaceBlock): PlaceStyle {
if (block.placeStyle == PlaceStyle.FRONT) return PlaceStyle.BACK_TURN_BACK;
if (block.placeStyle == PlaceStyle.FRONT_TURN_RIGHT) return PlaceStyle.BACK_TURN_RIGHT;
if (block.placeStyle == PlaceStyle.FRONT_TURN_BACK) return PlaceStyle.BACK;
if (block.placeStyle == PlaceStyle.FRONT_TURN_LEFT) return PlaceStyle.BACK_TURN_LEFT;
if (block.placeStyle == PlaceStyle.BACK_TURN_BACK) return PlaceStyle.FRONT;
if (block.placeStyle == PlaceStyle.BACK_TURN_RIGHT) return PlaceStyle.FRONT_TURN_RIGHT;
if (block.placeStyle == PlaceStyle.BACK) return PlaceStyle.FRONT_TURN_BACK;
if (block.placeStyle == PlaceStyle.BACK_TURN_LEFT) return PlaceStyle.FRONT_TURN_LEFT;
return PlaceStyle.FRONT;
}
/** 获得新的放置方式(对角翻) */
static getPlaceStyleAcrossCorner(block: PlaceBlock): PlaceStyle {
if (block.placeStyle == PlaceStyle.FRONT) return PlaceStyle.FRONT_TURN_BACK;
if (block.placeStyle == PlaceStyle.FRONT_TURN_RIGHT) return PlaceStyle.FRONT_TURN_LEFT;
if (block.placeStyle == PlaceStyle.FRONT_TURN_BACK) return PlaceStyle.FRONT;
if (block.placeStyle == PlaceStyle.FRONT_TURN_LEFT) return PlaceStyle.FRONT_TURN_RIGHT;
if (block.placeStyle == PlaceStyle.BACK_TURN_BACK) return PlaceStyle.BACK;
if (block.placeStyle == PlaceStyle.BACK_TURN_RIGHT) return PlaceStyle.BACK_TURN_LEFT;
if (block.placeStyle == PlaceStyle.BACK) return PlaceStyle.BACK_TURN_BACK;
if (block.placeStyle == PlaceStyle.BACK_TURN_LEFT) return PlaceStyle.BACK_TURN_RIGHT;
return PlaceStyle.FRONT;
}
}

View File

@@ -0,0 +1,176 @@
import { PlaceStyle, BoardPosition, PlaceBoard, PlaceBlock } from '../confClass'
export class PlacePositionHelper // 靠板类
{
static turnPlacePosition(pb: PlaceBoard, newlocator: BoardPosition, config: any) {
const { placeOriginByBoardLocation } = config
if (placeOriginByBoardLocation == false)
return
if (pb.isAdnormal())
return // 余料板是余料板,不参与翻转
let width = pb.width
let length = pb.length
// RIGHT_BOTTOM, 靠板
if (newlocator == BoardPosition.RIGHT_BOTTOM) {
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth
let y = block.placeY
let placeStyle = this.getPlaceStyle_zy(block)
block.placeX = x
block.placeY = y
block.placeStyle = placeStyle
}
}
// RIGHT_TOP, 靠板
if (newlocator == BoardPosition.RIGHT_TOP) {
console.log('BoardPosition=BoardPosition.RIGHT_TOP')
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth
let y = length - block.placeLength - block.placeY
let placeStyle = this.getPlaceStyle_dj(block)
block.placeX = x
block.placeY = y
block.placeStyle = placeStyle
}
}
// 左上角, 靠板
if (newlocator == BoardPosition.LEFT_TOP) {
console.log('BoardPosition=BoardPosition.左上角')
for (let block of pb.blockList) {
let x = block.placeX
let y = length - block.placeLength - block.placeY
let placeStyle = this.getPlaceStyle_sx(block)
block.placeX = x
block.placeY = y
block.placeStyle = placeStyle
}
}
}
static resetPlacePosition(pb: PlaceBoard, newlocator: BoardPosition, config: any) {
const { placeOriginByBoardLocation } = config
if (placeOriginByBoardLocation == false)
return
if (newlocator == BoardPosition.RIGHT_BOTTOM)
return
let width = pb.width
let length = pb.length
// RIGHT_BOTTOM, 靠板
if (newlocator == BoardPosition.RIGHT_TOP) {
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth
let y = block.placeY
let placeStyle = this.getPlaceStyle_zy(block)
block.placeX = x
block.placeY = y
block.placeStyle = placeStyle
}
}
// RIGHT_TOP, 靠板
if (newlocator == BoardPosition.RIGHT_TOP) {
for (let block of pb.blockList) {
let x = width - block.placeX - block.placeWidth
let y = length - block.placeLength - block.placeY
let placeStyle = this.getPlaceStyle_dj(block)
block.placeX = x
block.placeY = y
block.placeStyle = placeStyle
}
}
// 左上角, 靠板
if (newlocator == BoardPosition.LEFT_TOP) {
for (let block of pb.blockList) {
let x = block.placeX
let y = length - block.placeLength - block.placeY
let placeStyle = this.getPlaceStyle_sx(block)
block.placeX = x
block.placeY = y
block.placeStyle = placeStyle
}
}
}
// 获得新的放置方式
static getPlaceStyle_zy(block: PlaceBlock): PlaceStyle // 左右翻,
{
// if (block.IsUnRegular == false && block.HasThroghModel == false) return block.placeStyle;
if (block.placeStyle == PlaceStyle.FRONT)
return PlaceStyle.BACK
if (block.placeStyle == PlaceStyle.FRONT_TURN_RIGHT)
return PlaceStyle.BACK_TURN_LEFT
if (block.placeStyle == PlaceStyle.FRONT_TURN_BACK)
return PlaceStyle.BACK_TURN_BACK
if (block.placeStyle == PlaceStyle.FRONT_TURN_LEFT)
return PlaceStyle.BACK_TURN_RIGHT
if (block.placeStyle == PlaceStyle.BACK)
return PlaceStyle.FRONT
if (block.placeStyle == PlaceStyle.BACK_TURN_LEFT)
return PlaceStyle.FRONT_TURN_RIGHT
if (block.placeStyle == PlaceStyle.BACK_TURN_BACK)
return PlaceStyle.FRONT_TURN_BACK
if (block.placeStyle == PlaceStyle.BACK_TURN_RIGHT)
return PlaceStyle.FRONT_TURN_LEFT
return PlaceStyle.FRONT
}
static getPlaceStyle_sx(block: PlaceBlock): PlaceStyle // 上下翻,
{
// if (block.IsUnRegular == false && block.HasThroghModel == false) return block.placeStyle;
if (block.placeStyle == PlaceStyle.FRONT)
return PlaceStyle.BACK_TURN_BACK
if (block.placeStyle == PlaceStyle.FRONT_TURN_RIGHT)
return PlaceStyle.BACK_TURN_RIGHT
if (block.placeStyle == PlaceStyle.FRONT_TURN_BACK)
return PlaceStyle.BACK
if (block.placeStyle == PlaceStyle.FRONT_TURN_LEFT)
return PlaceStyle.BACK_TURN_LEFT
if (block.placeStyle == PlaceStyle.BACK_TURN_BACK)
return PlaceStyle.FRONT
if (block.placeStyle == PlaceStyle.BACK_TURN_RIGHT)
return PlaceStyle.FRONT_TURN_RIGHT
if (block.placeStyle == PlaceStyle.BACK)
return PlaceStyle.FRONT_TURN_BACK
if (block.placeStyle == PlaceStyle.BACK_TURN_LEFT)
return PlaceStyle.FRONT_TURN_LEFT
return PlaceStyle.FRONT
}
static getPlaceStyle_dj(block: PlaceBlock): PlaceStyle // 对角翻
{
// if (block.IsUnRegular == false && block.HasThroghModel == false) return block.placeStyle;
if (block.placeStyle == PlaceStyle.FRONT)
return PlaceStyle.FRONT_TURN_BACK
if (block.placeStyle == PlaceStyle.FRONT_TURN_RIGHT)
return PlaceStyle.FRONT_TURN_RIGHT
if (block.placeStyle == PlaceStyle.FRONT_TURN_BACK)
return PlaceStyle.FRONT
if (block.placeStyle == PlaceStyle.FRONT_TURN_LEFT)
return PlaceStyle.FRONT_TURN_RIGHT
if (block.placeStyle == PlaceStyle.BACK_TURN_BACK)
return PlaceStyle.BACK
if (block.placeStyle == PlaceStyle.BACK_TURN_RIGHT)
return PlaceStyle.BACK_TURN_LEFT
if (block.placeStyle == PlaceStyle.BACK)
return PlaceStyle.BACK_TURN_BACK
if (block.placeStyle == PlaceStyle.BACK_TURN_LEFT)
return PlaceStyle.BACK_TURN_RIGHT
return PlaceStyle.FRONT
}
}

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
/** 判断两个值是否相等 */
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 = (Array.isArray(arr)) ? 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
}

View File

@@ -0,0 +1,348 @@
/** 点 */
export class Point
{
/** 坐标x */
x: number
/** 坐标y */
y: number
constructor(x: number, y: number)
{
this.x = x
this.y = y
}
}
/** 二维向量 */
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 (Number.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: Point): Vector2
{
this.x = Math.min(this.x, v.x)
this.y = Math.min(this.y, v.y)
return this
}
max(v: Point): 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; const 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); const 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
}
}

View File

@@ -0,0 +1,375 @@
export class List<Item> extends Array<Item>
{
/** 返回符合条件的第一个元素 */
first(fn: (item: Item) => boolean): Item
{
if (this.length == 0)
return null
for (const item of this)
{
if (fn(item))
return item
}
return null
}
/** 返回符合条件的最后一元素 */
last(fn: (t: Item) => boolean): Item
{
if (this.length == 0)
return null
for (let i = this.length - 1; i >= 0; i--)
{
if (fn(this[i]))
return this[i]
}
return null
}
/** 最大值 */
max<T>(fn: (item: Item) => T): T
{
let maxV: T
for (const i of this)
{
let v = fn(i)
if (maxV == null)
{
maxV = fn(i)
}
else
{
if (v > maxV)
{
maxV = v
}
}
}
return maxV
}
/** 最小值 */
min<T>(fn: (item: Item) => T): T
{
let minV: T
for (const i of this)
{
let v = fn(i)
if (minV == null)
{
minV = fn(i)
}
else
{
if (v < minV)
{
minV = v
}
}
}
return minV
}
/** 累加 */
sum(fn: (item: Item) => number): number
{
let v: number = 0
for (const t of this)
{
v = v + fn(t)
}
return v
}
/** 平均值 */
avg(fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let sum = this.sum(fn)
return sum / this.length
}
/** 满足条件的元素数量 */
count(fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let c = 0
for (const item of this)
{
if (fn(item))
c = c + 1
}
return c
}
FindMax<T>(fn: (item: Item) => T): Item
{
return this.reduce((a: Item, b: Item): Item => fn(a) > fn(b) ? a : b)
}
}
export class ArrayExt
{
/** 返回满足条件的元素数量 */
static count<Item>(list: Item[], fn: (item: Item) => boolean): number
{
if (list.length == 0)
return 0
let c = 0
for (const item of list)
{
if (fn(item))
c = c + 1
}
return c
}
/** 移除 */
static remove<Item>(list: Item[], obj: Item)
{
let index = list.findIndex(t => t == obj)
if (index == -1)
return
list.splice(index, 1)
}
/** 返回符合条件的第一个元素 */
static first<Item>(list: Item[], fn: (item: Item) => boolean, orderFn1: (item: Item) => number = null, orderFn2: (item: Item) => number = null): Item
{
if (list.length == 0)
return null
if (orderFn1 == null)
{
for (const item of list)
{
if (fn(item))
return item
}
return null
}
let minValue1: number
let minValue2: number
let minItem: Item
for (const item of list)
{
if (fn(item) == false)
continue
let v1 = orderFn1(item)
let v2 = orderFn2 != null ? orderFn2(item) : 0
if (minValue1 == null || v1 < minValue1 || (v1 == minValue1 && v2 < minValue2))
{
minValue1 = v1
minValue2 = v2
minItem = item
}
}
return minItem
}
/** 返回符合条件的最后一元素 */
static last<Item>(list: Item[], fn: (t: Item) => boolean, orderFn1: (item: Item) => number = null, orderFn2: (item: Item) => number = null): Item
{
if (list.length == 0)
return null
if (orderFn1 == null)
{
for (let i = list.length - 1; i >= 0; i--)
{
if (fn(list[i]))
return list[i]
}
return null
}
//
let maxValue1: number
let maxValue2: number
let maxItem: Item
for (const item of list)
{
if (fn(item) == false)
continue
let v1 = orderFn1(item)
let v2 = orderFn2 ? orderFn2(item) : 0
if (maxValue1 == null || v1 > maxValue1 || (v1 == maxValue1 && v2 > maxValue2))
{
maxValue1 = v1
maxValue2 = v2
maxItem = item
}
}
return maxItem
}
/** 取最大值 */
static max<T1, T2>(list: T1[], fn: (item: T1) => T2, whereF: (item: T1) => boolean = null, defaultV: T2 = null): T2
{
let maxV: T2
for (const i of list)
{
if (whereF && whereF(i) == false)
continue
let v = fn(i)
if (maxV == undefined)
{
maxV = fn(i)
}
else
{
if (v > maxV)
{
maxV = v
}
}
}
if (maxV != undefined)
return maxV
return defaultV
}
/** 最小值 */
static min<Item, T>(list: Item[], fn: (item: Item) => T, whereF: (item: Item) => boolean = null, defaultV: T = null): T
{
let minV: T
for (const i of list)
{
if (whereF && whereF(i) == false)
continue
let v = fn(i)
if (minV == undefined)
{
minV = v
}
else
{
if (v < minV)
{
minV = v
}
}
}
if (minV != undefined)
return minV
return defaultV
}
/** 累加 */
static sum<Item>(list: Item[], fn: (item: Item) => number, wn?: (item: Item) => boolean): number
{
let v: number = 0
for (const t of list)
{
if (wn && wn(t) == false)
continue
v = v + fn(t)
}
return v
}
/** 平均值 */
static avg<Item>(list: Item[], fn: (item: Item) => number): number
{
if (this.length == 0)
return 0
let sum = ArrayExt.sum(list, fn)
return sum / this.length
}
/** 排序 */
static sortBy<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null)
{
if (fn2 == null)
return list.sort((a: Item, b: Item): number => fn(a) - fn(b))
else
return list.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(a) - fn2(b)) : fn(a) - fn(b))
}
/** 降序 排序 */
static sortByDescending<Item>(list: Item[], fn: (item: Item) => number)
{
list.sort((a: Item, b: Item): number => fn(b) - fn(a))
}
/** 排序成新的数组 */
static orderBy<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null): Item[]
{
let newList = list.concat([])
if (fn2 == null)
return newList.sort((a: Item, b: Item): number => fn(a) - fn(b))
else
return newList.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(a) - fn2(b)) : fn(a) - fn(b))
}
/** 降序成新的数组 */
static orderByDescending<Item>(list: Item[], fn: (item: Item) => number, fn2: (item: Item) => number = null): Item[]
{
let newList = list.concat([])
if (fn2 == null)
return list.sort((a: Item, b: Item): number => fn(b) - fn(a))
else
return list.sort((a: Item, b: Item): number => fn(a) == fn(b) ? (fn2(b) - fn2(a)) : fn(b) - fn(a))
}
/** 分组 */
static groupBy<Item, gT>(list: Item[], fn: (item: Item) => gT): GroupItem<gT, Item>[]
{
let groups = new Array<GroupItem<gT, Item>>()
for (const item of list)
{
let key = fn(item)
let group = groups.find(t => t.key == key)
if (group == null)
{
group = new GroupItem(key)
groups.push(group)
}
group.push(item)
}
return groups
}
/**
* 选择
* let newObjectList = ArrayExt.Select(list,t=>({pA:t.name, pB:t.age + "_"+ t.month}) ) ;
*/
static Select<Item, rT>(list: Item[], fn: (item: Item) => rT): rT[]
{
let newList = new Array<rT>()
for (const t of list)
{
newList.push(fn(t))
}
return newList
}
/** 过来,并按顺序排序 */
static where<Item>(list: Item[], whereFn: (item: Item) => boolean, orderfn1: (item: Item) => number = null, orderfn2: (item: Item) => number = null): Item[]
{
let newList = list.filter(whereFn)
if (orderfn1 == null && orderfn2 == null)
return newList
return ArrayExt.sortBy(newList, orderfn1, orderfn2)
}
}
export class GroupItem<gT, Item>
{
key: gT
get count() { return this.list.length }
list: Item[]
constructor(k: gT)
{
this.key = k
this.list = []
}
push(d: Item)
{
this.list.push(d)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,490 @@
export class DxfWritor
{
layers: WeakSet<Layer>
activeLayer: Layer
lineTypes: WeakSet<LineType>
offX = 0
offY = 0 // 基点
constructor()
{
this.layers = new WeakSet()
this.activeLayer = null
this.lineTypes = new WeakSet()
for (let i = 0; i < DrawingType.LINE_TYPES.length; ++i)
{
this.addLineType(
DrawingType.LINE_TYPES[i].name,
DrawingType.LINE_TYPES[i].description,
DrawingType.LINE_TYPES[i].elements,
)
}
for (let i = 0; i < DrawingType.LAYERS.length; ++i)
{
this.addLayer(
DrawingType.LAYERS[i].name,
DrawingType.LAYERS[i].colorNumber,
DrawingType.LAYERS[i].lineTypeName,
)
}
this.setActiveLayer('0')
}
/**
* @param {string} name
* @param {string} description
* @param {Array} elements - if elem > 0 it is a line, if elem < 0 it is gap, if elem == 0.0 it is a
*/
addLineType(name, description, elements)
{
this.lineTypes[name] = new LineType(name, description, elements)
return this
}
addLayer(name, colorNumber, lineTypeName)
{
this.layers[name] = new Layer(name, colorNumber, lineTypeName)
return this
}
/** 设置当前图层 白=0, 红=1, 黄=2, 绿=3,CYAN=4,BLUE=5,MAGENTA=6 */
setActiveLayer(name)
{
this.activeLayer = this.layers[name]
return this
}
setOffset(x1, y1)
{
this.offX = x1
this.offY = y1
}
clear()
{
}
drawLine(x1, y1, x2, y2)
{
this.activeLayer.addShape(new Line(x1 + this.offX, y1 + this.offY, x2 + this.offX, y2 + this.offY))
return this
}
drawRect(x1, y1, w, h)
{
x1 = x1 + this.offX
y1 = y1 + this.offY
this.activeLayer.addShape(new Line(x1, y1, x1 + w, y1))
this.activeLayer.addShape(new Line(x1 + w, y1, x1 + w, y1 + h))
this.activeLayer.addShape(new Line(x1 + w, y1 + h, x1, y1 + h))
this.activeLayer.addShape(new Line(x1, y1 + h, x1, y1))
return this
}
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} r - radius
* @param {number} startAngle - degree
* @param {number} endAngle - degree
*/
drawArc(x1, y1, r, startAngle, endAngle)
{
this.activeLayer.addShape(new Arc(x1 + this.offX, y1 + this.offY, r, startAngle, endAngle))
return this
}
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} r - radius
*/
drawCircle(x1, y1, r)
{
this.activeLayer.addShape(new Circle(x1 + this.offX, y1 + this.offY, r))
return this
}
/**
* @param {number} x1 - x
* @param {number} y1 - y
* @param {number} height - Text height
* @param {number} rotation - Text rotation
* @param {string} value - the string itself
*/
drawText(x1, y1, height, rotation, value)
{
this.activeLayer.addShape(new Text(x1 + this.offX, y1 + this.offY, height, rotation, value))
return this
}
/**
* @param {Array} points - Array of points like [ [x1, y1,r], [x2, y2,r]... ]
*/
drawPolyline(points, isClose = false)
{
for (const p of points)
{
p.x += this.offX
p.y += this.offY
}
this.activeLayer.addShape(new Polyline(points, isClose))
return this
}
_getDxfLtypeTable()
{
let s = '0\nTABLE\n' // start table
s += '2\nLTYPE\n' // name table as LTYPE table
for (let lineTypeName in this.lineTypes)
{
s += this.lineTypes[lineTypeName].toDxfString()
}
s += '0\nENDTAB\n' // end table
return s
}
_getDxfLayerTable()
{
let s = '0\nTABLE\n' // start table
s += '2\nLAYER\n' // name table as LAYER table
for (let layerName in this.layers)
{
s += this.layers[layerName].toDxfString()
}
s += '0\nENDTAB\n'
return s
}
toDxfString()
{
let s = ''
// start section
s += '0\nSECTION\n'
// name section as TABLES section
s += '2\nTABLES\n'
s += this._getDxfLtypeTable()
s += this._getDxfLayerTable()
// end section
s += '0\nENDSEC\n'
// ENTITES section
s += '0\nSECTION\n'
s += '2\nENTITIES\n'
for (let layerName in this.layers)
{
let layer = this.layers[layerName]
s += layer.shapesToDxf()
// let shapes = layer.getShapes();
}
s += '0\nENDSEC\n'
// close file
s += '0\nEOF'
return s
}
}
namespace DrawingType
{
// AutoCAD Color Index (ACI)
// http://sub-atomic.com/~moses/acadcolors.html
export const ACI = {
LAYER: 0,
RED: 1,
YELLOW: 2,
GREEN: 3,
CYAN: 4,
BLUE: 5,
MAGENTA: 6,
WHITE: 7,
}
export const LINE_TYPES = [
{ name: 'CONTINUOUS', description: '______', elements: [] },
{ name: 'DASHED', description: '_ _ _ ', elements: [5.0, -5.0] },
{ name: 'DOTTED', description: '. . . ', elements: [0.0, -5.0] },
]
export const LAYERS = [
{ name: '0', colorNumber: ACI.WHITE, lineTypeName: 'CONTINUOUS' },
{ name: '1', colorNumber: ACI.RED, lineTypeName: 'CONTINUOUS' },
{ name: '2', colorNumber: ACI.YELLOW, lineTypeName: 'CONTINUOUS' },
{ name: '3', colorNumber: ACI.GREEN, lineTypeName: 'CONTINUOUS' },
{ name: '4', colorNumber: ACI.CYAN, lineTypeName: 'CONTINUOUS' },
{ name: '5', colorNumber: ACI.BLUE, lineTypeName: 'CONTINUOUS' },
{ name: '6', colorNumber: ACI.MAGENTA, lineTypeName: 'CONTINUOUS' },
]
}
export class LineType
{
name: string
description: string
elements: any[]
/**
* @param {string} name
* @param {string} description
* @param {Array} elements - if elem > 0 it is a line, if elem < 0 it is gap, if elem == 0.0 it is a
*/
constructor(name, description, elements)
{
this.name = name
this.description = description
this.elements = elements
}
/**
* @link https://www.autodesk.com/techpubs/autocad/acadr14/dxf/ltype_al_u05_c.htm
*/
toDxfString()
{
let s = '0\nLTYPE\n'
s += '72\n65\n'
s += '70\n64\n'
s += `2\n${this.name}\n`
s += `3\n${this.description}\n`
s += `73\n${this.elements.length}\n`
s += `40\n${this.getElementsSum()}\n`
for (let i = 0; i < this.elements.length; ++i)
{
s += `49\n${this.elements[i]}\n`
}
return s
}
getElementsSum()
{
let sum = 0
for (let i = 0; i < this.elements.length; ++i)
{
sum += Math.abs(this.elements[i])
}
return sum
}
}
export class Layer
{
name: string
colorNumber: string
lineTypeName: string
shapes: any[]
constructor(name, colorNumber, lineTypeName)
{
this.name = name
this.colorNumber = colorNumber
this.lineTypeName = lineTypeName
this.shapes = []
}
toDxfString()
{
let s = '0\nLAYER\n'
s += '70\n64\n'
s += `2\n${this.name}\n`
s += `62\n${this.colorNumber}\n`
s += `6\n${this.lineTypeName}\n`
return s
}
addShape(shape)
{
this.shapes.push(shape)
shape.layer = this
}
getShapes()
{
return this.shapes
}
shapesToDxf()
{
let s = ''
for (let i = 0; i < this.shapes.length; ++i)
{
s += this.shapes[i].toDxfString()
}
return s
}
}
export class Arc
{
x1: number
y1: number
r: number
startAngle: number
endAngle: number
layer: Layer
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} r - radius
* @param {number} startAngle - degree
* @param {number} endAngle - degree
*/
constructor(x1, y1, r, startAngle, endAngle)
{
this.x1 = x1
this.y1 = y1
this.r = r
this.startAngle = startAngle
this.endAngle = endAngle
}
toDxfString()
{
// https://www.autodesk.com/techpubs/autocad/acadr14/dxf/line_al_u05_c.htm
let s = `0\nARC\n`
s += `8\n${this.layer.name}\n`
s += `10\n${this.x1}\n20\n${this.y1}\n30\n0\n`
s += `40\n${this.r}\n50\n${this.startAngle}\n51\n${this.endAngle}\n`
return s
}
}
export class Circle
{
x1: number
y1: number
r: number
layer: Layer
/**
* @param {number} x1 - Center x
* @param {number} y1 - Center y
* @param {number} r - radius
*/
constructor(x1, y1, r)
{
this.x1 = x1
this.y1 = y1
this.r = r
}
toDxfString()
{
// https://www.autodesk.com/techpubs/autocad/acadr14/dxf/circle_al_u05_c.htm
let s = `0\nCIRCLE\n`
s += `8\n${this.layer.name}\n`
s += `10\n${this.x1}\n20\n${this.y1}\n30\n0\n`
s += `40\n${this.r}\n`
return s
}
}
export class Line
{
x1: number
y1: number
x2: number
y2: number
layer: Layer
constructor(x1: number, y1: number, x2: number, y2: number)
{
this.x1 = x1
this.y1 = y1
this.x2 = x2
this.y2 = y2
}
toDxfString()
{
// https://www.autodesk.com/techpubs/autocad/acadr14/dxf/line_al_u05_c.htm
let s = `0\nLINE\n`
s += `8\n${this.layer.name}\n`
s += `10\n${this.x1}\n20\n${this.y1}\n30\n0\n`
s += `11\n${this.x2}\n21\n${this.y2}\n31\n0\n`
return s
}
}
export class Polyline
{
points: any[]
isClose = false
layer: Layer
/**
* @param {Array} points - Array of points like [ [x1, y1,r], [x2, y2,r]... ]
*/
constructor(points, isClose)
{
this.points = points
this.isClose = isClose
}
toDxfString()
{
// https://www.autodesk.com/techpubs/autocad/acad2000/dxf/polyline_dxf_06.htm
// https://www.autodesk.com/techpubs/autocad/acad2000/dxf/vertex_dxf_06.htm
let s = `0\nPOLYLINE\n`
s += `8\n${this.layer.name}\n`
s += `66\n1\n70\n${this.isClose ? 1 : 0}\n`
for (let i = 0; i < this.points.length; ++i)
{
s += `0\nVERTEX\n`
s += `8\n${this.layer.name}\n`
s += `70\n0\n`
s += `10\n${this.points[i].x}\n20\n${this.points[i].y}\n42\n${this.points[i].r}\n`
}
s += `0\nSEQEND\n`
return s
}
}
export class Text
{
x1: number
y1: number
height: number
rotation: number
value: string
layer: Layer
/**
* @param {number} x1 - x
* @param {number} y1 - y
* @param {number} height - Text height
* @param {number} rotation - Text rotation
* @param {string} value - the string itself
*/
constructor(x1, y1, height, rotation, value)
{
this.x1 = x1
this.y1 = y1
this.height = height
this.rotation = rotation
this.value = value
}
toDxfString()
{
// https://www.autodesk.com/techpubs/autocad/acadr14/dxf/text_al_u05_c.htm
let s = `0\nTEXT\n`
s += `8\n${this.layer.name}\n`
s += `1\n${this.value}\n`
s += `10\n${this.x1}\n20\n${this.y1}\n30\n0\n`
s += `40\n${this.height}\n50\n${this.rotation}\n`
return s
}
}

View File

@@ -0,0 +1,122 @@
export class textFile
{
/**保存单文件 text类型的 */
static saveFile(filename: string, content: string)
{
let blob = new Blob([content], { type: "text/plain;charset=utf-8" });
this.saveAs(filename, blob);
}
/**读取文件 选择文件组件,读取文档处理函数 */
static readFile(eleFile, fn_doText, fileType = "", fn_msg = null)
{
let noEleFile = !(eleFile);
if (noEleFile)
{
eleFile = document.createElement('input');
eleFile.type = 'file';
eleFile.accept = 'text/*';
eleFile.hidden = true;
document.body.appendChild(eleFile);
}
if (fileType && fileType != "") eleFile.accept = fileType;
let reader = new FileReader();
reader.onload = function (event)
{
let text = event.target["result"];
if (fn_doText) fn_doText(text);
};
// 选择文件
eleFile.onchange = function (event)
{
let file = event.target.files[0];
if (file)
{
reader.readAsText(file);
}
if (fn_msg != null)
{
fn_msg();
}
};
eleFile.click();
if (noEleFile)
{
document.body.removeChild(eleFile);
}
}
static getStringFromFile(eleFile, fileType = "", fn_msg = null): string
{
let noEleFile = !(eleFile);
if (noEleFile)
{
eleFile = document.createElement('input');
eleFile.type = 'file';
eleFile.accept = 'text/*';
eleFile.hidden = true;
document.body.appendChild(eleFile);
}
if (fileType && fileType != "") eleFile.accept = fileType;
let reader = new FileReader();
reader.onload = function (event)
{
let text = event.target["result"];
return text;
};
// 选择文件
eleFile.onchange = function (event)
{
let file = event.target.files[0];
if (file)
{
reader.readAsText(file);
}
if (fn_msg != null)
{
fn_msg();
}
};
eleFile.click();
if (noEleFile)
{
document.body.removeChild(eleFile);
}
return '';
}
/**保存文件 */
static saveAs(fileName: string, data: Blob)
{
// 创建隐藏的可下载链接
let eleLink = document.createElement('a');
eleLink.download = fileName;
eleLink.style.display = 'none';
eleLink.href = URL.createObjectURL(data);
// 触发点击
document.body.appendChild(eleLink);
eleLink.click();
// 然后移除
document.body.removeChild(eleLink);
}
static readFile2()
{
let rf = document.createElement('input');
rf.type = 'file';
rf.accept = 'text/*';
rf.hidden = true;
document.body.removeChild(rf);
}
}

View File

@@ -0,0 +1,118 @@
/** 文本表示的数字取值范围,比如 : "2,3,5-8" 表示
* () => 大于等于 小于等于
* [] => 大于 小于
*/
export class MaskedNumberRange {
maskedNumbers: any[]
constructor(strRs: string) {
let rs = []
let strs = strRs.split(',')
for (let str of strs) {
if (str.trim() == '')
continue
let n = Number(str)
let s_flag = 'in'
let e_flag = 'in'
if (!Number.isNaN(n)) {
rs.push({ s: n, e: n, s_flag, e_flag })
}
else {
let zys = str.split('-')
if (zys.length != 2)
continue
if (zys[0].trim() == '' || zys[1].trim() == '')
continue
let start :any = zys[0]
let end :any = zys[1]
if (zys[0].trim().includes('(')) {
s_flag = 'notIn'
start = start.replaceAll('(', '')
} else if (zys[0].trim().includes('[')) {
s_flag = 'in'
start = start.replaceAll('[', '')
}
if (start == 'x') {
s_flag = 'infinite'
}else{
start = Number(start)
}
if (zys[1].trim().includes(')')) {
e_flag = 'notIn'
end = end.replaceAll(')', '')
} else if (zys[1].trim().includes(']')) {
e_flag = 'in'
end = end.replaceAll(']', '')
}
if (end == 'x') {
e_flag = 'infinite'
}else{
end = Number(end)
}
let s = start // Number(zys[0])
let e = end //Number(zys[1])
rs.push({ s: s, e: e, s_flag, e_flag })
}
}
this.maskedNumbers = rs
}
/** 参数是否在给定范围内 */
isInRange(r: number): boolean {
let res = false
for (let se of this.maskedNumbers) {
if (se.s_flag == 'infinite') {
// 无穷 不判断
res = true
} else if (se.s_flag == 'notIn') {
if (r > se.s) {
res = true
}
} else if (se.s_flag == 'in') {
if (r >= se.s) {
res = true
}
}
if (res == true) {
let res1 = false
if (se.e_flag == 'infinite') {
res1 = true
} else if (se.e_flag == 'notIn') {
if (r < se.e) {
res1 = true
}
} else if (se.e_flag == 'in') {
if (r <= se.e) {
res1 = true
}
}
res = res && res1
}
if(res == true){
return true
}
// if (r > se.s && r < se.e)
// return true
}
return res
}
}

View File

@@ -0,0 +1,128 @@
/** 相等, 相差小于diff */
export function equal(a: number, b: number, diff = 0.001): boolean
{
return Math.abs(a - b) < diff
}
/** ab相差dis */
export function isDiffer(a: number, b: number, dis: number, diff = 0.001): boolean
{
let rdis = Math.abs(a - b)
return rdis > dis - diff && rdis < dis + diff
}
/** 两点间距离 */
export function getDis2(p1, p2): number
{
let x1 = p1.pointX || p1.X || p1.x || 0
let y1 = p1.pointY || p1.Y || p1.y || 0
let x2 = p2.pointX || p2.X || p2.x || 0
let y2 = p2.pointY || p2.Y || p2.y || 0
return getDis(x1, y1, x2, y2)
}
/** 两点间距离 */
export function getDis(x1: number, y1: number, x2: number, y2: number): number
{
let xt = x1 - x2
let yt = y1 - y2
return Math.sqrt(xt * xt + yt * yt)
}
/** 两点间弧度半径 */
export function getR(x1: number, y1: number, x2: number, y2: number, bul: number): number
{
if (bul == 0)
return 0
let d = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) / 2
return 0.5 * d * (1 + bul ** 2) / bul
}
/** 获取两点间的半径 */
export function getR2(p1, p2): number
{
let bul = p1.Curve || p1.bul || 0
if (bul == 0)
return 0
let x1 = p1.pointX || p1.X || p1.x
let y1 = p1.pointY || p1.Y || p1.y
let x2 = p2.pointX || p2.X || p2.x
let y2 = p2.pointY || p2.Y || p2.y
return getR(x1, y1, x2, y2, bul)
}
/** 截取小数点 */
export function round(n: number, bit = 3): number
{
if (bit < 1)
return n
let tt = 10 ** bit
return Math.round(n * tt) / tt
}
/**
* 获得四点p{x,y}形成的矩形{x,y,w,l};
* 不是矩形 返回null
*/
export function getRect(p0, p1, p2, p3): any
{
if (equal(p0.x, p1.x) == false && equal(p0.y, p1.y) == false)
return null
// 中心点
let xc = (p0.x + p1.x + p2.x + p3.x) / 4
let yc = (p0.y + p1.y + p2.y + p3.y) / 4
let dis0 = getDis(p0.x, p0.y, xc, yc)
let dis1 = getDis(p1.x, p1.y, xc, yc)
let dis2 = getDis(p2.x, p2.y, xc, yc)
let dis3 = getDis(p3.x, p3.y, xc, yc)
if (equal(dis0, dis1) && equal(dis0, dis2) && equal(dis0, dis3))
{
let x = Math.min(p0.x, p1.x, p2.x, p3.x)
let y = Math.min(p0.y, p1.y, p2.y, p3.y)
let w = Math.max(p0.x, p1.x, p2.x, p3.x) - x
let l = Math.max(p0.y, p1.y, p2.y, p3.y) - y
if (w < 0.01 || l < 0.01)
return null
return { x, y, w, l }
}
return null
}
/** 获取两点的直线的一般式方程AX+BY+C=0 ABC */
export function getLineABC(x1: number, y1: number, x2: number, y2: number)
{
let A = y2 - y1
let B = x1 - x2
let C = x2 * y1 - x1 * y2
return { A, B, C }
}
/** 获取点 p(x,y) 到两点的线的距离 */
export function getDis_PointLine(x1: number, y1: number, x2: number, y2: number, x: number, y: number)
{
let abc = getLineABC(x1, y1, x2, y2)
let aabb = Math.sqrt(abc.A * abc.A + abc.B * abc.B)
if (aabb == 0)
return 0
return Math.abs(abc.A * x + abc.B * y + abc.C) / aabb
}
/** 删除重复点 */
export function removeRepeatPoint(pts, fuzz = 0.01)
{
for (let n = pts.length - 1; n >= 0; n--) {
let p = n - 1
if (p < 0)
p = pts.length - 1
if (getDis2(pts[p], pts[n]) < fuzz)
pts.splice(n, 1)
}
}

View File

@@ -0,0 +1,170 @@
import { PlaceStyle,EdgeType } from '../../confClass.js'
// import { EdgeType } from '../../vo/enums/EdgeType.js'
/**
* 排版方式,返回排版后的[下右上左]所对应原始边 rt = [] 下0 右1 上2 左3
* 比如 rt[0] = 3 表示现在的下边是原始的左边
* rt[2] = 0 表示现在的上边是原始的下边
* @param placeStyle 放置方式
*/
export function getOriginalSides(placeStyle: PlaceStyle): number[]
{
// let orgSides = [0, 1, 2, 3];
let orgSides = [EdgeType.BOTTOM, EdgeType.RIGHT, EdgeType.TOP, EdgeType.LEFT]
switch (placeStyle)
{
case PlaceStyle.FRONT: // 正面
break
case PlaceStyle.FRONT_TURN_RIGHT: // 正面右转
orgSides[EdgeType.BOTTOM] = EdgeType.RIGHT
orgSides[EdgeType.RIGHT] = EdgeType.TOP
orgSides[EdgeType.TOP] = EdgeType.LEFT
orgSides[EdgeType.LEFT] = EdgeType.BOTTOM
break
case PlaceStyle.FRONT_TURN_BACK: // 正面后转
orgSides[EdgeType.BOTTOM] = EdgeType.TOP
orgSides[EdgeType.RIGHT] = EdgeType.LEFT
orgSides[EdgeType.TOP] = EdgeType.BOTTOM
orgSides[EdgeType.LEFT] = EdgeType.RIGHT
break
case PlaceStyle.FRONT_TURN_LEFT: // 正面左转
orgSides[EdgeType.BOTTOM] = EdgeType.LEFT
orgSides[EdgeType.RIGHT] = EdgeType.BOTTOM
orgSides[EdgeType.TOP] = EdgeType.RIGHT
orgSides[EdgeType.LEFT] = EdgeType.TOP
break
case PlaceStyle.BACK: // 反面
orgSides[EdgeType.BOTTOM] = EdgeType.BOTTOM
orgSides[EdgeType.RIGHT] = EdgeType.LEFT
orgSides[EdgeType.TOP] = EdgeType.TOP
orgSides[EdgeType.LEFT] = EdgeType.RIGHT
break
case PlaceStyle.BACK_TURN_RIGHT: // 反面右转
orgSides[EdgeType.BOTTOM] = EdgeType.LEFT
orgSides[EdgeType.RIGHT] = EdgeType.TOP
orgSides[EdgeType.TOP] = EdgeType.RIGHT
orgSides[EdgeType.LEFT] = EdgeType.BOTTOM
break
case PlaceStyle.BACK_TURN_BACK: // 反面后转
orgSides[EdgeType.BOTTOM] = EdgeType.TOP
orgSides[EdgeType.RIGHT] = EdgeType.RIGHT
orgSides[EdgeType.TOP] = EdgeType.BOTTOM
orgSides[EdgeType.LEFT] = EdgeType.LEFT
break
case PlaceStyle.BACK_TURN_LEFT: // 反面左转
orgSides[EdgeType.BOTTOM] = EdgeType.RIGHT
orgSides[EdgeType.RIGHT] = EdgeType.BOTTOM
orgSides[EdgeType.TOP] = EdgeType.LEFT
orgSides[EdgeType.LEFT] = EdgeType.TOP
break
default:
break
}
return orgSides
}
/**
* 返回原始边[下右上左] 放置后位置 rt = [,,,,]
* 例如 rt[0] = 3 表示原始下边,现在放置后的新位置是左边
* rt[2] = 0 表示原始上边,现在放置后的新位置是下边
* @param placeStyle 放置方式
*/
export function getPlacedSides(placeStyle: PlaceStyle): number[]
{
// let orgSides = [0, 1, 2, 3];
let orgSides = [EdgeType.BOTTOM, EdgeType.RIGHT, EdgeType.TOP, EdgeType.LEFT]
switch (placeStyle)
{
case PlaceStyle.FRONT: // 正面
break
case PlaceStyle.FRONT_TURN_RIGHT: // 正面右转
orgSides[EdgeType.BOTTOM] = EdgeType.LEFT
orgSides[EdgeType.RIGHT] = EdgeType.BOTTOM
orgSides[EdgeType.TOP] = EdgeType.RIGHT
orgSides[EdgeType.LEFT] = EdgeType.TOP
break
case PlaceStyle.FRONT_TURN_BACK: // 正面后转
orgSides[EdgeType.BOTTOM] = EdgeType.TOP
orgSides[EdgeType.RIGHT] = EdgeType.LEFT
orgSides[EdgeType.TOP] = EdgeType.BOTTOM
orgSides[EdgeType.LEFT] = EdgeType.RIGHT
break
case PlaceStyle.FRONT_TURN_LEFT: // 正面左转
orgSides[EdgeType.BOTTOM] = EdgeType.RIGHT
orgSides[EdgeType.RIGHT] = EdgeType.TOP
orgSides[EdgeType.TOP] = EdgeType.LEFT
orgSides[EdgeType.LEFT] = EdgeType.BOTTOM
break
case PlaceStyle.BACK: // 反面
orgSides[EdgeType.BOTTOM] = EdgeType.BOTTOM
orgSides[EdgeType.RIGHT] = EdgeType.LEFT
orgSides[EdgeType.TOP] = EdgeType.TOP
orgSides[EdgeType.LEFT] = EdgeType.RIGHT
break
case PlaceStyle.BACK_TURN_RIGHT: // 反面右转
orgSides[EdgeType.BOTTOM] = EdgeType.LEFT
orgSides[EdgeType.RIGHT] = EdgeType.TOP
orgSides[EdgeType.TOP] = EdgeType.RIGHT
orgSides[EdgeType.LEFT] = EdgeType.BOTTOM
break
case PlaceStyle.BACK_TURN_BACK: // 反面后转
orgSides[EdgeType.BOTTOM] = EdgeType.TOP
orgSides[EdgeType.RIGHT] = EdgeType.RIGHT
orgSides[EdgeType.TOP] = EdgeType.BOTTOM
orgSides[EdgeType.LEFT] = EdgeType.LEFT
break
case PlaceStyle.BACK_TURN_LEFT: // 反面左转
orgSides[EdgeType.BOTTOM] = EdgeType.RIGHT
orgSides[EdgeType.RIGHT] = EdgeType.BOTTOM
orgSides[EdgeType.TOP] = EdgeType.LEFT
orgSides[EdgeType.LEFT] = EdgeType.TOP
break
default:
break
}
return orgSides
}
/** 获取排版位置 */
export function getPlacePosition(x: number, y: number, width: number, length: number, placeStyle: PlaceStyle): any
{
let posX = x
let posY = y
switch (placeStyle)
{
case PlaceStyle.FRONT: // 正面
break
case PlaceStyle.FRONT_TURN_RIGHT: // 正面右转
posX = y
posY = width - x
break
case PlaceStyle.FRONT_TURN_BACK: // 正面后转
posX = width - x
posY = length - y
break
case PlaceStyle.FRONT_TURN_LEFT: // 正面左转
posX = length - y
posY = x
break
case PlaceStyle.BACK: // 反面
posX = width - x
posY = y
break
case PlaceStyle.BACK_TURN_RIGHT: // 反面右转
posX = y
posY = x
break
case PlaceStyle.BACK_TURN_BACK: // 反面后转
posX = x
posY = length - y
break
case PlaceStyle.BACK_TURN_LEFT: // 反面左转
posX = length - y
posY = width - x
break
default:
break
}
return { x: posX, y: posY }
}

View File

@@ -0,0 +1,152 @@
import gb2312 from './gb2312.json'
export class StringBase64 {
static ToBase64_gb2312(str: string): string {
let bin = this.toGB2312Bytes(str || '')
let str64 = this.encode(bin)
return str64
}
static _table = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']
/** 将二进制转换成base64 */
static encode(bin: number[]): string {
let codes = []
let un = 0
un = bin.length % 3
if (un == 1)
bin.push(0, 0)
else if (un == 2)
bin.push(0)
for (let i = 2; i < bin.length; i += 3) {
let c = bin[i - 2] << 16
c |= bin[i - 1] << 8
c |= bin[i]
codes.push(this._table[c >> 18 & 0x3F])
codes.push(this._table[c >> 12 & 0x3F])
codes.push(this._table[c >> 6 & 0x3F])
codes.push(this._table[c & 0x3F])
}
if (un >= 1) {
codes[codes.length - 1] = '='
bin.pop()
}
if (un == 1) {
codes[codes.length - 2] = '='
bin.pop()
}
return codes.join('')
}
/** 将utf8 转成gb2312字符串 */
static toGb2312String(str1: string): string {
let substr = ''
let a = ''
let b = ''
let c = ''
let i = -1
i = str1.indexOf('%')
if (i == -1) {
return str1
}
while (i != -1) {
if (i < 3) {
substr = substr + str1.substr(0, i - 1)
str1 = str1.substr(i + 1, str1.length - i)
a = str1.substr(0, 2)
str1 = str1.substr(2, str1.length - 2)
if ((Number.parseInt(`0x${a}`) & 0x80) == 0) {
substr = substr + String.fromCharCode(Number.parseInt(`0x${a}`))
}
else if ((Number.parseInt(`0x${a}`) & 0xE0) == 0xC0) { // two byte
b = str1.substr(1, 2)
str1 = str1.substr(3, str1.length - 3)
let widechar = (Number.parseInt(`0x${a}`) & 0x1F) << 6
widechar = widechar | (Number.parseInt(`0x${b}`) & 0x3F)
substr = substr + String.fromCharCode(widechar)
}
else {
b = str1.substr(1, 2)
str1 = str1.substr(3, str1.length - 3)
c = str1.substr(1, 2)
str1 = str1.substr(3, str1.length - 3)
let widechar = (Number.parseInt(`0x${a}`) & 0x0F) << 12
widechar = widechar | ((Number.parseInt(`0x${b}`) & 0x3F) << 6)
widechar = widechar | (Number.parseInt(`0x${c}`) & 0x3F)
substr = substr + String.fromCharCode(widechar)
}
}
else {
substr = substr + str1.substring(0, i)
str1 = str1.substring(i)
}
i = str1.indexOf('%')
}
return substr + str1
}
private static _unicode2gb
static getUnicode2gb() {
if (this._unicode2gb == null) {
this._unicode2gb = gb2312
}
return this._unicode2gb
}
static toGB2312Bytes(str: string): number[] {
let unicode2gb = this.getUnicode2gb()
let res = []; let len = str.length
for (let i = 0; i < len; i++) {
let code = str.charCodeAt(i)
if (code <= 0x007F) {
res.push(code)
}
else {
let hex = unicode2gb[`0x${code.toString(16).toUpperCase()}`]
let gb = Number(hex)
if (Number.isNaN(gb))
gb = Number('0xA1F5')
let arr = []
while (gb > 0) {
arr.push(gb & 0xFF)
gb >>= 8
}
while (arr.length > 0) res.push(arr.pop())
}
}
return res
}
static fromGB2312Bytes(gb2312Bytes: number[]): string {
let unicode2gb = this.getUnicode2gb()
let res = []
// var i = 0
for (let i = 0; i < gb2312Bytes.length; i++) {
let code = gb2312Bytes[i]
if (code < 0xA1 || code > 0xFE || i + 1 == gb2312Bytes.length) {
res.push(String.fromCharCode(code))
continue
}
let c2 = gb2312Bytes[i + 1]
if (code < 0xA1 || code > 0xFE) {
res.push(String.fromCharCode(code))
continue
}
let g = c2 | code << 8
c2 = Number(unicode2gb[`0x${g.toString(16).toUpperCase()}`])
if (typeof c2 == 'undefined') {
res.push(String.fromCharCode(code))
continue
}
res.push(String.fromCharCode(c2))
i++
}
return res.join('')
}
}

View File

@@ -0,0 +1,82 @@
export class StringBuider
{
strings: string[]
constructor()
{
this.strings = []
}
/** 插入 */
Append(obj: any)
{
this.strings.push(obj)
}
/** 插入文本 */
AppendString(str: string) { this.strings.push(str) }
/** 格式化 插入 */
AppendFormat(str: string, ...arg: any[]): string
{
for (let i = 0; i < arg.length; i++)
{
let parent = `\\{${i}\\}`
let reg = new RegExp(parent, 'g')
str = str.replace(reg, arg[i])
}
this.strings.push(str)
return str
}
/** 添加一行(回车) */
AppendLine(str = '')
{
if (str != null)
{
this.AppendString(str)
}
}
/** 插入 */
Insert(index: number, ...strs)
{
if (strs.length == 0)
return
if (index > this.strings.length)
index = this.strings.length
if (index < 0)
index = 0
this.strings.splice(index, 0, ...strs)
}
/** 删除片段 */
Remove(startIndex: number, length: number): string[]
{
return this.strings.splice(startIndex, length)
}
/** 所有文本 */
ToString(): string
{
return this.strings.join('\r\n')
}
/** 清空 */
Clear() { this.strings = [] }
/** 容量 */
get size() { return this.strings.length }
/** 文本数 */
get Count() { return this.strings.length }
GetStringLength(): number
{
let length = 0
for (const str of this.strings)
{
length += str.length
}
return length
}
}

View File

@@ -0,0 +1,126 @@
export class StringFormat
{
/**
* 对Date的扩展将 Date 转化为指定格式的String
* 月(M)、日(d)、12小时(h)、24小时(H)、分(m)、秒(s)、周(E)、季度(q) 可以用 1-2 个占位符
* 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* eg:
* (new Date()).pattern("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
* (new Date()).pattern("yyyy-MM-dd E HH:mm:ss") ==> 2009-03-10 二 20:09:04
* (new Date()).pattern("yyyy-MM-dd EE hh:mm:ss") ==> 2009-03-10 周二 08:09:04
* (new Date()).pattern("yyyy-MM-dd EEE hh:mm:ss") ==> 2009-03-10 星期二 08:09:04
* (new Date()).pattern("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
*/
static Date(date: Date, fmt): string
{
let o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours() % 12 == 0 ? 12 : date.getHours() % 12, // 小时
'H+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S': date.getMilliseconds(), // 毫秒
}
let week = {
0: '/u65e5',
1: '/u4e00',
2: '/u4e8c',
3: '/u4e09',
4: '/u56db',
5: '/u4e94',
6: '/u516d',
}
if (/(y+)/.test(fmt))
{
fmt = fmt.replace(RegExp.$1, (`${date.getFullYear()}`).substr(4 - RegExp.$1.length))
}
if (/(E+)/.test(fmt))
{
fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length > 1) ? (RegExp.$1.length > 2 ? '/u661f/u671f' : '/u5468') : '') + week[`${date.getDay()}`])
}
for (let k in o)
{
if (new RegExp(`(${k})`).test(fmt))
{
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : ((`00${o[k]}`).substr((`${o[k]}`).length)))
}
}
return fmt
}
/** 返回数字的固定小数点 123.00 */
static number(value: number, bit: number): string
{
return value?.toFixed(bit).toString()
}
static toFixed(value: number, bit: number = 3): number
{
let tt = 10 ** bit
return Math.round(value * tt) / tt
}
static filterIllegalChar(str: string): string
{
// var pattern = new RegExp("[/:*?'<>|\\]");
// var rs = "";
// for (var i = 0; i < str.length; i++)
// {
// rs = rs + str.substr(i, 1).replace(pattern, '');
// }
// return rs;
let rt = str.replace(/\\/g, '').replace(/\:/g, '').replace(/\*/g, '').replace(/\?/g, '').replace(/\"/g, '').replace(/\</g, '').replace(/\>/g, '').replace(/\|/g, '').replace(/\'/g, '')
return rt
}
/** 文本格式化 */
static format(str: string, ...args: any[]): string
{
let data = args
let tmpl = str
for (const item of tmpl.matchAll(/\{(.+?)\}/g))
{
let parts = item[1].split(',').map(i => i.trim())
let index = Number(parts[0])
let arg = data[index]
let val = (arg || '').toString() // 默认
if (arg instanceof Date) // 日期
{
let fm = 'MM-dd HH;mm'
if (parts.length > 1)
{
fm = parts[1]
}
val = this.Date(arg, fm)
}
if (parts.length > 1 && parts[1][0] === '#')
{
// {2,#3} -> 数字 转成3位 001,...023,
val = val.padStart(Number(parts[1].substring(1)), '0')
}
tmpl = tmpl.replace(item[0], val)
}
return tmpl
}
/** 实现右对齐,右边填充 (25,4,'*') => '25**' */
static PadEnd(num: number, totalWidth: number, paddingChar: string): string
{
let str = num.toString()
return str.padEnd(totalWidth, paddingChar[0])
}
/** 实现右对齐,左边填充 (25,4,'0') => '0025' */
static PadStart(num: number, totalWidth: number, paddingChar: string): string
{
let str = num.toString()
return str.padStart(totalWidth, paddingChar[0])
}
}

View File

@@ -0,0 +1,119 @@
export class textFile
{
/** 保存单文件 text类型的 */
static saveFile(filename: string, content: string)
{
let blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
this.saveAs(filename, blob)
}
/** 读取文件 选择文件组件,读取文档处理函数 */
static readFile(eleFile, fn_doText, fileType = '', fn_msg = null)
{
let noEleFile = !(eleFile)
if (noEleFile)
{
eleFile = document.createElement('input')
eleFile.type = 'file'
eleFile.accept = 'text/*'
eleFile.hidden = true
document.body.appendChild(eleFile)
}
if (fileType && fileType != '')
eleFile.accept = fileType
let reader = new FileReader()
reader.onload = function (event)
{
let text = event.target.result
if (fn_doText)
fn_doText(text)
}
// 选择文件
eleFile.onchange = function (event)
{
let file = event.target.files[0]
if (file)
{
reader.readAsText(file)
}
if (fn_msg != null)
{
fn_msg()
}
}
eleFile.click()
if (noEleFile)
{
document.body.removeChild(eleFile)
}
}
static getStringFromFile(eleFile, fileType = '', fn_msg = null): string
{
let noEleFile = !(eleFile)
if (noEleFile)
{
eleFile = document.createElement('input')
eleFile.type = 'file'
eleFile.accept = 'text/*'
eleFile.hidden = true
document.body.appendChild(eleFile)
}
if (fileType && fileType != '')
eleFile.accept = fileType
let reader = new FileReader()
reader.onload = function (event)
{
let text = event.target.result
return text
}
// 选择文件
eleFile.onchange = function (event)
{
let file = event.target.files[0]
if (file)
{
reader.readAsText(file)
}
if (fn_msg != null)
{
fn_msg()
}
}
eleFile.click()
if (noEleFile)
{
document.body.removeChild(eleFile)
}
return ''
}
/** 保存文件 */
static saveAs(fileName: string, data: Blob)
{
// 创建隐藏的可下载链接
let eleLink = document.createElement('a')
eleLink.download = fileName
eleLink.style.display = 'none'
eleLink.href = URL.createObjectURL(data)
// 触发点击
document.body.appendChild(eleLink)
eleLink.click()
// 然后移除
document.body.removeChild(eleLink)
}
static readFile2()
{
let rf = document.createElement('input')
rf.type = 'file'
rf.accept = 'text/*'
rf.hidden = true
document.body.removeChild(rf)
}
}

View File

@@ -0,0 +1,138 @@
import { saveAs } from 'file-saver'
import type { ZipProvider } from '../zip.js'
import { DefaultZipProvider, FileInfo } from '../zip.js'
import { StringBuider } from './StringBuidler.js'
// import { textFile } from './File';
import { StringBase64 } from './StringBase64.js'
export class FileZip
{
zipName: string
// zip对象
private zip: ZipProvider = DefaultZipProvider()
// 文件数据
private stringBuider: StringBuider
constructor()
{
this.stringBuider = new StringBuider()
this.SetZipFileName('')
}
/** 设置zip文件名 */
SetZipFileName(_name: string)
{
this.zipName = _name
}
getZipFileName()
{
return this.zipName
}
PushBlobFile(fileName: string, content)
{
let newFile = new FileInfo(fileName, content, false)
newFile.binary = true
this.zip.addFile(newFile)
}
// /**添加文件 */
// PushFile(fileName: string, fileText: string, isBase64 = false, isgb2312 = false, isUtf8Bom=false)
// {
// if (isgb2312)
// {
// isBase64 = true;
// fileText = StringBase64.ToBase64_gb2312(fileText);
// }
// if(isUtf8Bom)
// {
// fileText = String.fromCharCode(parseInt("0xFEFF")) + fileText;
// }
// let newFile = new FileInfo(fileName, fileText, isBase64);
// this.zip.addFile(newFile);
// }
/** 添加文件 fileName文件名 fileText文件内容 isBase64是否base64 encoding编码格式(gb2312, utf8bom) */
PushFile(fileName: string, fileText: string, isBase64 = false, encoding: string = 'gb2312')
{
if (encoding == 'gb2312')
{
isBase64 = true
fileText = StringBase64.ToBase64_gb2312(fileText)
}
if (encoding == 'utf8bom')
{
fileText = String.fromCharCode(Number.parseInt('0xFEFF')) + fileText
}
let newFile = new FileInfo(fileName, fileText, isBase64)
this.zip.addFile(newFile)
}
pushFile_withBom(fileName: string, fileText: string)
{
this.PushFile(fileName, fileText, false, 'utf8bom')
}
PushFile_GB2312(fileName: string, fileText: string)
{
this.PushFile(fileName, fileText, false, 'gb2312')
}
/** 新建文件 */
NewFile()
{
this.stringBuider.Clear()
}
/** 推送文本到缓存区 */
AppentText(comment: string)
{
this.stringBuider.Append(comment)
}
// //将缓存区的文本 保存起来
// SaveFile(fileName: string, isgb2312: boolean = false, isutf8bom = false)
// {
// let fileText = this.stringBuider.ToString();
// this.stringBuider.Clear();
// this.PushFile(fileName, fileText, false, isgb2312, isutf8bom);
// }
// 将缓存区的文本 保存起来
SaveFile(fileName: string, encoding: string = 'gb2312')
{
let fileText = this.stringBuider.ToString()
this.stringBuider.Clear()
this.PushFile(fileName, fileText, false, encoding)
}
// 下载zip文件
async Download(zipName = '')
{
let content = await this.zip.saveAsync()
// textFile.saveAs(FileZip.zipName,content);
let name = zipName || this.zipName
saveAs(content, name)
}
static WriteFile(fname: string, data: BlobPart)
{
let blob = new Blob([data], { type: 'octet/stream' })
let download = document.createElement('a')
download.download = fname
download.href = window.URL.createObjectURL(blob)
download.style.display = 'none'
download.onclick = function ()
{
document.body.removeChild(download)
}
document.body.appendChild(download)
download.click()
};
}
export { FileInfo }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
const decoder = new TextDecoder()
export class BmpDecoder {
pos: number
buffer: ArrayBuffer
is_with_alpha: boolean
bottom_up: boolean
flag: string
constructor(buffer: ArrayBuffer, is_with_alpha: boolean) {
this.pos = 0
this.buffer = buffer
this.is_with_alpha = !!is_with_alpha
this.bottom_up = true
this.flag = decoder.decode(this.buffer.slice(0, (this.pos += 2)))
if (this.flag != 'BM')
throw new Error('Invalid BMP File')
// this.parseHeader();
// this.parseRGBA();
}
parseHeader() {}
parseRGBA() {}
}
export function createBmpFile(
imgData: ImageData,
paramDic: { [key: string]: string } = {},
) {
let dpi = 300
let bitPP = paramDic.bpp ? Number(paramDic.bpp) : 24
let printResolution = Math.round(dpi * 39.3701)
if ([1, 24, 32].includes(bitPP) == false) {
throw new Error(`不支持的${bitPP}bpp BMP`)
}
let data = imgData.data
let width = imgData.width
let height = imgData.height
let extraBytes = width % 4
let rgbSize = data.length
switch (bitPP) {
case 1:
{
let rowSize = width / 8 + (width % 8 > 0 ? 1 : 0)
extraBytes = rowSize % 4
rgbSize = height * (rowSize + extraBytes)
}
break
case 24:
{
let rowSize = 3 * width
extraBytes = rowSize % 4
rgbSize = height * (rowSize + extraBytes)
}
break
}
let headerInfoSize = 40
/** ****************header */
let flag = 'BM'
let reserved = 0
let offset = 54
let fileSize = rgbSize + offset
let planes = 1
let compress = 0
let hr = printResolution
let vr = printResolution
let colors = 0
let importantColors = 0
let colorPaletteSize = 0
if (bitPP == 1) {
colorPaletteSize = 2 * 4
}
let buffer = new ArrayBuffer(offset + colorPaletteSize + rgbSize)
let tempBuffer = new DataView(buffer)
let pos = 0
// BMP Header
// ID
tempBuffer.setUint8(pos, flag.charCodeAt(0))
pos += 1
tempBuffer.setUint8(pos, flag.charCodeAt(1))
pos += 1
// size
tempBuffer.setUint32(pos, fileSize, true)
pos += 4
// Application specific
tempBuffer.setUint32(pos, reserved, true)
pos += 4
// Offset
tempBuffer.setUint32(pos, offset, true)
pos += 4
// DIB Header
// Number of bytes in the DIB header (from this point)
tempBuffer.setUint32(pos, headerInfoSize, true)
pos += 4
tempBuffer.setUint32(pos, width, true)
pos += 4
tempBuffer.setUint32(pos, height, true)
pos += 4
tempBuffer.setUint16(pos, planes, true)
pos += 2
tempBuffer.setUint16(pos, bitPP, true)
pos += 2
tempBuffer.setUint16(pos, compress, true)
pos += 4
tempBuffer.setUint16(pos, rgbSize, true)
pos += 4
tempBuffer.setUint16(pos, hr, true)
pos += 4
tempBuffer.setUint16(pos, vr, true)
pos += 4
tempBuffer.setUint16(pos, colors, true)
pos += 4
tempBuffer.setUint16(pos, importantColors, true)
pos += 4
if (bitPP == 1) {
tempBuffer.setUint8(pos++, 0) // b
tempBuffer.setUint8(pos++, 0) // g
tempBuffer.setUint8(pos++, 0) // r
tempBuffer.setUint8(pos++, 0) // a
tempBuffer.setUint8(pos++, 255) // b
tempBuffer.setUint8(pos++, 255) // g
tempBuffer.setUint8(pos++, 255) // r
tempBuffer.setUint8(pos++, 0) // a
}
let pixData = ''
let writeBinData = () => {
tempBuffer.setUint8(pos++, Number.parseInt(pixData.padEnd(8, '0'), 2))
pixData = ''
}
for (let y = height - 1; y >= 0; y--) {
let rowIndex = y * width * 4
for (let x = 0; x < width; x++) {
let p = rowIndex + x * 4
if (bitPP == 1) {
if (pixData.length == 8) {
writeBinData()
}
let rgb = [data[p + 0], data[p + 1], data[p + 2]]
pixData += rgb.filter((v, i) => v > 170).length == 3 ? '1' : '0' // 170 以下小板缩略图条会断
} else {
tempBuffer.setUint8(pos++, data[p + 2]) // b
tempBuffer.setUint8(pos++, data[p + 1]) // g
tempBuffer.setUint8(pos++, data[p + 0]) // r
if (bitPP == 32) {
tempBuffer.setUint8(pos++, data[p + 3]) // a
}
}
}
if (bitPP == 1 && pixData.length > 0) {
writeBinData()
}
for (let index = 0; index < extraBytes; index++) {
tempBuffer.setUint8(pos++, 0)
}
}
return tempBuffer.buffer
}

View File

@@ -0,0 +1,399 @@
import { ClipType, PolyFillType } from 'js-angusj-clipper/web'
import type { Paths } from 'js-angusj-clipper/web'
import type { SubjectInput } from 'js-angusj-clipper/web/clipFunctions'
import { Box2 } from '../Box2'
import { clipperCpp } from '../ClipperCpp'
import type { CompareVectorFn } from '../ComparePoint'
import { ComparePoint } from '../ComparePoint'
import { ConvexHull2D } from '../ConvexHull2D'
import type { NestFiler } from '../Filer'
import type { Point } from '../Vector2'
import { equaln } from '../Util'
import { Vector2 } from '../Vector2'
import { PlaceType } from '../../confClass' //'../../vo/enums/PlaceType'
import { NestCache } from './NestCache'
import type { Part } from './Part'
import { PartGroup } from './Part'
import type { Path } from './Path'
import { Area, TranslatePath } from './Path'
/**
* 当零件尝试放置后容器的状态,用于尝试放置,并且尝试贪心
*/
interface PartPlacedContainerState
{
p: Point
area?: number
hull?: Point[]
box?: Box2
}
/**
* 排料零件的容器,用来放置零件
* 它是一块大板
* 也可以是一个网洞
* 也可以是余料
*/
export class Container
{
ParentId: number = -1// 容器bin来源 -1表示默认的bin,大于等于0表示来自板件网洞
ChildrenIndex: number = 0// 网洞的索引位置
ParentM: Point// 在旋转网洞时,和非旋转网洞时表现不一样. (网洞相对于父零件的位置,如果是使用旋转网洞则表示网洞的最下角位置)
PlacedParts: Part[] = []
// 放置状态
PlacedArea = 0
PlacedBox: Box2
PlacedHull: Point[]
StatusKey: string
CompartePoint: CompareVectorFn
constructor(protected BinPath?: Path, private _PlaceType = PlaceType.Box, compare = 'xy')
{
if (BinPath)
this.StatusKey = `${this.BinPath.Id.toString()},${this._PlaceType}`
this.CompartePoint = ComparePoint(compare)
}
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.PlacedParts.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 = Number.POSITIVE_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)) && this.CompartePoint(p, translate) === -1))
{
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)) && this.CompartePoint(p, translate) === -1))
{
translate = p
minArea = area
bestHull = nhull
}
break
}
case PlaceType.Gravity:
{
if (!translate || this.CompartePoint(p, translate) === -1)
translate = p
break
}
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.PlacedParts)
{
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] 是否计算当前的容器状态?
*/
private AppendPart(part: Part, calc = true): void
{
this.StatusKey += `,${part.State.Contour.Id}`
this.PlacedParts.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.concat(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
for (let path of nfp)
{
for (let p of path)
{
if (!leftP || this.CompartePoint(p, leftP) === -1)
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.PlacedParts = []
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.PlacedParts.push(part)
}
if (!this.PlacedBox)
this.PlacedBox = new Box2()
this.PlacedBox.min.fromArray(file.Read())
this.PlacedBox.max.fromArray(file.Read())
return this
}
// 对象将自身数据写入到文件.
WriteFile(file: NestFiler)
{
file.Write(this.ParentId)
file.Write(this.ChildrenIndex)
file.Write(this.ParentM)
let parts: Part[] = []
for (let p of this.PlacedParts)
{
if (p instanceof PartGroup)
parts.push(...p.Export())
else
parts.push(p)
}
file.Write(parts.length)
for (let p of parts)
{
file.Write(p.Id)
file.Write(p.StateIndex)
file.Write(p.PlacePosition)
}
if (!this.PlacedBox)
this.PlacedBox = new Box2()
file.Write(this.PlacedBox.min.toArray())
file.Write(this.PlacedBox.max.toArray())
}
// #endregion
}

View File

@@ -0,0 +1,5 @@
export const GNestConfig = {
RotateHole: true, // 在旋转零件的时候旋转网洞
UsePartGroup: false, // 如果开启这个特性,将在第一次放置零件时,尝试计算完全对插的板件,并且使用它.(基因注册,模范夫妻)
UseOffsetSimplify: true,
}

View File

@@ -0,0 +1,274 @@
import { arrayLast, arrayRemoveOnce } from '../ArrayExt'
import type { NestFiler } from '../Filer'
import { RandomIndex } from '../Random'
import type { PlaceType } from '../../vo/enums/PlaceType'
import { Container } from './Container'
import { GNestConfig } from './GNestConfig'
import { DefaultComparePointKeys } from './NestDatabase'
import type { Part } from './Part'
import type { Path } from './Path'
/**
* 个体(表示某一次优化的结果,或者还没开始优化的状态)
* 个体是由一堆零件组成的,零件可以有不同的状态。
*
* 个体单独变异
* 可以是某个零件的旋转状态发生改变
* 可以是零件的顺序发生改变
*
* 个体交配(感觉暂时不需要)
* 个体和其他个体进行基因交换
*/
export class Individual {
constructor(public Parts?: Part[], public mutationRate = 0.5, private bin?: Path, private OddmentsBins: Path[] = [], private ComparePointKeys: string[] = DefaultComparePointKeys) { }
Fitness: number // (评估健康) 大板个数-1 + 最后一块板的利用率
Containers: Container[] // 大板列表(已排
HoleContainers: Container[]// 网洞列表
OddmentsContainers: Container[]// 余料列表
/**
* 评估健康程度
*/
Evaluate(bestCount: number, greedy = false, type?: PlaceType) {
if (GNestConfig.RotateHole)
return this.EvaluateOfUseRotateHole(bestCount, greedy, type)
this.Containers = []
this.HoleContainers = []
// 初始化余料列表
this.OddmentsContainers = this.OddmentsBins.map(path => new Container(path, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)]))
this.InitHoleContainers()
// 余料优先
let parts = this.Parts.filter(p => !this.OddmentsContainers.some(odd => odd.PutPart(p)))
// 网洞优先
parts = parts.filter(p => !this.HoleContainers.some(hole => hole.PutPart(p)))
while (parts.length > 0) {
if (this.Containers.length > bestCount) {
this.Fitness = Math.ceil(bestCount) + 1
return
}
const container = new Container(this.bin, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)])
if (this.mutationRate > 0.5) // 大板优先,可以提高收敛速度
{
const maxP = parts.reduce((preP, curP) => preP.State.Contour.Area > curP.State.Contour.Area ? preP : curP)
const PutGroupF = (): boolean => {
// 基因注册
const nextPartIndex = parts.findIndex(p => p !== maxP)
if (nextPartIndex !== -1) {
const nextPart = parts[nextPartIndex]
const groups = maxP.ParseGroup(nextPart, this.bin)
for (const group of groups) {
if (container.PutPart(group)) {
parts.splice(nextPartIndex, 1)
arrayRemoveOnce(parts, maxP)
return true
}
}
}
return false
}
if (GNestConfig.UsePartGroup && PutGroupF()) { }
else if (container.PutPart(maxP, greedy)) { arrayRemoveOnce(parts, maxP) }
}
parts = parts.filter(p => !container.PutPart(p, greedy))
if (!greedy)// 如果没有贪心,排完后在贪心一下
parts = parts.filter(p => !container.PutPart(p, true))
this.Containers.push(container)
}
this.Fitness = this.Containers.length - 1 + arrayLast(this.Containers)?.UseRatio ?? 0
}
/**
* 在网洞利用时,保持纹路正确
*
* @param bestCount
* @param greedy
* @param type
*/
private EvaluateOfUseRotateHole(bestCount: number, greedy = false, type?: PlaceType) {
// 初始化余料列表
this.OddmentsContainers = this.OddmentsBins.map(path => new Container(path, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)]))
this.Containers = []
this.HoleContainers = []
let parts = this.Parts.concat()
while (parts.length > 0) {
if (this.Containers.length > bestCount)// 提前结束,已经超过最佳用板量
{
this.Fitness = Math.ceil(bestCount) + 1
return
}
const container = new Container(this.bin, type ?? RandomIndex(3), this.ComparePointKeys[RandomIndex(2)])
const holes: Container[] = []
const PutPart = (part: Part, greedy: boolean): boolean => {
// 先放置在网洞内
const isOk
= this.OddmentsContainers.some(oc => oc.PutPart(part, greedy)) // 余料
|| holes.some((h) => {
const isOk = h.PutPart(part, greedy)
return isOk
}) // 网洞
|| container.PutPart(part, greedy) // 大板
if (isOk)
this.AppendHoles(part, holes)
return isOk
}
if (this.mutationRate > 0.5) // 大板优先,可以提高收敛速度
{
const maxP = parts.reduce((preP, curP) => preP.State.Contour.Area > curP.State.Contour.Area ? preP : curP)
const PutGroupF = (): boolean => {
// 基因注册
const nextPartIndex = parts.findIndex(p => p !== maxP)
if (nextPartIndex !== -1) {
const nextPart = parts[nextPartIndex]
const groups = maxP.ParseGroup(nextPart, this.bin)
for (const group of groups) {
if (PutPart(group, greedy)) {
parts.splice(nextPartIndex, 1)
arrayRemoveOnce(parts, maxP)
return true
}
}
}
return false
}
if (GNestConfig.UsePartGroup && PutGroupF()) { }
else if (PutPart(maxP, greedy)) { arrayRemoveOnce(parts, maxP) }
}
parts = parts.filter(p => !PutPart(p, greedy))
if (!greedy)// 如果没有贪心,排完后在贪心一下
parts = parts.filter(p => !PutPart(p, true))
if (container.PlacedParts.length)
this.Containers.push(container)
}
this.Fitness = this.Containers.length - 1 + (arrayLast(this.Containers)?.UseRatio ?? 0)
}
private AppendHoles(part: Part<any, any>, holes: Container[]) {
for (let i = 0; i < part.Holes.length; i++) {
const hole = part.Holes[i]
const container = new Container(hole.Contour)
container.ParentId = part.Id
container.ChildrenIndex = i
container.ParentM = GNestConfig.RotateHole ? hole.MinPoint : hole.OrigionMinPoint
this.HoleContainers.push(container)
holes.push(container)
}
}
private InitHoleContainers() {
if (GNestConfig.RotateHole)
return
for (const part of this.Parts) {
for (let i = 0; i < part.Holes.length; i++) {
const hole = part.Holes[i]
const container = new Container(hole.Contour)
container.ParentId = part.Id
container.ChildrenIndex = i
container.ParentM = GNestConfig.RotateHole ? hole.MinPoint : hole.OrigionMinPoint
this.HoleContainers.push(container)
}
}
}
Clone() {
const p = new Individual(this.Parts.map(p => p.Clone()), this.mutationRate, this.bin, this.OddmentsBins, this.ComparePointKeys)
p.Mutate()
return p
}
/**
* 突变
*/
Mutate() {
if (this.mutationRate > 0.5) {
for (let i = 0; i < this.Parts.length; i++) {
const rand = Math.random()
if (rand < this.mutationRate * 0.5) {
// 和下一个调换顺序
const 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 {
// 洗牌
const rand = Math.random()
if (rand < 0.5) {
const index = RandomIndex(this.Parts.length - 2)
const count = Math.ceil(RandomIndex(this.Parts.length - 2 - index) * this.mutationRate * 2) || 1
const parts = this.Parts.splice(index, count)
this.Parts.push(...parts)
this.Parts[index].Mutate()
}
else {
for (let i = 0; i < this.Parts.length; i++) {
const rand = Math.random()
if (rand < this.mutationRate) {
this.Parts[i].Mutate()
i += 5
}
}
}
}
if (this.mutationRate > 0.2)
this.mutationRate -= 0.004
return this
}
// #region -------------------------File-------------------------
// 对象从文件中读取数据,初始化自身
ReadFile(file: NestFiler) {
const ver = file.Read()// ver
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))
count = file.Read()
this.OddmentsContainers = []
for (let i = 0; i < count; i++)
this.OddmentsContainers.push(new Container().ReadFile(file, this.Parts))
}
// 对象将自身数据写入到文件.
WriteFile(f: NestFiler) {
f.Write(1)// ver
f.Write(this.Fitness)
f.Write(this.Containers.length)
for (const c of this.Containers)
c.WriteFile(f)
f.Write(this.HoleContainers.length)
for (const c of this.HoleContainers)
c.WriteFile(f)
f.Write(this.OddmentsContainers.length)
for (const c of this.OddmentsContainers)
c.WriteFile(f)
}
// #endregion
}

View File

@@ -0,0 +1,37 @@
import type { Point } from '../Vector2'
import { Path } from './Path'
export class NestCache
{
static count1 = 0
static count2 = 0// noset
static PositionCache: { [key: string]: Point } = {}
static NoPutCache: { [key: string]: Set<number> } = {}
private static CacheRect = new Map<string, Path>()
/**
* 用于创建原点在0点的矩形路径
*/
static CreatePath(x: number, y: number, knifRadius = 3.5): Path
{
let minX = -knifRadius
let maxX = x + knifRadius
let minY = -knifRadius
let maxY = y + knifRadius
return new Path([
{ x: minX, y: minY },
{ x: maxX, y: minY },
{ x: maxX, y: maxY },
{ x: minX, y: maxY },
])
}
static Clear()
{
this.count1 = 0
this.count2 = 0
this.CacheRect.clear()
this.PositionCache = {}
}
}

View File

@@ -0,0 +1,82 @@
import { NestFiler } from '../Filer'
import { Part } from './Part'
import { Path } from './Path'
import { PathGeneratorSingle } from './PathGenerator'
export const DefaultComparePointKeys = ['xy', 'yx']
/**
* 排料数据库,用这个类来序列化需要排料的数据
* 用于在Work间传输
*/
export class NestDatabase {
Bin: Path // 默认的容器
OddmentsBins: Path[]// 余料容器列表
Paths: Path[] // 所有的Path都在这里
Parts: Part[] // 所有的零件
ComparePointKeys: string[] = DefaultComparePointKeys// 用来决定零件靠边模式
// #region -------------------------File-------------------------
// 对象从文件中读取数据,初始化自身
ReadFile(file: NestFiler) {
const ver = file.Read()
let count = file.Read() as number
this.Paths = []
for (let i = 0; i < count; i++) {
const 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++) {
const part = new Part()
part.ReadFile(file)
this.Parts.push(part)
}
count = file.Read()
this.OddmentsBins = []
for (let i = 0; i < count; i++) {
const path = new Path()
path.ReadFile(file)
this.OddmentsBins.push(path)
}
if (ver > 1)
this.ComparePointKeys = file.Read()
return this
}
// 对象将自身数据写入到文件.
WriteFile(file: NestFiler) {
file.Write(2)
file.Write(this.Paths.length)
for (const path of this.Paths)
path.WriteFile(file)
file.Write(this.Bin.Id)
file.Write(this.Parts.length)
for (const part of this.Parts)
part.WriteFile(file)
if (!this.OddmentsBins)
this.OddmentsBins = []
file.Write(this.OddmentsBins.length)
for (const path of this.OddmentsBins)
path.WriteFile(file)
file.Write(this.ComparePointKeys)
return this
}
// #endregion
get File() {
const f = new NestFiler()
this.WriteFile(f)
return f
}
}

View File

@@ -0,0 +1,163 @@
import { arrayRemoveIf } from '../ArrayExt'
import { clipperCpp } from '../ClipperCpp'
import { Sleep } from '../Sleep'
import { Individual } from './Individual'
import { NestCache } from './NestCache'
import { DefaultComparePointKeys } from './NestDatabase'
import type { Part } from './Part'
import type { Path } from './Path'
/**
* 优化器
* 配置优化器
* 放入零件
* 按下启动
* 按下暂停
* 清理机台
*/
export class OptimizeMachine {
// 配置
Config: {
PopulationCount: number// 种群个数
}
Bin: Path // 默认的容器
OddmentsBins: Path[]// 余料容器列表
Parts: Part[] // 所有的零件
ComparePointKeys: string[] = DefaultComparePointKeys// 用来决定零件靠边模式
// //计算重复的零件 TODO:需要对相同的零件提取出来
// PartCount: number[] = [];
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
// //计算重复的零件(暂时不用)
// for (let part of parts)
// {
// let count = this.PartCount[part.Id];
// this.PartCount[part.Id] = count === undefined ? 1 : (count + 1);
// }
}
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, this.Bin, this.OddmentsBins, this.ComparePointKeys)]
for (let i = 1; i < this.Config.PopulationCount; i++) {
const 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, this.Bin, this.OddmentsBins, this.ComparePointKeys))
}
// 2.执行
await this.Run()
}
calcCount = 0
best = Number.POSITIVE_INFINITY
bestP: Individual
bestCount = 0
private async Run() {
console.time('1')
if (this.Parts.length === 0)
return
// 开始自然选择
while (!this._IsSuspend) // 实验停止信号
{
const goBack = this.calcCount - this.bestCount > 8000
// 1.适应环境(放置零件)
for (let i = 0; i < this._Individuals.length; i++) {
if (globalThis.document || !clipperCpp.lib)
await Sleep(0)
const p = this._Individuals[i]
if (this.calcCount < 1000 || this.calcCount % 1000 === 0)
p.Evaluate(this.best, true, i % 3)
else
p.Evaluate(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) {
await Sleep(0)
}
this._Individuals.sort((i1, i2) => i1.Fitness - i2.Fitness)
const 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 (const p of this._Individuals) {
if (p.Fitness !== undefined)
p.Mutate()
}
}
}
// 暂停
Suspend() {
console.timeEnd('1')
console.log(this.best, this.calcCount, NestCache.count1)
this._IsSuspend = true
console.log('暂停')
}
// 继续
Continue() {
this._IsSuspend = false
this.Run()
}
// 清理机台
Clear() {
}
}

View File

@@ -0,0 +1,238 @@
import { ClipType, EndType, JoinType, PolyFillType } from 'js-angusj-clipper/web'
import type { ClipInput } from 'js-angusj-clipper/web'
import { Box2 } from '..//Box2'
import { clipperCpp } from '../ClipperCpp'
import type { Container } from './Container'
import { NestCache } from './NestCache'
import { Path, PathScale, TranslatePath, TranslatePath_Self } from './Path'
const SquarePath = NestCache.CreatePath(60, 60, 0)
const CanPutPaths = [
NestCache.CreatePath(200, 200, 0),
NestCache.CreatePath(600, 100, 0),
NestCache.CreatePath(100, 600, 0),
]
/**
* 分析排料结果的余料
* @param container 排料结果的容器
* @param binPath 容器的bin
* @param [knifeRadius] 刀半径(以便我们再次偏移)
* @param squarePath 使用一个正方形路径来简化余料轮廓
* @param canPutPaths 使用可以放置的路径列表来测试余料是否可用,如果可用,则保留
* @returns Path[] 轮廓的位置存储在OrigionMinPoint中
*/
export function ParseOddments(container: Container, binPath: Path, knifeRadius: number = 3.5, squarePath: Path = SquarePath, canPutPaths: Path[] = CanPutPaths): Path[]
{
// 构建轮廓数据
let partPaths: ClipInput[] = container.PlacedParts.map((part) =>
{
// 直接在这里偏移,而不缓存,应该没有性能问题
let newPts = clipperCpp.lib.offsetToPaths({
delta: knifeRadius * 1e4,
offsetInputs: [{ data: part.State.Contour.BigIntPoints, joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
})[0]
let path = TranslatePath(newPts, { x: part.PlacePosition.x - 5e3, y: part.PlacePosition.y - 5e3 })// 因为移动了0.5,0.5,所以这里也要移动0.5
return { data: path }
})
// console.log('构建轮廓数据', partPaths)
// 所有的余料(使用布尔差集)
let oddmentsPolygon = clipperCpp.lib.clipToPolyTree({
subjectInputs: [{ data: binPath.BigIntPoints, closed: true }],
clipInputs: partPaths,
clipType: ClipType.Difference,
subjectFillType: PolyFillType.NonZero,
})
// 现在我们用树状结构,应该不会自交了?(文档写了,返回的结果不可能重叠或者自交)
// 简化结果,避免自交
// oddmentsPolygon = clipperCpp.lib.simplifyPolygons(oddmentsPolygon);
function CreatePolygon(minx: number, miny: number, maxx: number, maxy: number)
{
return [
{ x: minx, y: miny },
{ x: maxx, y: miny },
{ x: maxx, y: maxy },
{ x: minx, y: maxy },
]
}
let clipedPaths: Path[] = []// 已经减去网洞投影的余料轮廓列表
// 由于手动排版可能造成余料网洞,我们将网洞的盒子投影,然后裁剪余料,避免余料有网洞
for (let node of oddmentsPolygon.childs)
{
let nodePolygon = node.contour
// 减去网洞
if (node.childs.length)
{
let box = new Box2().setFromPoints(nodePolygon)
let childBoxPolygon = node.childs.map((cnode) =>
{
let cbox = new Box2().setFromPoints(cnode.contour)
let type = 0// 0左1右2上3下
let minDist = Number.POSITIVE_INFINITY
let letftDist = cbox.min.x - box.min.x
let rightDist = box.max.x - cbox.max.x
let topDist = box.max.y - cbox.max.y
let downDist = cbox.min.y - box.min.y
if (rightDist < letftDist)
{
type = 1
minDist = rightDist
}
if (topDist < minDist)
{
type = 2
minDist = topDist
}
if (downDist < minDist)
type = 3
if (type === 0)
return CreatePolygon(box.min.x, cbox.min.y, cbox.max.x, cbox.max.y)
if (type === 1)
return CreatePolygon(cbox.min.x, cbox.min.y, box.max.x, cbox.max.y)
if (type === 2)
return CreatePolygon(cbox.min.x, cbox.min.y, cbox.max.x, box.max.y)
if (type === 3)
return CreatePolygon(cbox.min.x, box.min.y, cbox.max.x, cbox.max.y)
})
let splits = clipperCpp.lib.clipToPaths({
subjectInputs: [{ data: nodePolygon, closed: true }],
clipInputs: childBoxPolygon.map((polygon) => { return { data: polygon } }),
clipType: ClipType.Difference,
subjectFillType: PolyFillType.NonZero,
})
for (let p of splits)
clipedPaths.push(new Path(PathScale(p, 1e-4)))
}
else
clipedPaths.push(new Path(node.contour.map((p) => { return { x: p.x * 1e-4, y: p.y * 1e-4 } })))
}
let OddmentsPaths: Path[] = []
for (let polygonPath of clipedPaths)
{
// 先获取内部的nfp
let insideNFPS = polygonPath.GetInsideNFP(squarePath)
if (!insideNFPS)
continue
let beferPolygons: ClipInput[] = []
for (let nfp of insideNFPS)
{
let nfpPath = new Path(PathScale(nfp, 1e-4))
// 通过内部nfp还原实际轮廓
let sumPolygons = clipperCpp.lib.minkowskiSumPath(nfpPath.BigIntPoints, squarePath.BigIntPoints, true)
sumPolygons = clipperCpp.lib.simplifyPolygons(sumPolygons)
for (let poly of sumPolygons)
{
if (clipperCpp.lib.area(poly) < 0)
continue// 移除内部的,无意义的
let tempPath = new Path(poly.map((p) => { return { x: p.x * 1e-4, y: p.y * 1e-4 } }))// 这里new一个新的,下面就复用这个
if (canPutPaths.some(p => tempPath.GetInsideNFP(p)?.length))// 能塞的下指定的轮廓才会被留下
{
if (beferPolygons.length)
{
// 移动到实际位置
TranslatePath_Self(poly, (polygonPath.OrigionMinPoint.x + nfpPath.OrigionMinPoint.x) * 1e4, (polygonPath.OrigionMinPoint.y + nfpPath.OrigionMinPoint.y) * 1e4)
// 在这里裁剪之前的余料轮廓
let tree = clipperCpp.lib.clipToPolyTree({
subjectInputs: [{ data: poly, closed: true }],
clipInputs: beferPolygons,
clipType: ClipType.Difference,
subjectFillType: PolyFillType.NonZero,
})
for (let node of tree.childs)
{
if (node.childs.length)
continue
tempPath = new Path(node.contour.map((p) => { return { x: p.x * 1e-4, y: p.y * 1e-4 } }))
// 继续简化
tempPath = SimplifyPathOfSqPath(tempPath, squarePath)
if (!tempPath)
continue
OddmentsPaths.push(tempPath)
// 偏移2把刀
let offsetedPolygon = clipperCpp.lib.offsetToPaths({
delta: knifeRadius * 2e4,
offsetInputs: [{ data: node.contour, joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
})[0]
beferPolygons.push({ data: offsetedPolygon })// 用于裁剪后续的余料
}
}
else
{
// 设置轮廓的位置
tempPath.OrigionMinPoint.x += nfpPath.OrigionMinPoint.x + polygonPath.OrigionMinPoint.x
tempPath.OrigionMinPoint.y += nfpPath.OrigionMinPoint.y + polygonPath.OrigionMinPoint.y
OddmentsPaths.push(tempPath)
// 将余料轮廓加入到裁剪轮廓中,用于裁剪后续的余料
if (insideNFPS.length)
{
// 移动到实际位置
TranslatePath_Self(poly, (polygonPath.OrigionMinPoint.x + nfpPath.OrigionMinPoint.x) * 1e4, (polygonPath.OrigionMinPoint.y + nfpPath.OrigionMinPoint.y) * 1e4)
// 偏移2把刀
let offsetedPolygon = clipperCpp.lib.offsetToPaths({
delta: knifeRadius * 2e4,
offsetInputs: [{ data: poly, joinType: JoinType.Miter, endType: EndType.ClosedPolygon }],
})[0]
beferPolygons.push({ data: offsetedPolygon })// 用于裁剪后续的余料
}
}
}
}
}
}
// console.log('ParseOddments end', OddmentsPaths)
return OddmentsPaths
}
// 使用矩形轮廓来简化余料轮廓(通常一进一出)
function SimplifyPathOfSqPath(polygonPath: Path, sqPath: Path): Path | undefined
{
// 先获取内部的nfp
let insideNFPS = polygonPath.GetInsideNFP(sqPath)
if (insideNFPS.length > 0)// 目前一般只有1个,不知道会不会有多个
{
let nfp = insideNFPS[0]
let nfpPath = new Path(PathScale(nfp, 1e-4))
// 通过内部nfp还原实际轮廓
let sumPolygons = clipperCpp.lib.minkowskiSumPath(nfpPath.BigIntPoints, sqPath.BigIntPoints, true)
sumPolygons = clipperCpp.lib.simplifyPolygons(sumPolygons)
for (let poly of sumPolygons)// 通常是一个内部的+一个外部的
{
if (clipperCpp.lib.area(poly) < 0)
continue// 移除内部的,无意义的
let tempPath = new Path(PathScale(poly, 1e-4))
tempPath.OrigionMinPoint.x += nfpPath.OrigionMinPoint.x + polygonPath.OrigionMinPoint.x
tempPath.OrigionMinPoint.y += nfpPath.OrigionMinPoint.y + polygonPath.OrigionMinPoint.y
return tempPath
}
}
}

View File

@@ -0,0 +1,394 @@
import type { Box2 } from '../Box2'
import type { NestFiler } from '../Filer'
import type { Point } from '../Vector2'
import { RandomIndex } from '../Random'
import { FixIndex } from '../Util'
import { Vector2 } from '../Vector2'
import { GNestConfig } from './GNestConfig'
import { NestCache } from './NestCache'
import { PartState } from './PartState'
import { Path } from './Path'
import { PathGeneratorSingle } from './PathGenerator'
const EmptyArray = []
/**
* 零件类
* 零件类可以绑定数据,也存在位置和旋转状态的信息
*
* 初始化零件:
* 传入零件的轮廓,刀半径,包围容器(或者为空?)
* 初始化用于放置的轮廓。将轮廓首点移动到0点记录移动的点P。
*
* 零件放置位置:
* 表示零件轮廓首点的位置。
*
* 零件的旋转:
* 表示零件轮廓按照首点0旋转。
*
* 还原零件的放置状态:
* 同样将零件移动到0点
* 同样将零件旋转
* 同样将零件移动到指定的位置
* 零件可能处于容器中,变换到容器坐标系
*
*/
export class Part<T = any, Matrix = any>
{
Id: number// 用于确定Part的唯一性,并且有助于在其他Work中还原
private _Holes: PartState[] = []
_RotateHoles: PartState[][] = []
// 零件的不同旋转状态,这个数组会在所有的状态间共享,以便快速切换状态
StateIndex = 0 // 旋转状态
RotatedStates: PartState[] = []// 可能的旋转状态列表
PlacePosition: Point // 放置位置(相对于容器的位置)
HolePosition: Point//
// #region 临时数据(不会被序列化到优化线程)
UserData: T// 只用于还原零件的显示状态(或者关联到实际数据)
Parent: Part// 如果这个零件放置在了网洞中,这个Parent表示这个网洞所属的零件,我们可以得到这个零件的放置信息并且可以从Container中的ParentM得到网洞相对于零件的位置
PlaceCS: Matrix// 放置矩阵 Matrix4
PlaceIndex: number// 放置的大板索引
// #endregion
GroupMap: { [key: number]: Part[] } = {}
get State(): PartState // 零件当前的状态
{
return this.RotatedStates[this.StateIndex]
}
get Holes(): PartState[]
{
if (GNestConfig.RotateHole)
return this._RotateHoles[this.StateIndex] || EmptyArray
return this._Holes
}
// 初始化零件的各个状态,按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
partState.MinPoint = path.OrigionMinPoint
}
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)
// 计算正确的最小点
let tempPath = new Path(path.OrigionPoints, pa)
partState.MinPoint = tempPath.OrigionMinPoint
}
// 记录已有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
}
ParseGroup(partOther: Part, bin: Path): Part[]
{
let arr = this.GroupMap[partOther.Id]
if (arr)
return arr
arr = []
if (this.Holes.length || partOther.Holes.length)
return arr
this.GroupMap[partOther.Id] = arr
if (this.State.Contour.IsRect || partOther.State.Contour.IsRect)
return arr
if (this.State.Contour.Area > this.State.Contour.BoundingBox.area * 0.9)
return arr
if (partOther.State.Contour.Area > partOther.State.Contour.BoundingBox.area * 0.9)
return arr
for (let i = 0; i < this.RotatedStates.length; i++)
{
let s1 = this.RotatedStates[i]
for (let j = 1; j < partOther.RotatedStates.length; j++)
{
let s2 = partOther.RotatedStates[j]
let nfps = s1.Contour.GetOutsideNFP(s2.Contour)
for (let nfp of nfps)
{
for (let k = 0; k < nfp.length * 2; k++)
{
let p: Point
if (k % 2 === 0)
{
p = { ...nfp[k / 2] }
}
else
{
let p1 = nfp[FixIndex(k / 2 - 0.5, nfp)]
let p2 = nfp[FixIndex(k / 2 + 0.5, nfp)]
p = { x: p1.x + p2.x, y: p1.y + p2.y }
p.x *= 0.5
p.y *= 0.5
}
p.x *= 1e-4
p.y *= 1e-4
let newBox = s2.Contour.BoundingBox.clone().translate(p)
newBox.union(s1.Contour.BoundingBox)
if (newBox.area < (s1.Contour.Area + s2.Contour.Area) * 1.3)
{
let partGroup = new PartGroup(this, partOther, i, j, p, newBox, bin)
if (partGroup.RotatedStates.length > 0
&& !arr.some(p => p.State.Contour === partGroup.State.Contour)// 类似的
)
{
arr.push(partGroup)
return arr
}
}
}
}
}
}
return arr
}
// 添加网洞
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)
if (GNestConfig.RotateHole)
for (let i = 0; i < this.RotatedStates.length; i++)
{
let r = this.RotatedStates[i].Rotation
let arr = this._RotateHoles[i]
if (!arr)
{
arr = []
this._RotateHoles[i] = arr
}
if (r === 0)
{
hole.MinPoint = path.OrigionMinPoint
arr.push(hole)
}
else
{
let newPath = new Path(path.Points, r)
let newHole = new PartState()
newHole.Rotation = r
newHole.Contour = PathGeneratorSingle.Allocate(newPath)
PathGeneratorSingle.RegisterId(newHole.Contour)
newHole.OrigionMinPoint = newPath.OrigionMinPoint
// 计算正确的最小点
let tempPath = new Path(path.OrigionPoints, r)
newHole.MinPoint = tempPath.OrigionMinPoint
arr.push(newHole)
}
}
}
// TODO:因为现在实现的是左右翻转,所以会出现角度匹配不完全的问题(缺失上下翻转)
Mirror(doubleFace: boolean)
{
let states = this.RotatedStates
let holes = this._Holes
let roholes = this._RotateHoles
if (!doubleFace)
{
this.RotatedStates = []
this._Holes = []
this._RotateHoles = []
}
let count = states.length
for (let i = 0; i < count; i++)
{
let s = states[i]
let ns = s.Mirror()
if (ns && !this.RotatedStates.some(state => state.Contour === s.Contour))
{
this.RotatedStates.push(ns)
if (this._Holes.length > 0)
this._Holes.push(holes[i].Mirror())
if (this._RotateHoles.length > 0)
this._RotateHoles.push(roholes[i].map(s => s.Mirror()))
}
}
}
// 浅克隆
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
part._RotateHoles = this._RotateHoles
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)
}
// 旋转网洞
count = file.Read()
this._RotateHoles = []
for (let i = 0; i < count; i++)
{
let count2 = file.Read() as number
let holes: PartState[] = []
for (let j = 0; j < count2; j++)
{
let state = new PartState()
state.ReadFile(file)
holes.push(state)
}
this._RotateHoles.push(holes)
}
}
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)
// 写入旋转网洞
file.Write(this._RotateHoles.length)
for (let holes of this._RotateHoles)
{
file.Write(holes.length)
for (let hole of holes)
{
hole.WriteFile(file)
}
}
}
// #endregion
}
// 零件组合
export class PartGroup extends Part
{
constructor(public part1: Part,
public part2: Part,
public index1: number,
public index2: number,
public p: Point,
public box: Box2,
bin: Path,
)
{
super()
let size = box.getSize(new Vector2())
this.Init2(NestCache.CreatePath(size.x, size.y, 0), bin, [0])
}
Export(): Part[]
{
this.part1.StateIndex = this.index1
this.part2.StateIndex = this.index2
this.part1.PlacePosition = {
x: this.PlacePosition.x - this.box.min.x * 1e4,
y: this.PlacePosition.y - this.box.min.y * 1e4,
}
this.part2.PlacePosition = {
x: this.PlacePosition.x - this.box.min.x * 1e4 + this.p.x * 1e4,
y: this.PlacePosition.y - this.box.min.y * 1e4 + this.p.y * 1e4,
}
return [this.part1, this.part2]
}
}

View File

@@ -0,0 +1,61 @@
import type { Point } from '../Vector2'
import type { NestFiler } from '../Filer'
import { Path } from './Path'
import { PathGeneratorSingle } from './PathGenerator'
/**
* 用于存放零件旋转后的状态
* 记录了用于放置时的轮廓。该轮廓总是首点等于0便于放置时的计算。
*/
export class PartState
{
Rotation: number
OrigionMinPoint: Point// 使用 Rotation(O - OrigionMinPoint) 将零件变换到和排料矩形状态相同的状态,然后使用PlacePoint(O)将零件设置到正确的状态
MinPoint: Point// 这个状态下的最小点
Contour: Path// 轮廓
IsMirror: boolean = false
MirrorOriginMinPoint: Point
Mirror(): PartState
{
if (this.Contour.IsRect)
return
let mpts = this.Contour.Points.map((p) => { return { x: -p.x, y: p.y } }).reverse()
let path = new Path(mpts, 0)
let partState = new PartState()
partState.Contour = PathGeneratorSingle.Allocate(path)
PathGeneratorSingle.RegisterId(partState.Contour)
partState.Rotation = this.Rotation
partState.OrigionMinPoint = this.OrigionMinPoint
partState.MinPoint = this.MinPoint
partState.IsMirror = true
partState.MirrorOriginMinPoint = path.OrigionMinPoint
return partState
}
// #region -------------------------File-------------------------
ReadFile(file: NestFiler)
{
this.Rotation = file.Read()
this.OrigionMinPoint = file.Read()
this.MinPoint = file.Read()
let index = file.Read() as number
this.Contour = PathGeneratorSingle.paths[index]
if (!this.Contour)
console.error('无法得到PartState的轮廓!')
}
WriteFile(file: NestFiler)
{
file.Write(this.Rotation)
file.Write(this.OrigionMinPoint)
file.Write(this.MinPoint)
file.Write(this.Contour.Id)
}
// #endregion
}

Some files were not shown because too many files have changed in this diff Show More