提交:1、导XML格式的CNC 2、NCwriter功能填充
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mes-processors-libs-*.tgz
|
||||||
|
node_modules
|
||||||
|
dist/*
|
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# @mes-processors/libs
|
||||||
|
|
||||||
|
这是一个用于处理MES(制造执行系统)相关工作流的处理器类库。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
在内网环境下执行以下脚本进行安装
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm add http://gitea.cf/MES-FE/mes-packages/releases/download/0.1/mes-processors-libs-0.1.0.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> 在安装库之前,请确认以下信息:
|
||||||
|
>
|
||||||
|
> - 该库发布于内网gitea仓库的release中,所以你需要提前在gitea中进行登录,并确保你有该仓库的访问权限。
|
||||||
|
> - 库的版本需要手动进行控制,注意上述链接中的版本信息,在安装前需要主动修改版本号,请前往<http://gitea.cf/MES-FE/mes-packages/releases>来确认最新版本。
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
该库提供了MES/iMES公用的处理器,并已配置为导出项,请参考以下Typescript代码进行使用:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 引入矩形优化处理器
|
||||||
|
import { RectLayoutProcConfig } from 'cut-abstractions';
|
||||||
|
import { RectLayoutProc } from '@mes-processors/libs';
|
||||||
|
|
||||||
|
// 实例化处理器
|
||||||
|
const proc = new RectLayoutProc();
|
||||||
|
// 构建上下文f proc.exec>[0] = {
|
||||||
|
input: testObj,
|
||||||
|
params: new RectLayoutProcConfig()
|
||||||
|
};
|
||||||
|
// 异步执行
|
||||||
|
const ctx: Parameters<typeo
|
||||||
|
await proc.exec(ctx);
|
||||||
|
|
||||||
|
// 从上下文对象中获取输出内容
|
||||||
|
console.log("RESULT: ", ctx.output);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Q&A
|
||||||
|
|
||||||
|
### 运行某些处理器时出现404(Not Found)错误
|
||||||
|
|
||||||
|
**错误描述**
|
||||||
|
当执行处理器时,出现类似下面的错误:
|
||||||
|
|
||||||
|
```log
|
||||||
|
GET http://localhost:5173/node_modules/.vite/deps/assets/RectOptimizeMachine.worker-BO2fmpVH.js 404 (Not Found)
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**
|
||||||
|
该库中某些处理器使用了Web Worker来实现多线程异步处理(例如矩形优化处理器)。
|
||||||
|
|
||||||
|
Web Worker为单独打包的资产文件,但某些打包工具可能会对`node_modules`中的依赖进行预构建来提高性能,如果Worker文件被视为了预构建的一部分,就可能导致处理器无法正确地处理Worker文件的相对引用路径,导致在运行时尝试从`node_modules/.vite/deps/assets/`这样的内部路径加载,而这个路径在实际部署或服务时是不存在的。
|
||||||
|
|
||||||
|
**解决方法**
|
||||||
|
在打包工具中对该库进行配置,禁用对该库的优化和预构建,以`vite`为例:
|
||||||
|
```ts
|
||||||
|
// vite.config.ts
|
||||||
|
...
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ["@mes-processors/libs"] // 从optimizeDeps中排除该库
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 发布并打包项目
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm build
|
||||||
|
pnpm pack
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 发布前记得更改版本号
|
||||||
|
|
||||||
|
### 约定
|
||||||
|
|
||||||
|
**目录**
|
||||||
|
|
||||||
|
```
|
||||||
|
src
|
||||||
|
├── modules 项目模块分组
|
||||||
|
├── processors 处理器
|
||||||
|
└── utils 公用的工具类
|
||||||
|
```
|
||||||
|
|
||||||
|
**导出和打包**
|
||||||
|
|
||||||
|
- 编写的处理器请在`src/index.ts`中进行导出
|
||||||
|
- 编写的工具类请在`src/utils/index.ts`中进行导出
|
||||||
|
- 在打包时项目仅会对`src/index.ts`进行打包,工具类相关模块不会进行打包
|
||||||
|
- 关于打包相关明细请自行查看相关文件
|
||||||
|
- [package.json](package.json)
|
||||||
|
- [vite.config.ts](vite.config.ts)
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> 在该工作区中编写模块时,禁止使用绝对路径进行导入,禁止在`tsconfig.json`或`vite.config.ts`中添加"@"别名,所有导入语句请使用相对路径进行引入,否则会因monorepo内部导入混乱导致模块解析失败。
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
项目使用[Vitest](http://vitest.dev/)作为单元测试框架,若要对TS文件编写单元测试,请在文件的同目录下创建`<文件名>.test.ts`文件,并遵循Vitest规范编写单元测试。
|
||||||
|
|
||||||
|
要执行单元测试,请运行下面的命令:
|
||||||
|
```sh
|
||||||
|
pnpm test
|
||||||
|
```
|
39
package.json
Normal file
39
package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@mes-process-code-converter/libs",
|
||||||
|
"version": "0.1.4",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"test": "vitest",
|
||||||
|
"check": "tsc --noEmit --skipLibCheck -p tsconfig.app.json"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"package.json",
|
||||||
|
"src",
|
||||||
|
"tsconfig.json",
|
||||||
|
"tsconfig.app.json",
|
||||||
|
"tsconfig.node.json"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./utils": "./src/utils/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cut-abstractions": "http://gitea.cf/MES-FE/cut-abstractions/releases/download/0.3/cut-abstractions-0.3.3.tgz",
|
||||||
|
"mes-processors":"http://gitea.cf/MES-FE/mes-packages/releases/download/0.2/mes-processors-libs-0.2.1.tgz"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.10",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^7.0.0",
|
||||||
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
|
"vite-plugin-resolve": "^2.5.2",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
2471
pnpm-lock.yaml
generated
Normal file
2471
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
src/cncTest.test.ts
Normal file
68
src/cncTest.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { test } from "vitest";
|
||||||
|
import { CncConverter, CncTemplateParams, TemplateEndTargetType } from "./processors/CncConverter/CncConverter";
|
||||||
|
|
||||||
|
test('cncTest', async () => {
|
||||||
|
const testData: CncTemplateParams = {
|
||||||
|
templateName: 'MicroDrawBan_XML',
|
||||||
|
propertyList: [{
|
||||||
|
propertyName: 'Version',
|
||||||
|
propertyValue: '3.0',
|
||||||
|
}, {
|
||||||
|
propertyName: 'Time',
|
||||||
|
propertyValue: '20250903',
|
||||||
|
}, {
|
||||||
|
propertyName: 'Source',
|
||||||
|
propertyValue: '福州晨丰科技有限公司',
|
||||||
|
}, {
|
||||||
|
propertyName: 'SourceType',
|
||||||
|
propertyValue: 'CNC-Drilling',
|
||||||
|
}],
|
||||||
|
children: [{
|
||||||
|
templateName: 'Plane',
|
||||||
|
propertyList: [{
|
||||||
|
propertyName: 'Name',
|
||||||
|
propertyValue: '板名称',
|
||||||
|
}, {
|
||||||
|
propertyName: 'Code',
|
||||||
|
propertyValue: '板编号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: 'Order',
|
||||||
|
propertyValue: '生产单号',
|
||||||
|
}],
|
||||||
|
children: [{
|
||||||
|
templateName: 'Outline',
|
||||||
|
propertyList: [],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
templateName: 'Point',
|
||||||
|
propertyList: [{
|
||||||
|
propertyName:'Value',
|
||||||
|
propertyValue: 213,
|
||||||
|
}],
|
||||||
|
children: [],
|
||||||
|
templateEndType: TemplateEndTargetType.SingleEnd
|
||||||
|
}
|
||||||
|
],
|
||||||
|
templateEndType: TemplateEndTargetType.DoubleEnd
|
||||||
|
},
|
||||||
|
{
|
||||||
|
templateName:'HoleV',
|
||||||
|
propertyList:[
|
||||||
|
{
|
||||||
|
propertyName:'Name',
|
||||||
|
propertyValue:"HoleV"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children:[],
|
||||||
|
templateEndType: TemplateEndTargetType.SingleEnd
|
||||||
|
}
|
||||||
|
],
|
||||||
|
templateEndType: TemplateEndTargetType.DoubleEnd
|
||||||
|
}],
|
||||||
|
templateEndType: TemplateEndTargetType.DoubleEnd
|
||||||
|
}
|
||||||
|
let cncWriter = new CncConverter()
|
||||||
|
await cncWriter.doXML([testData])
|
||||||
|
debugger
|
||||||
|
})
|
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// processors
|
||||||
|
export * from "./processors/processCodeConverter/processCodeConverter";
|
||||||
|
export * from "./processors/NcConverter/NcConverter"
|
2
src/modules/README.md
Normal file
2
src/modules/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
### react-layout
|
||||||
|
矩形优化算法(陈总新优化)
|
1523
src/modules/rect-layout/KLSCclass.ts
Normal file
1523
src/modules/rect-layout/KLSCclass.ts
Normal file
File diff suppressed because it is too large
Load Diff
1993
src/modules/rect-layout/RectOptimizeMachine.ts
Normal file
1993
src/modules/rect-layout/RectOptimizeMachine.ts
Normal file
File diff suppressed because it is too large
Load Diff
17
src/modules/rect-layout/RectOptimizeMachine.worker.ts
Normal file
17
src/modules/rect-layout/RectOptimizeMachine.worker.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {type Big_bang,type xbang } from "./bang";
|
||||||
|
import { RectOptimizeMachine } from "./RectOptimizeMachine";
|
||||||
|
|
||||||
|
const ctx: Worker = self as any;
|
||||||
|
|
||||||
|
ctx.addEventListener("message", async (event) =>
|
||||||
|
{
|
||||||
|
// 小板 / 大板 / 大板数量 / 优化迭代次数 / 双面加工优先排版? / 刀路间隙 / 电子锯算法 / ???
|
||||||
|
let [xbangs, B_bang, B_bangsl, yhcs, isdtwosided, gap, dzjsf, yuliaodo2face] = (event.data) as [xbang[], Big_bang[], number[], number, boolean, number, boolean, boolean];
|
||||||
|
|
||||||
|
let m = new RectOptimizeMachine(async (best, scrap, fit) =>
|
||||||
|
{
|
||||||
|
ctx.postMessage([best, scrap, fit]);
|
||||||
|
});
|
||||||
|
|
||||||
|
m.Start(xbangs, B_bang, B_bangsl, yhcs, isdtwosided, gap, dzjsf, yuliaodo2face);
|
||||||
|
});
|
42
src/modules/rect-layout/bang.ts
Normal file
42
src/modules/rect-layout/bang.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ComposingType, HoleType, WaveType } from "cut-abstractions";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单块1220*2440的板结果 Container
|
||||||
|
*/
|
||||||
|
export type Con = YH_bang[];
|
||||||
|
/**
|
||||||
|
* 个体:优化结果 包含多个大板 Individual
|
||||||
|
*/
|
||||||
|
export type Inv = Con[];
|
||||||
|
|
||||||
|
export interface YH_bang {
|
||||||
|
bangid: number;
|
||||||
|
line: WaveType;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
pbg: number;
|
||||||
|
pbk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待优化小板 */
|
||||||
|
export interface xbang {
|
||||||
|
l: number;
|
||||||
|
w: number;
|
||||||
|
line: WaveType;
|
||||||
|
face: ComposingType;
|
||||||
|
id: number;
|
||||||
|
bno: string;
|
||||||
|
holeFaceCount: HoleType;
|
||||||
|
isRect?: boolean;
|
||||||
|
hasHole?: boolean;
|
||||||
|
isdtwosided?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待优化大板 */
|
||||||
|
export interface Big_bang
|
||||||
|
{
|
||||||
|
l: number;
|
||||||
|
w: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
29
src/modules/rect-layout/utils.ts
Normal file
29
src/modules/rect-layout/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export async function Sleep(time: number)
|
||||||
|
{
|
||||||
|
return new Promise(res =>
|
||||||
|
{
|
||||||
|
setTimeout(res, time);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayLast<T>(arr: { [key: number]: T, length: number; }): T
|
||||||
|
{
|
||||||
|
return arr[arr.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayMax<T>(arr: T[], f: (item: T) => number = a => (a as unknown as number)): [T, number]
|
||||||
|
{
|
||||||
|
let max = -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];
|
||||||
|
}
|
149
src/processors/CncConverter/CncConverter.ts
Normal file
149
src/processors/CncConverter/CncConverter.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
|
||||||
|
export class CncConverter implements ICncWriter {
|
||||||
|
private lines: string[] = []
|
||||||
|
private nodes: any[] = [];
|
||||||
|
private actionRecord: CncAction[] = []
|
||||||
|
get cncActions(): CncAction[] {
|
||||||
|
return this.actionRecord
|
||||||
|
}
|
||||||
|
config: CncConverterConfig = {
|
||||||
|
isNcFileComment: true,
|
||||||
|
/** 换行符 个别文件需要用 ;\n 结束*/
|
||||||
|
lineBreak: '\n',
|
||||||
|
|
||||||
|
leaderChar: '//',
|
||||||
|
}
|
||||||
|
async doXML(data: CncTemplateParams[]) {
|
||||||
|
// let line = []
|
||||||
|
for (const template of data) {
|
||||||
|
await this.doCncTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCncTemplate(templateItem: CncTemplateParams, level: number = 0) {
|
||||||
|
/** 行缩进 内容 */
|
||||||
|
let tabContent = ` `
|
||||||
|
/** 实际行缩进输出内容 */
|
||||||
|
let tabVal = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < level; i++) {
|
||||||
|
// 按照节点层级 生成缩进内容
|
||||||
|
tabVal = tabVal + tabContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node: any[] = []
|
||||||
|
node.push(tabVal)
|
||||||
|
let startCode = `<${templateItem.templateName}`
|
||||||
|
node.push(startCode)
|
||||||
|
for (const propertyInfo of templateItem.propertyList) {
|
||||||
|
let propertyItem = `${propertyInfo.propertyName}="${propertyInfo.propertyValue}"`
|
||||||
|
node.push(propertyItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
let endStr = ' >'
|
||||||
|
if (templateItem.templateEndType == TemplateEndTargetType.SingleEnd) {
|
||||||
|
endStr = ' />'
|
||||||
|
}
|
||||||
|
|
||||||
|
node.push(endStr)
|
||||||
|
|
||||||
|
this.lines.push(node.join(' '))
|
||||||
|
if (Array.isArray(templateItem.children) && templateItem.children.length > 0) {
|
||||||
|
// 子节点 内容
|
||||||
|
for (const kid of templateItem.children) {
|
||||||
|
await this.doCncTemplate(kid, level + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结尾 */
|
||||||
|
let endCode = templateItem.templateEndType == TemplateEndTargetType.DoubleEnd ? `</ ${templateItem.templateName}>` : ''
|
||||||
|
if(endCode){
|
||||||
|
this.lines.push((tabVal + endCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
recordAction(type: CncActionType): string {
|
||||||
|
const id = this.createActionId();
|
||||||
|
const act: CncAction = {
|
||||||
|
id: id,
|
||||||
|
type,
|
||||||
|
lineIndex: this.lines.length
|
||||||
|
};
|
||||||
|
this.comment(`CMP ${act.id} ${act.type}`);
|
||||||
|
this.actionRecord.push(act);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
private _actionIdx = 0;
|
||||||
|
private createActionId() {
|
||||||
|
return `A${this._actionIdx++}`;
|
||||||
|
}
|
||||||
|
comment(content: string): string {
|
||||||
|
let markContent = content + this.config.lineBreak
|
||||||
|
let isShowMark = this.config.isNcFileComment || false
|
||||||
|
if (isShowMark) {
|
||||||
|
let leaderChar = this.config.leaderChar || ''
|
||||||
|
markContent = `${leaderChar} ${markContent}`
|
||||||
|
} else {
|
||||||
|
markContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return markContent + this.config.lineBreak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICncWriter {
|
||||||
|
get cncActions(): CncAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CncAction {
|
||||||
|
readonly id: string;
|
||||||
|
readonly type: CncActionType;
|
||||||
|
readonly lineIndex: number;
|
||||||
|
// parent: CncAction;
|
||||||
|
// children: CncAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CncActionType = string
|
||||||
|
|
||||||
|
export type CncTemplateParams = {
|
||||||
|
/** 标签名 */
|
||||||
|
templateName: string
|
||||||
|
/** 属性列表 */
|
||||||
|
propertyList: CncTemplatePropertyType[]
|
||||||
|
/** 子标签 */
|
||||||
|
children?: CncTemplateParams[]
|
||||||
|
/** 标签结束方式 */
|
||||||
|
templateEndType?: TemplateEndTargetType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CncTemplatePropertyType {
|
||||||
|
/** 属性名 */
|
||||||
|
propertyName: string
|
||||||
|
/** 属性值 */
|
||||||
|
propertyValue?: string | number
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 节点结束类型
|
||||||
|
* 标记文本语言的结尾形式
|
||||||
|
*
|
||||||
|
* <div></div>
|
||||||
|
* <div />
|
||||||
|
*/
|
||||||
|
export enum TemplateEndTargetType {
|
||||||
|
/** 这种 <div></div>*/
|
||||||
|
DoubleEnd = 0,
|
||||||
|
/** 这种 <div />*/
|
||||||
|
SingleEnd = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CncConverterConfig = {
|
||||||
|
/** 换行符 个别文件需要用 ;\n 结束*/
|
||||||
|
lineBreak?: string
|
||||||
|
/** 是否添加注释信息 */
|
||||||
|
isNcFileComment?: boolean
|
||||||
|
/** 注释标识符 */
|
||||||
|
leaderChar?: string
|
||||||
|
}
|
5
src/processors/NcConverter/NcConverter.test.ts
Normal file
5
src/processors/NcConverter/NcConverter.test.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { test } from "vitest";
|
||||||
|
|
||||||
|
test('NcConverterTest',()=>{
|
||||||
|
|
||||||
|
})
|
794
src/processors/NcConverter/NcConverter.ts
Normal file
794
src/processors/NcConverter/NcConverter.ts
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
import { CCode, CCodeParams, FaceType, GCode, GCodeParams, INcWriter, IPoint, Knife, NcAction, NcActionType, } from "cut-abstractions";
|
||||||
|
// // import { CArc2GCode, NcArcType, NcReductionType } from "mes-processors"
|
||||||
|
// import { Vector2 } from "node_modules/mes-processors/src/math/vector2";
|
||||||
|
|
||||||
|
|
||||||
|
/** 用以对接 NC类型的加工文件--即 解析器 */
|
||||||
|
export class NcConverter implements INcWriter {
|
||||||
|
/** NC加工动作记录 */
|
||||||
|
|
||||||
|
private lines: string[] = [];
|
||||||
|
/** 刀库 */
|
||||||
|
knifeList: Array<Knife> = []
|
||||||
|
/** 当前刀具 */
|
||||||
|
private currentKnife?: Knife = undefined;
|
||||||
|
private actionRecord: NcAction[] = [];
|
||||||
|
arcType: NcArcType = 'R';
|
||||||
|
/** 最后一行代码参数 */
|
||||||
|
// private lastParams?: any
|
||||||
|
private lastParams: GCodeParams = new GCodeParams();
|
||||||
|
private lastCode: string = '';
|
||||||
|
/** 可以做代码转换 如G2转G3,G3转G2等 */
|
||||||
|
codeMap: Record<string, string> = {};
|
||||||
|
get ncActions(): NcAction[] {
|
||||||
|
return this.actionRecord;
|
||||||
|
}
|
||||||
|
reductionType: NcReductionType = NcReductionType.None;
|
||||||
|
/** 配置 这里给默认值*/
|
||||||
|
config: NcConverterConfig = {
|
||||||
|
isEnableConverterAxis: false,
|
||||||
|
thickness: 18,
|
||||||
|
doSimpleFirstCode: false,
|
||||||
|
isNcFileComment: true,
|
||||||
|
isNcLinePrefixEnabled: false,
|
||||||
|
ncLinePrefix: '',
|
||||||
|
isUseSimpleCode: false,
|
||||||
|
isSimpleFirstCode: false,
|
||||||
|
arcType: 'R',
|
||||||
|
reverseArcCode: false,
|
||||||
|
NcCodeFreeMove: 'G00',
|
||||||
|
NcCodeLineInterpolation: 'G01',
|
||||||
|
NcCodeClockwiseArcInterpolation: 'G02',
|
||||||
|
NcCodeAnticlockwiseArcInterpolation: 'G03',
|
||||||
|
NcCodeAxisX: 'X',
|
||||||
|
NcCodeAxisY: 'Y',
|
||||||
|
NcCodeAxisZ: 'Z',
|
||||||
|
NcCodeSpeed: 'F',
|
||||||
|
NcCodeIncrementAxisX: 'I',
|
||||||
|
NcCodeIncrementAxisY: 'J',
|
||||||
|
NcCodeIncrementAxisZ: 'K',
|
||||||
|
leaderChar: '//',
|
||||||
|
boardLength: 2440,
|
||||||
|
boardWidth: 1220,
|
||||||
|
boardHeight: 50,
|
||||||
|
originPointPosition: BoardPosition.LEFT_TOP,
|
||||||
|
originZ0Position: OriginZPosition.WorkTop,
|
||||||
|
heightAxis: AxisType.Z_POS,
|
||||||
|
decimalPointPrecision: 3,
|
||||||
|
fixFloatNumberEndZero: true,
|
||||||
|
intNumberAddDecimalPoint: true,
|
||||||
|
lineBreak: '\n'
|
||||||
|
}
|
||||||
|
/** G代码转换关系 */
|
||||||
|
codeTransform = {
|
||||||
|
[GCode.G0]: this.config.NcCodeFreeMove,
|
||||||
|
[GCode.G1]: this.config.NcCodeLineInterpolation,
|
||||||
|
[GCode.G2]: this.config.NcCodeClockwiseArcInterpolation,
|
||||||
|
[GCode.G3]: this.config.NcCodeAnticlockwiseArcInterpolation
|
||||||
|
}
|
||||||
|
initConfig(conf: NcConverterConfig) {
|
||||||
|
this.config = { ...this.config, ...conf }
|
||||||
|
|
||||||
|
/** 更新下代码转换关系 */
|
||||||
|
this.codeTransform = {
|
||||||
|
[GCode.G0]: this.config.NcCodeFreeMove,
|
||||||
|
[GCode.G1]: this.config.NcCodeLineInterpolation,
|
||||||
|
[GCode.G2]: this.config.NcCodeClockwiseArcInterpolation,
|
||||||
|
[GCode.G3]: this.config.NcCodeAnticlockwiseArcInterpolation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected code(code: keyof typeof GCode, params: Partial<GCodeParams>) {
|
||||||
|
const line: any[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* G0 - G3 代码 显示的时候可以配置
|
||||||
|
* 例
|
||||||
|
* G00 - G03
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (this.reductionType & NcReductionType.Code) {
|
||||||
|
if (this.lastCode != code) {
|
||||||
|
line.push(this.codeTransform[code]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line.push(this.codeTransform[code]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastCode = code;
|
||||||
|
|
||||||
|
let x = params.x ??= this.lastParams.x;
|
||||||
|
let y = params.y ??= this.lastParams.y;
|
||||||
|
let z = params.z ??= this.lastParams.z;
|
||||||
|
|
||||||
|
let x_val = this.handleValue_DecimalPointPrecision(x)
|
||||||
|
let y_val = this.handleValue_DecimalPointPrecision(y)
|
||||||
|
let z_val = this.handleValue_DecimalPointPrecision(z)
|
||||||
|
|
||||||
|
let x_code = this.config.NcCodeAxisX || 'X'
|
||||||
|
let y_code = this.config.NcCodeAxisY || 'Y'
|
||||||
|
let z_code = this.config.NcCodeAxisZ || 'Z'
|
||||||
|
|
||||||
|
if (this.reductionType & NcReductionType.Position) {
|
||||||
|
if (x != this.lastParams.x) {
|
||||||
|
line.push(x_code + x_val);
|
||||||
|
}
|
||||||
|
if (y != this.lastParams.y) {
|
||||||
|
line.push(y_code + y_val);
|
||||||
|
}
|
||||||
|
if (z != this.lastParams.z) {
|
||||||
|
line.push(z_code + z_val);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line.push(x_code + x_val);
|
||||||
|
line.push(y_code + y_val);
|
||||||
|
line.push(z_code + z_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == 'G2' || code == 'G3') {
|
||||||
|
if (this.config.arcType == 'R') {
|
||||||
|
|
||||||
|
let r = params.r ??= this.lastParams.r;
|
||||||
|
|
||||||
|
let r_val = this.handleValue_DecimalPointPrecision(r)
|
||||||
|
|
||||||
|
line.push('R' + r_val);
|
||||||
|
}
|
||||||
|
if (this.config.arcType == 'IJK') {
|
||||||
|
let i = params.i ??= this.lastParams.i;
|
||||||
|
let j = params.j ??= this.lastParams.j;
|
||||||
|
|
||||||
|
let i_val = this.handleValue_DecimalPointPrecision(i)
|
||||||
|
let j_val = this.handleValue_DecimalPointPrecision(j)
|
||||||
|
|
||||||
|
line.push('I' + i_val);
|
||||||
|
line.push('J' + j_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const speed = params.f ??= this.lastParams.f
|
||||||
|
if (speed != 0) {
|
||||||
|
if (this.reductionType & NcReductionType.Speed) {
|
||||||
|
if (speed != this.lastParams.f) {
|
||||||
|
line.push('F' + speed);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line.push('F' + speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.assign(this.lastParams, params); // 更新上一次参数
|
||||||
|
if (this.codeMap[line[0]]) { // 命令转换
|
||||||
|
line[0] = this.codeMap[line[0]];
|
||||||
|
}
|
||||||
|
this.lines.push(line.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
gCode<TCode extends (keyof typeof GCode | keyof typeof CCode)>(code: TCode, params: Partial<TCode extends keyof typeof GCode ? GCodeParams : CCodeParams>): void {
|
||||||
|
switch (code) {
|
||||||
|
case 'G0':
|
||||||
|
case 'G1':
|
||||||
|
case 'G2':
|
||||||
|
case 'G3': {
|
||||||
|
this.code(code, params);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 自定义代码
|
||||||
|
case 'CArc': {
|
||||||
|
// 凸度转GCode
|
||||||
|
const cParam = params as Partial<CCodeParams>;
|
||||||
|
if (!cParam.x || !cParam.y || !cParam.b) throw new Error("CArc命令缺少必要参数(X, Y, B)");
|
||||||
|
const targetPoint = { x: cParam.x, y: cParam.y };
|
||||||
|
if (this.config.arcType === 'R') {
|
||||||
|
const result = CArc2GCode(this.lastParams, targetPoint, cParam.b, 'R');
|
||||||
|
this.code(result.gCode, { ...targetPoint, r: result.r });
|
||||||
|
} else {
|
||||||
|
const result = CArc2GCode(this.lastParams, targetPoint, cParam.b, 'IJK');
|
||||||
|
this.code(result.gCode, { ...targetPoint, i: result.i, j: result.j });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理值 最终显示的值 小数点后X位功能 */
|
||||||
|
handleValue_DecimalPointPrecision(val: number) {
|
||||||
|
const { fixFloatNumberEndZero, intNumberAddDecimalPoint, decimalPointPrecision } = this.config
|
||||||
|
/**
|
||||||
|
* 2种方式 末尾补零 或者 直接保留小数点后N位
|
||||||
|
*/
|
||||||
|
let isToFix = false
|
||||||
|
|
||||||
|
let resVal
|
||||||
|
|
||||||
|
if (fixFloatNumberEndZero == true) {
|
||||||
|
// 末尾补零
|
||||||
|
isToFix = true
|
||||||
|
} else if (intNumberAddDecimalPoint == true) {
|
||||||
|
// 整数值末尾加小数点
|
||||||
|
if (Number.isInteger(val)) {
|
||||||
|
isToFix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isToFix) {
|
||||||
|
resVal = val.toFixed(decimalPointPrecision)
|
||||||
|
} else {
|
||||||
|
resVal = val.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return resVal
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 更换刀具 */
|
||||||
|
changeKnife() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验值是否有效 不为'' 或 undefined */
|
||||||
|
checkVal(val: any): boolean {
|
||||||
|
let r = true
|
||||||
|
if ((val == undefined || val == '')) {
|
||||||
|
r = false
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
return this.lines.join(this.config.lineBreak);
|
||||||
|
}
|
||||||
|
comment(content: string): string {
|
||||||
|
|
||||||
|
let markContent = content + this.config.lineBreak
|
||||||
|
let isShowMark = this.config.isNcFileComment || false
|
||||||
|
if (isShowMark) {
|
||||||
|
let leaderChar = this.config.leaderChar || ''
|
||||||
|
markContent = `${leaderChar} ${markContent}`
|
||||||
|
} else {
|
||||||
|
markContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return markContent + this.config.lineBreak
|
||||||
|
}
|
||||||
|
|
||||||
|
recordAction(type: NcActionType): string {
|
||||||
|
const id = this.createActionId();
|
||||||
|
const act: NcAction = {
|
||||||
|
id: id,
|
||||||
|
type,
|
||||||
|
lineIndex: this.lines.length
|
||||||
|
};
|
||||||
|
this.comment(`CMP ${act.id} ${act.type}`);
|
||||||
|
this.actionRecord.push(act);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
private _actionIdx = 0;
|
||||||
|
private createActionId() {
|
||||||
|
return `A${this._actionIdx++}`;
|
||||||
|
}
|
||||||
|
append(str: string) {
|
||||||
|
// this.lines.push(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLine(str: string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param point 加工项的点
|
||||||
|
* @param processItemInfo 加工项的信息 水平基准、垂直基准、轴方向(x、y、z),板件方向(x、y、z)
|
||||||
|
* // 这里加工项的点数据 都是经过数据处理的 假定这里拿到的数据都是基于左上角 台面
|
||||||
|
*
|
||||||
|
* @returns 实际加工项的点
|
||||||
|
*/
|
||||||
|
getXYZ(point: CodeParams, processItemInfo: ProcessInfo): CodeParams {
|
||||||
|
let newPoint: any = {}
|
||||||
|
|
||||||
|
if (this.config.isEnableConverterAxis) {
|
||||||
|
// 进行坐标轴转换
|
||||||
|
for (const key in point) {
|
||||||
|
if (point[key] != undefined) {
|
||||||
|
Reflect.set(newPoint, key, parseFloat(point[key]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /** 有2个部分
|
||||||
|
// * 一个是基于机台和板件的转换 依据板件定位
|
||||||
|
// * 另外一个是基于板件和加工项的转换 依据板件长高方向*/
|
||||||
|
// switch (this.config.originZ0Position) {
|
||||||
|
// case 0:
|
||||||
|
// // 台面 不操作
|
||||||
|
// break;
|
||||||
|
// case 1:
|
||||||
|
// // 板面 Z坐标需要转换
|
||||||
|
// newPoint.z = newPoint.z - this.config.thickness
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /** step 先转换板件的位置 */
|
||||||
|
// // 大板定位 不同 根据不同的定位点修改
|
||||||
|
|
||||||
|
// // processItemInfo
|
||||||
|
// switch (this.config.originPointPosition) {
|
||||||
|
// case BoardPosition.LEFT_TOP:
|
||||||
|
// // 不操作
|
||||||
|
// newPoint = this.changeXYZAxiosSide(newPoint)
|
||||||
|
// break;
|
||||||
|
// case BoardPosition.LEFT_BOTTOM:
|
||||||
|
// // 左下角 x坐标要转换
|
||||||
|
// newPoint.x = newPoint.x + this.config.boardWidth - processItemInfo.block.cutWidth //400
|
||||||
|
// newPoint = this.changeXYZAxiosSide(newPoint)
|
||||||
|
// break;
|
||||||
|
// case BoardPosition.RIGHT_TOP:
|
||||||
|
// // 右上角 y坐标要转换
|
||||||
|
// newPoint.y = newPoint.y + this.config.boardLength - processItemInfo.block.cutLength // 600
|
||||||
|
// newPoint = this.changeXYZAxiosSide(newPoint)
|
||||||
|
// break;
|
||||||
|
// case BoardPosition.RIGHT_BOTTOM:
|
||||||
|
// // 右下角 xy 坐标要转换
|
||||||
|
// newPoint.x = newPoint.x + this.config.boardWidth - processItemInfo.block.cutWidth
|
||||||
|
// newPoint.y = newPoint.y + this.config.boardLength - processItemInfo.block.cutLength
|
||||||
|
// newPoint = this.changeXYZAxiosSide(newPoint)
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 这里做 数值的小数点处理
|
||||||
|
for (const key in newPoint) {
|
||||||
|
if (['x', 'y', 'z', 'r', 'i', 'j', 'k'].includes(key)) {
|
||||||
|
let isTofix = false
|
||||||
|
if (this.config.fixFloatNumberEndZero == true) {
|
||||||
|
// 末尾补零
|
||||||
|
isTofix = true
|
||||||
|
} else if (this.config.intNumberAddDecimalPoint == true) {
|
||||||
|
// 整数值末尾加小数点
|
||||||
|
if (Number.isInteger(newPoint[key])) {
|
||||||
|
isTofix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isTofix) {
|
||||||
|
newPoint[key] = parseFloat(newPoint[key]).toFixed(this.config.decimalPointPrecision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPoint
|
||||||
|
} else {
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
/** 根据 轴向变更坐标 */
|
||||||
|
changeXYZAxiosSide(point: CodeParams) {
|
||||||
|
let newPoint: any = {}
|
||||||
|
for (const key in point) {
|
||||||
|
if (point[key] != undefined) {
|
||||||
|
Reflect.set(newPoint, key, parseFloat(point[key]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = this.config.boardWidth
|
||||||
|
let length = this.config.boardLength
|
||||||
|
let height = this.config.boardHeight
|
||||||
|
|
||||||
|
|
||||||
|
if (this.config.widthSideAxis == AxisType.X_POS && this.config.lengthSideAxis == AxisType.Y_POS) {
|
||||||
|
// 默认 为 X = x 正 Y = y 正 不操作
|
||||||
|
} else if (this.config.widthSideAxis == AxisType.Y_POS && this.config.lengthSideAxis == AxisType.X_POS) {
|
||||||
|
// x = y正 y = x正 X Y坐标 倒转
|
||||||
|
newPoint = { ...newPoint, x: newPoint.y, y: newPoint.x }
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (this.config.widthSideAxis == AxisType.X_NEG && this.config.lengthSideAxis == AxisType.Y_POS) {
|
||||||
|
// x = x负 y = y正
|
||||||
|
newPoint = { ...newPoint, x: newPoint.x - width, y: newPoint.y }
|
||||||
|
} else if (this.config.widthSideAxis == AxisType.Y_POS && this.config.lengthSideAxis == AxisType.X_NEG) {
|
||||||
|
// x = y正 y = x负
|
||||||
|
newPoint = { ...newPoint, x: newPoint.y - width, y: newPoint.x }
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (this.config.widthSideAxis == AxisType.X_NEG && this.config.lengthSideAxis == AxisType.Y_NEG) {
|
||||||
|
// x = x负 y = y负
|
||||||
|
newPoint = { ...newPoint, x: newPoint.x - width, y: newPoint.y - length }
|
||||||
|
} else if (this.config.widthSideAxis == AxisType.Y_NEG && this.config.lengthSideAxis == AxisType.X_NEG) {
|
||||||
|
// x = y负 y = x负
|
||||||
|
newPoint = { ...newPoint, x: newPoint.y - width, y: newPoint.x - length }
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (this.config.widthSideAxis == AxisType.X_POS && this.config.lengthSideAxis == AxisType.Y_NEG) {
|
||||||
|
// x = x正 y = y负
|
||||||
|
newPoint = { ...newPoint, x: newPoint.x, y: newPoint.y - length }
|
||||||
|
} else if (this.config.widthSideAxis == AxisType.Y_NEG && this.config.lengthSideAxis == AxisType.X_POS) {
|
||||||
|
// x = y负 y = x正
|
||||||
|
newPoint = { ...newPoint, x: newPoint.y, y: newPoint.x - length }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.heightAxis == AxisType.Z_NEG) {
|
||||||
|
// Z轴负
|
||||||
|
newPoint = { ...newPoint, z: newPoint.z - height }
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NcConverterConfig = {
|
||||||
|
/** 是否启用解析器的坐标系转换 */
|
||||||
|
isEnableConverterAxis?: boolean,
|
||||||
|
/** 板厚 */
|
||||||
|
thickness: number,
|
||||||
|
/**是否执行换刀后的第一行精简指令 */
|
||||||
|
doSimpleFirstCode?: boolean
|
||||||
|
/** 是否添加注释信息 */
|
||||||
|
isNcFileComment?: boolean
|
||||||
|
/** 是否空行插入前缀 */
|
||||||
|
isNcLinePrefixEnabled?: boolean
|
||||||
|
/** 空行插入前缀 前缀内容*/
|
||||||
|
ncLinePrefix?: string
|
||||||
|
/** 使用精简指令 */
|
||||||
|
isUseSimpleCode?: boolean
|
||||||
|
/** 精简换刀后第一行指令 */
|
||||||
|
isSimpleFirstCode?: boolean
|
||||||
|
/** 圆弧指令模式类型 */
|
||||||
|
arcType?: NcArcType
|
||||||
|
/** 反转圆弧指令 */
|
||||||
|
reverseArcCode?: boolean
|
||||||
|
/** 空程移动指令 */
|
||||||
|
NcCodeFreeMove?: string
|
||||||
|
/** 直线插补标识 */
|
||||||
|
NcCodeLineInterpolation?: string
|
||||||
|
/** 顺时针圆弧插补标识 */
|
||||||
|
NcCodeClockwiseArcInterpolation?: string
|
||||||
|
/** 逆时针圆弧插补标识 */
|
||||||
|
NcCodeAnticlockwiseArcInterpolation?: string
|
||||||
|
/** 水平坐标横轴标识 */
|
||||||
|
NcCodeAxisX?: string
|
||||||
|
/** 水平坐标纵轴标识 */
|
||||||
|
NcCodeAxisY?: string
|
||||||
|
/** 垂直坐标轴标识 */
|
||||||
|
NcCodeAxisZ?: string
|
||||||
|
/** 速度标识 */
|
||||||
|
NcCodeSpeed?: string
|
||||||
|
/** 水平坐标横轴增量标识 */
|
||||||
|
NcCodeIncrementAxisX?: string
|
||||||
|
/** 水平坐标纵轴增量标识 */
|
||||||
|
NcCodeIncrementAxisY?: string
|
||||||
|
/** 垂直坐标轴增量标识 */
|
||||||
|
NcCodeIncrementAxisZ?: string
|
||||||
|
/** 注释标识符 */
|
||||||
|
leaderChar?: string
|
||||||
|
/** 工作区域长 x */
|
||||||
|
boardLength: number
|
||||||
|
/** 工作区域宽 y */
|
||||||
|
boardWidth: number
|
||||||
|
/** 工作区域高 z */
|
||||||
|
boardHeight: number
|
||||||
|
/** 水平基准点 */
|
||||||
|
originPointPosition?: BoardPosition
|
||||||
|
/** 垂直基准点 */
|
||||||
|
originZ0Position?: OriginZPosition
|
||||||
|
/** 水平纵轴坐标轴向 */
|
||||||
|
widthSideAxis?: AxisType
|
||||||
|
/** 水平横轴坐标轴向 */
|
||||||
|
lengthSideAxis?: AxisType
|
||||||
|
/** 垂直轴坐标轴向 */
|
||||||
|
heightAxis?: AxisType
|
||||||
|
/** 保留小数点位数 */
|
||||||
|
decimalPointPrecision?: number
|
||||||
|
/** 末尾补零 */
|
||||||
|
fixFloatNumberEndZero?: boolean
|
||||||
|
/** 整数值末尾加小数点 */
|
||||||
|
intNumberAddDecimalPoint?: boolean
|
||||||
|
/** 换行符 个别文件需要用 ;\n 结束*/
|
||||||
|
lineBreak?: string
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 枚举 大板边角位置 */
|
||||||
|
export enum BoardPosition {
|
||||||
|
/** 左上角 */
|
||||||
|
LEFT_TOP = 3,
|
||||||
|
/** 左下角 */
|
||||||
|
LEFT_BOTTOM = 0,
|
||||||
|
/** 右下角 */
|
||||||
|
RIGHT_BOTTOM = 1,
|
||||||
|
/** 右上角 */
|
||||||
|
RIGHT_TOP = 2,
|
||||||
|
}
|
||||||
|
/** 枚举 坐标轴类型 */
|
||||||
|
export enum OriginZPosition {
|
||||||
|
/** 台面向上Z轴正 */
|
||||||
|
WorkTop = 0,
|
||||||
|
/** 板面向上Z轴正 */
|
||||||
|
BoardFace = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 枚举 坐标轴类型 */
|
||||||
|
export enum AxisType {
|
||||||
|
/** X轴正 */
|
||||||
|
X_POS = 0,
|
||||||
|
/** X轴负 */
|
||||||
|
X_NEG = 1,
|
||||||
|
/** Y轴正 */
|
||||||
|
Y_POS = 2,
|
||||||
|
/** Y轴负 */
|
||||||
|
Y_NEG = 3,
|
||||||
|
/** 向上Z轴正 */
|
||||||
|
Z_POS = 4,
|
||||||
|
/** 向下Z轴负 */
|
||||||
|
Z_NEG = 5,
|
||||||
|
}
|
||||||
|
// 加工项 点数据
|
||||||
|
export class CodeParams {
|
||||||
|
/** x坐标 */
|
||||||
|
x?: Number | String
|
||||||
|
/** y坐标 */
|
||||||
|
y?: Number | String
|
||||||
|
/** z坐标 */
|
||||||
|
z?: Number | String
|
||||||
|
/** 调用的代码编号 */
|
||||||
|
dir?: Number | String
|
||||||
|
/** 圆弧半径 */
|
||||||
|
r?: Number | String
|
||||||
|
/** 速度 */
|
||||||
|
f?: Number | String
|
||||||
|
/** IJK 模式的i */
|
||||||
|
i?: Number | String
|
||||||
|
/** IJK 模式的j */
|
||||||
|
j?: Number | String
|
||||||
|
/** IJK 模式的k */
|
||||||
|
k?: Number | String
|
||||||
|
|
||||||
|
/** 代码标识 */
|
||||||
|
codeKey?: String
|
||||||
|
/** x坐标 */
|
||||||
|
xKey?: String
|
||||||
|
/** y坐标 */
|
||||||
|
yKey?: String
|
||||||
|
/** z坐标 */
|
||||||
|
zKey?: String
|
||||||
|
/** 圆弧半径 */
|
||||||
|
rKey?: String
|
||||||
|
/** 速度 */
|
||||||
|
fKey?: String
|
||||||
|
/** IJK 模式的i */
|
||||||
|
iKey?: String
|
||||||
|
/** IJK 模式的j */
|
||||||
|
jKey?: String
|
||||||
|
/** IJK 模式的k */
|
||||||
|
kKey?: String
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加工项对应的信息 */
|
||||||
|
export class ProcessInfo {
|
||||||
|
/**当前加工项的下标*/
|
||||||
|
i?: Number
|
||||||
|
/** 加工项 对应刀具的数据 */
|
||||||
|
knife?: Knife
|
||||||
|
/** 加工项的类型 */
|
||||||
|
type?: processItemType
|
||||||
|
/** 加工项所在的 文件名 */
|
||||||
|
belong?: string
|
||||||
|
/** 该加工项基于哪个加工面 传入数据 主要用于 加工项坐标转换 感觉可能没用 */
|
||||||
|
belongFace?: FaceType
|
||||||
|
/** 板件信息 */
|
||||||
|
block?: any
|
||||||
|
// /** 垂直基准点 */
|
||||||
|
// originZ0Position?: OriginZPosition
|
||||||
|
// /** 水平基准点 */
|
||||||
|
// originPointPosition?: BoardPosition
|
||||||
|
// /** 大板定位 */
|
||||||
|
// boardLocation?: BoardPosition
|
||||||
|
// /** 加工项的宽方向 x */
|
||||||
|
// widthSideAxis?: AxisType
|
||||||
|
// /** 加工项的长方向 y */
|
||||||
|
// lengthSideAxis ?: AxisType
|
||||||
|
// /** 加工项的高方向 */
|
||||||
|
// heightSideAxis ?: AxisType
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 加工项的类别
|
||||||
|
* 用于区分加工项的类别,不同的类别 在文件生成的时候 可能需要对应某些独立的节点
|
||||||
|
*/
|
||||||
|
export enum processItemType {
|
||||||
|
/**排钻 */
|
||||||
|
Hole = 'hole',
|
||||||
|
/** 铣孔 */
|
||||||
|
MillingHole = 'millingHole',
|
||||||
|
/** 造型 非槽加工 铣型 */
|
||||||
|
Model = 'model',
|
||||||
|
/** 造型 槽-- 拉槽 */
|
||||||
|
Grooves = 'grooves',
|
||||||
|
/** 造型 铣槽 */
|
||||||
|
MillingGrooves = 'millingGrooves',
|
||||||
|
/** 侧面造型 */
|
||||||
|
SideModel = 'SideModel',
|
||||||
|
/** 侧面槽 - 拉槽 */
|
||||||
|
SideGrooves = 'sideGrooves',
|
||||||
|
/** 侧面槽 - 拉槽 */
|
||||||
|
SideMillingGrooves = 'SideMillingGrooves',
|
||||||
|
/** 侧面孔 排钻 */
|
||||||
|
SideHole = 'sideHole',
|
||||||
|
/** 侧面孔 铣孔 */
|
||||||
|
MillingSideHole = 'MillingSideHole',
|
||||||
|
/** 开料 */
|
||||||
|
CutBlock = 'cutBlock',
|
||||||
|
/** 修边 */
|
||||||
|
MillingBlockBoard = 'MillingBlockBoard'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NcArcType = 'R' | 'IJK';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将基于凸度的圆弧转换为基于圆弧半径/圆心坐标表示的圆弧,暂不支持三维圆弧(Z轴或K分量)
|
||||||
|
* @param source 圆弧起始点
|
||||||
|
* @param target 圆弧终点
|
||||||
|
* @param bulge 凸度值
|
||||||
|
* @param mode 圆弧模式
|
||||||
|
* @returns 圆弧参数(IJ或R)
|
||||||
|
*/
|
||||||
|
export function CArc2GCode(source: IPoint, target: IPoint, bulge: number, mode: 'R'): { gCode: 'G2' | 'G3'; r: number; };
|
||||||
|
export function CArc2GCode(source: IPoint, target: IPoint, bulge: number, mode: 'IJK'): { gCode: 'G2' | 'G3'; i: number; j: number; };
|
||||||
|
export function CArc2GCode(source: IPoint, target: IPoint, bulge: number, mode: NcArcType): { gCode: 'G2' | 'G3'; r?: number; i?: number; j?: number; } {
|
||||||
|
/*
|
||||||
|
* ♿♿♿ 修改必看!!!
|
||||||
|
* 凸度为圆弧圆心角的1/4正切值
|
||||||
|
* Bulge = tan(θ / 4) θ为圆心角
|
||||||
|
* 凸度为正数,则从起点顺时针绘制圆弧到终点
|
||||||
|
* 凸度为负数,则逆时针绘制
|
||||||
|
* 当凸度为0时,绘制直线
|
||||||
|
* 当凸度为1时,圆心角为180度 tan(180 / 4) = 1
|
||||||
|
* 凸度转换公式见 https://www.lee-mac.com/bulgeconversion.html
|
||||||
|
*
|
||||||
|
* NC圆弧规则
|
||||||
|
* 圆弧半径(R)模式:
|
||||||
|
* 从起点到终点绘制半径为|R|的圆弧,R越大圆弧越平滑,越小圆弧越弯曲
|
||||||
|
* 当R为正数时,表示绘制圆的短弧
|
||||||
|
* 当R为负数时,表示绘制圆的长弧
|
||||||
|
* ⚠️注意 起点到终点的距离不可大于 2 * |R|,否则应当抛出错误
|
||||||
|
*
|
||||||
|
* 圆心坐标(IJ)模式:
|
||||||
|
* 从起点到终点绘制圆弧,圆心坐标为(I, J),I为圆心在X轴上的坐标(带符号),J为圆心在Y轴上的坐标(带符号)
|
||||||
|
* ⚠️注意 圆心到圆弧起点和圆心到圆弧终点的距离必须相等(都等于圆弧的半径),否则应当抛出错误
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (bulge === 0) {
|
||||||
|
throw new Error("圆弧模式下,凸度值不能为0");
|
||||||
|
}
|
||||||
|
|
||||||
|
const p1 = Vector2.FromPoint(source);
|
||||||
|
const p2 = Vector2.FromPoint(target);
|
||||||
|
|
||||||
|
const gCode = bulge > 0 ? 'G2' : 'G3';
|
||||||
|
|
||||||
|
const delta = p2.Subtract(p1);
|
||||||
|
const dist = delta.Magnitude;
|
||||||
|
|
||||||
|
if (dist < 1e-6) {
|
||||||
|
throw new Error("起点与终点距离过近");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过凸度值计算圆弧半径
|
||||||
|
// 圆弧半径 r = (弦长 / 2) * (1 + bulge²) / (2 * |bulge|)
|
||||||
|
// 详见: https://www.lee-mac.com/bulgeconversion.html#bulgeradius
|
||||||
|
const chordLength = delta.Magnitude;
|
||||||
|
const radius = (chordLength / 2) * (1 + bulge * bulge) / (2 * Math.abs(bulge));
|
||||||
|
|
||||||
|
// R模式
|
||||||
|
if (mode === 'R') {
|
||||||
|
return { gCode: gCode, r: radius };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IJK模式
|
||||||
|
// 圆心位于弦的垂直平分线
|
||||||
|
// 计算弦的中点
|
||||||
|
const midPoint = p1.Add(delta.Multiply(0.5));
|
||||||
|
// 计算弦心距d
|
||||||
|
// 公式:d = √(r² - a²),r为半径,a为弦长的一半(勾股定理)
|
||||||
|
const d = Math.sqrt(radius * radius - (dist / 2) * (dist / 2));
|
||||||
|
|
||||||
|
// 垂直平分线单位向量
|
||||||
|
const perpVec = new Vector2(-delta.y, delta.x).Normalize();
|
||||||
|
|
||||||
|
// 从弦的中点向垂直平分线向量移动弦心距d来计算出圆心的坐标
|
||||||
|
const center = midPoint.Add(perpVec.Multiply(d * -Math.sign(bulge)));
|
||||||
|
|
||||||
|
const i = center.x - p1.x;
|
||||||
|
const j = center.y - p1.y;
|
||||||
|
|
||||||
|
return { gCode, i, j };
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NcReductionType {
|
||||||
|
None = 0,
|
||||||
|
Position = 1 << 0,
|
||||||
|
Speed = 1 << 1,
|
||||||
|
Code = 1 << 2,
|
||||||
|
/**
|
||||||
|
* 换刀重置
|
||||||
|
*/
|
||||||
|
SwitchKnifeReset = 1 << 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Vector2 implements IPoint {
|
||||||
|
static Zero = new Vector2(0, 0);
|
||||||
|
static One = new Vector2(1, 1);
|
||||||
|
static Up = new Vector2(0, 1);
|
||||||
|
static Down = new Vector2(0, -1);
|
||||||
|
static Left = new Vector2(-1, 0);
|
||||||
|
static Right = new Vector2(1, 0);
|
||||||
|
static FromPoint(pt: IPoint) {
|
||||||
|
if (pt instanceof Vector2) return pt as Vector2;
|
||||||
|
return new Vector2(pt.x, pt.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
|
||||||
|
/** 获取向量的模长 */
|
||||||
|
get Magnitude() {
|
||||||
|
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取向量的平方模长,这个属性比 `Magnitude` 属性更快 */
|
||||||
|
get SquaredMagnitude() {
|
||||||
|
return Math.pow(this.x, 2) + Math.pow(this.y, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(x: number, y: number) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 克隆向量 */
|
||||||
|
Clone(): Vector2 {
|
||||||
|
return new Vector2(this.x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算两个向量之间的距离 */
|
||||||
|
Distance(other: Vector2): number {
|
||||||
|
return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算两个向量之间的平方距离,这个属性比 `Distance` 属性更快 */
|
||||||
|
SquaredDistance(other: Vector2): number {
|
||||||
|
return Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向量和 */
|
||||||
|
Add(other: Vector2): Vector2 {
|
||||||
|
return new Vector2(this.x + other.x, this.y + other.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向量差 */
|
||||||
|
Subtract(other: Vector2): Vector2 {
|
||||||
|
return new Vector2(this.x - other.x, this.y - other.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向量点乘 */
|
||||||
|
Dot(other: Vector2): number {
|
||||||
|
return this.x * other.x + this.y * other.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向量叉乘 */
|
||||||
|
Cross(other: Vector2): number {
|
||||||
|
return this.x * other.y - this.y * other.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向量与标量相乘 */
|
||||||
|
Multiply(scalar: number): Vector2 {
|
||||||
|
return new Vector2(this.x * scalar, this.y * scalar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向量归一化 */
|
||||||
|
Normalize(): Vector2 {
|
||||||
|
const magnitude = this.Magnitude;
|
||||||
|
if (magnitude === 0) {
|
||||||
|
return Vector2.Zero;
|
||||||
|
}
|
||||||
|
return new Vector2(this.x / magnitude, this.y / magnitude);
|
||||||
|
}
|
||||||
|
}
|
4528
src/processors/processCodeConverter/processCodeConverter.test.ts
Normal file
4528
src/processors/processCodeConverter/processCodeConverter.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
114
src/processors/processCodeConverter/processCodeConverter.ts
Normal file
114
src/processors/processCodeConverter/processCodeConverter.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { ConfigBase, ProcessorBase, ProcessorContext, IProcessingItem } from "cut-abstractions";
|
||||||
|
import { NcConverter } from "../NcConverter/NcConverter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加工代码转换器
|
||||||
|
*/
|
||||||
|
export class ProcessCodeConverterProc extends ProcessorBase<ProcessCodeConverterProcInput, ProcessCodeConverterProcOutput, ProcessCodeConverterProcConfig> {
|
||||||
|
get name(): string {
|
||||||
|
return 'ProcessCodeConverter';
|
||||||
|
}
|
||||||
|
get version(): string {
|
||||||
|
return '1.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 正在使用的解析器 */
|
||||||
|
private _usedConverter?: NcConverter;
|
||||||
|
exec(context: ProcessorContext<ProcessCodeConverterProcInput, ProcessCodeConverterProcOutput, ProcessCodeConverterProcConfig>): Promise<void> | void {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
resolve()
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精简指令 NC代码精简指令处理
|
||||||
|
* 兼容注释的行处理 精简指令只处理G代码所在的行
|
||||||
|
*/
|
||||||
|
handleNcSimpleCode(code: string) {
|
||||||
|
let arr = code.split('\n')
|
||||||
|
let objArr = arr.map((e, i) => {
|
||||||
|
let type = (!e.includes('//') && !e.includes('/**') || e.includes('"//')) ? 'code' : 'remark'
|
||||||
|
let temp = {
|
||||||
|
code: e,
|
||||||
|
index: i,
|
||||||
|
regList: e.match(/[XYZF]([-+]?\d*\.?\d+)/g) || [],
|
||||||
|
type
|
||||||
|
}
|
||||||
|
return temp
|
||||||
|
})
|
||||||
|
let checkLines = objArr.filter(e => e.type == 'code')
|
||||||
|
|
||||||
|
// 校验对象 存键值
|
||||||
|
let targetObject: any = {}
|
||||||
|
for (const key in checkLines) {
|
||||||
|
let line = checkLines[key]
|
||||||
|
if (line.regList.length > 0) {
|
||||||
|
for (const regVal of line.regList) {
|
||||||
|
let reg = regVal.match(/([XYZF])([-+]?\d*\.?\d+)/)
|
||||||
|
if (!reg) {
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let FullVal = reg[0]
|
||||||
|
let valKey = reg[1]
|
||||||
|
let val = reg[2]
|
||||||
|
if (Reflect.has(targetObject, valKey)) {
|
||||||
|
if (targetObject[valKey] == val) {
|
||||||
|
// 相同 需要精简操作
|
||||||
|
objArr[line.index].code = objArr[line.index].code.replace(FullVal, ``)
|
||||||
|
} else {
|
||||||
|
// 不同 更新校验的值
|
||||||
|
targetObject[valKey] = val
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 键值 没有 赋给 校验对象
|
||||||
|
Reflect.set(targetObject, valKey, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let newCode = objArr.map(e => e.code).join('\n')
|
||||||
|
// 这里 数据中莫名其妙多了个空格 处理下
|
||||||
|
let list = newCode.split(' ').filter(e => e != '')
|
||||||
|
newCode = list.join(' ')
|
||||||
|
return newCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProcessCodeConverterProcInput = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProcessCodeConverterProcOutput = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class ProcessCodeConverterProcConfig extends ConfigBase {
|
||||||
|
|
||||||
|
}
|
||||||
|
/** 加工流程类型 加工项与加工项直接的流程类型 */
|
||||||
|
export enum ProcessStepType {
|
||||||
|
/** 开料加工 */
|
||||||
|
blockProcess = "processBlock",
|
||||||
|
/** 修边加工 */
|
||||||
|
blockBoardProcess = "processBlockBoard",
|
||||||
|
/** 孔加工 */
|
||||||
|
holeProcess = "holeProcess",
|
||||||
|
/** 造型加工 */
|
||||||
|
modelProcess = "modelProcess",
|
||||||
|
/** 槽加工 */
|
||||||
|
grooveProcess = "grooveProcess"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单一加工项内的业务流程类型 */
|
||||||
|
export enum ProcessItemStepType {
|
||||||
|
/** 快速定位 --空程移动 */
|
||||||
|
knifeRapidMove = 'knifeRapidMove'
|
||||||
|
/** */
|
||||||
|
}
|
25
src/utils/corsWorker.ts
Normal file
25
src/utils/corsWorker.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export class CorsWorker {
|
||||||
|
private readonly url: string | URL;
|
||||||
|
private readonly options?: WorkerOptions;
|
||||||
|
|
||||||
|
// Have this only to trick Webpack into properly transpiling
|
||||||
|
// the url address from the component which uses it
|
||||||
|
constructor(url: string | URL, options?: WorkerOptions) {
|
||||||
|
this.url = url;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWorker(): Promise<Worker> {
|
||||||
|
if (import.meta.env.DEV) { // 修复调试模式
|
||||||
|
return new Worker(this.url, this.options);
|
||||||
|
}
|
||||||
|
const f = await fetch(this.url);
|
||||||
|
const t = await f.text();
|
||||||
|
const b = new Blob([t], {
|
||||||
|
type: 'application/javascript',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(b);
|
||||||
|
const worker = new Worker(url, this.options);
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
}
|
116
src/utils/helper.array.ts
Normal file
116
src/utils/helper.array.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { AssignByPath, Resolve, type Paths, type PathValue } from "./helper.lang";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将对象数组按指定字段分组,并返回分组结果的数组。
|
||||||
|
*
|
||||||
|
* @template T - 输入数组中对象的类型。
|
||||||
|
* @param data - 要分组的对象数组。
|
||||||
|
* @param fields - 用于分组对象的键(字段)数组。
|
||||||
|
* @param sort - 一个布尔值,指示是否按键对分组结果进行排序。默认为 `true`。
|
||||||
|
* @param separator - 用于连接字段值以形成分组键的字符串。默认为 `'-'`。
|
||||||
|
* @returns 分组结果的数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const data = [
|
||||||
|
* { category: 'A', type: 'X', value: 10 },
|
||||||
|
* { category: 'A', type: 'Y', value: 20 },
|
||||||
|
* { category: 'B', type: 'X', value: 30 },
|
||||||
|
* ];
|
||||||
|
* const grouped = GroupBy(data, ['category', 'type']);
|
||||||
|
* console.log(grouped);
|
||||||
|
* // 输出:
|
||||||
|
* // [
|
||||||
|
* // {
|
||||||
|
* // key: 'A-X',
|
||||||
|
* // keyList: ['A', 'X'],
|
||||||
|
* // value: [{ category: 'A', type: 'X', value: 10 }],
|
||||||
|
* // keyObj: { category: 'A', type: 'X' }
|
||||||
|
* // },
|
||||||
|
* // {
|
||||||
|
* // key: 'A-Y',
|
||||||
|
* // keyList: ['A', 'Y'],
|
||||||
|
* // value: [{ category: 'A', type: 'Y', value: 20 }],
|
||||||
|
* // keyObj: { category: 'A', type: 'Y' }
|
||||||
|
* // },
|
||||||
|
* // {
|
||||||
|
* // key: 'B-X',
|
||||||
|
* // keyList: ['B', 'X'],
|
||||||
|
* // value: [{ category: 'B', type: 'X', value: 30 }],
|
||||||
|
* // keyObj: { category: 'B', type: 'X' }
|
||||||
|
* // }
|
||||||
|
* // ]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function GroupBy<T extends object, TKeys extends (Paths<T>)[]>(
|
||||||
|
data: T[],
|
||||||
|
fields: TKeys,
|
||||||
|
sort = true,
|
||||||
|
separator = '-',
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const groupList = {} as any;
|
||||||
|
const groupArray = [] as IGrouping<T, { [Key in TKeys[number]]: PathValue<T, Key> }>[];
|
||||||
|
if (fields.length === 0) return [];
|
||||||
|
for (const item of data) {
|
||||||
|
const keyList = fields.map((field) => Resolve(item, field));
|
||||||
|
const key = keyList.join(separator);
|
||||||
|
groupList[key] = groupList[key] || [];
|
||||||
|
groupList[key].push(item);
|
||||||
|
groupList[key].keyList = keyList;
|
||||||
|
const keyObj = {} as T;
|
||||||
|
fields.forEach((t) => {
|
||||||
|
AssignByPath(keyObj, t, Resolve(item, t) as PathValue<T, typeof t>);
|
||||||
|
});
|
||||||
|
groupList[key].keyObj = keyObj;
|
||||||
|
}
|
||||||
|
for (const key in groupList) {
|
||||||
|
groupArray.push({
|
||||||
|
key,
|
||||||
|
keyList: groupList[key].keyList,
|
||||||
|
value: groupList[key],
|
||||||
|
keyObj: groupList[key].keyObj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sort)
|
||||||
|
return groupArray.sort(function (a, b) {
|
||||||
|
return a.key.localeCompare(b.key, 'zh-CN', { numeric: true });
|
||||||
|
});
|
||||||
|
else return groupArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将对象数组按指定字段分组,并返回分组结果的数组。
|
||||||
|
*/
|
||||||
|
export interface IGrouping<T, TKeyRecord> {
|
||||||
|
/**
|
||||||
|
* 分组的键,若有多个键,则用连接符连接
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* 分组的键列表
|
||||||
|
*/
|
||||||
|
keyList: unknown[];
|
||||||
|
/**
|
||||||
|
* 分组的对象数组
|
||||||
|
*/
|
||||||
|
value: T[];
|
||||||
|
/**
|
||||||
|
* 分组键对象,其属性名为分组键名,属性值为分组键
|
||||||
|
*/
|
||||||
|
keyObj: TKeyRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Array<T> {
|
||||||
|
GroupBy: <TKeys extends (Paths<T>)[]>(
|
||||||
|
fields: TKeys,
|
||||||
|
sort?: boolean,
|
||||||
|
separator?: string,
|
||||||
|
) => Array<IGrouping<T, { [Key in TKeys[number]]: PathValue<T, Key> }>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.GroupBy = function (fields, sort, separator) {
|
||||||
|
return GroupBy(this, fields, sort, separator);
|
||||||
|
};
|
21
src/utils/helper.async.ts
Normal file
21
src/utils/helper.async.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 异步等待
|
||||||
|
* @param ms 等待毫秒数
|
||||||
|
* @param value 等待完成后返回的值
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function AsyncDelay<T>(ms: number, value?: T) {
|
||||||
|
return new Promise<T>((resolve) => setTimeout(resolve, ms, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function AsyncWaitUntil(predicate: () => boolean, waitTime = 100) {
|
||||||
|
while (!predicate()) {
|
||||||
|
await AsyncDelay(waitTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function AsyncWaitForChange(obj: () => any, waitTime = 100) {
|
||||||
|
while (obj() === undefined) {
|
||||||
|
await AsyncDelay(waitTime);
|
||||||
|
}
|
||||||
|
}
|
128
src/utils/helper.lang.ts
Normal file
128
src/utils/helper.lang.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 检查枚举值中是否包含某个标志位
|
||||||
|
* @param $enum 枚举值
|
||||||
|
* @param flag 标志位
|
||||||
|
* @returns 是否包含标志位
|
||||||
|
*/
|
||||||
|
export function HasFlag($enum: number, ...flags: number[]) {
|
||||||
|
return flags.every(flag => ($enum & flag) === flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝
|
||||||
|
* @param obj 对象
|
||||||
|
* @returns 深拷贝后的对象
|
||||||
|
*/
|
||||||
|
export function Clone<T>(obj: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToPercent(num: number, precision = 2) {
|
||||||
|
return (num * 100).toFixed(precision) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 访问对象的成员 */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function Resolve<T extends object, TPath extends Paths<T>>(obj: T, path: TPath): PathValue<T, TPath> | T {
|
||||||
|
return path.split('.').reduce(function (item, key) {
|
||||||
|
return item ? (item as any)[key] : null;
|
||||||
|
}, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 根据字符串路径向对象深层嵌套的属性赋值。
|
||||||
|
* 如果路径中的对象不存在,则会自动创建。
|
||||||
|
* @param obj 要修改的对象。注意:这个函数会直接修改传入的对象(in-place mutation)。
|
||||||
|
* @param path 属性路径字符串,例如 "a.b.c"。
|
||||||
|
* @param value 要赋给目标属性的值。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const obj = { a: { b: { c: 1 } } };
|
||||||
|
* AssignByPath(obj, "a.b.c", 2);
|
||||||
|
* console.log(obj); // { a: { b: { c: 2 } } }
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function AssignByPath<T extends object, TPath extends Paths<T>>(obj: T, path: TPath, value: PathValue<T, TPath>) {
|
||||||
|
const keys = path.split(".");
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let current: any = obj;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let key = keys[i];
|
||||||
|
|
||||||
|
// 如果当前层级的对象不存在,或者不是一个对象,就需要创建它
|
||||||
|
// 这是一个关键步骤,确保路径是可写的
|
||||||
|
if (current[key] === undefined || typeof current[key] !== 'object' || current[key] === null) {
|
||||||
|
// 检查下一个键是否是数字,来决定创建对象还是数组。
|
||||||
|
// 这是一个启发式方法,对于像 'a.0.b' 这样的路径,它会创建 { a: [ { b: ... } ] }
|
||||||
|
// 注意:这个逻辑对于严格的类型安全来说是一个简化。
|
||||||
|
// 我们的 Paths<T> 类型目前不支持数字索引,所以这个逻辑是为了运行时健壮性。
|
||||||
|
const nextKey = keys[i + 1];
|
||||||
|
current[key] = /^\d+$/.test(nextKey) ? [] : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动到下一层
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
// 获取最后一个键,并进行赋值
|
||||||
|
const lastKey = keys[keys.length - 1];
|
||||||
|
current[lastKey] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查对象是否是构造函数的直接实例(不检查继承关系)
|
||||||
|
* @param obj 对象
|
||||||
|
* @param constructor 构造函数
|
||||||
|
*/
|
||||||
|
export function IsDirectInstanceOf(obj: object, constructor: Function) {
|
||||||
|
// 确保 obj 是一个对象,并且 Constructor 是一个函数/类
|
||||||
|
if (obj === null || typeof obj !== 'object' && typeof obj !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof constructor !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较对象的直接原型与构造函数的 prototype 属性
|
||||||
|
return Object.getPrototypeOf(obj) === constructor.prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 递归生成对象所有可能的属性访问路径。
|
||||||
|
* 类型T必须是object,不支持数组对象
|
||||||
|
* 例如:{ a: { b: string } } -> "a" | "a.b"
|
||||||
|
*/
|
||||||
|
export type Paths<T> = T extends object
|
||||||
|
? {
|
||||||
|
[K in keyof T]:
|
||||||
|
// K 本身是一个有效的路径
|
||||||
|
| `${K & string}`
|
||||||
|
// 如果 T[K] 也是一个对象,则递归地为其生成路径,并加上前缀 "K."
|
||||||
|
// 使用NonNullable来解决T[K]可能为null | undefined的情况
|
||||||
|
| (NonNullable<T[K]> extends object ?
|
||||||
|
// 对于数组类型的子对象,不进行解析,防止TS类型死循环
|
||||||
|
NonNullable<T[K]> extends any[] ? never
|
||||||
|
: `${K & string}.${NonNullable<Paths<T[K]>>}` : never);
|
||||||
|
}[keyof T] // 取出所有可能路径的联合类型
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 根据给定的路径字符串 P,在对象 T 中查找并返回对应值的类型。
|
||||||
|
* 例如:PathValue<{ a: { b: string } }, "a.b"> -> string
|
||||||
|
*/
|
||||||
|
export type PathValue<T, P extends string> =
|
||||||
|
// 使用条件类型和 infer 来分割路径字符串 P
|
||||||
|
P extends `${infer Key}.${infer Rest}`
|
||||||
|
? // 如果 Key 是 T 的一个键
|
||||||
|
Key extends keyof T
|
||||||
|
? // 则在 T[Key] 类型中递归查找剩余路径 Rest 的值类型
|
||||||
|
NonNullable<PathValue<T[Key], Rest>>
|
||||||
|
: never
|
||||||
|
: // 如果路径 P 不包含 ".", 说明是最后一层
|
||||||
|
P extends keyof T
|
||||||
|
? // 直接返回 T[P] 的类型
|
||||||
|
T[P]
|
||||||
|
: never;
|
33
src/utils/helper.string.ts
Normal file
33
src/utils/helper.string.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export function IsNullOrEmpty(str?: string | null) {
|
||||||
|
return str === null || str === undefined || str === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsNullOrWhitespace(str?: string | null) {
|
||||||
|
return str === null || str === undefined || str.trim() === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface String {
|
||||||
|
/** 将字符串转换为 camelCase 格式 */
|
||||||
|
ToCamelCase(): string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.ToCamelCase = function (this: string) {
|
||||||
|
return ToCamelCase(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串转换为 camelCase 格式
|
||||||
|
* @param str 输入字符串
|
||||||
|
* @returns camelCase 格式的字符串
|
||||||
|
*/
|
||||||
|
export function ToCamelCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/([-_][a-z])/g, group =>
|
||||||
|
group.toUpperCase()
|
||||||
|
.replace('-', '')
|
||||||
|
.replace('_', '')
|
||||||
|
)
|
||||||
|
.replace(/^[A-Z]/, firstChar => firstChar.toLowerCase());
|
||||||
|
}
|
35
src/utils/helper.version.ts
Normal file
35
src/utils/helper.version.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export class Version {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
build: number;
|
||||||
|
revision: number;
|
||||||
|
toString(): string {
|
||||||
|
return `${this.major}.${this.minor}.${this.build}.${this.revision}`
|
||||||
|
}
|
||||||
|
constructor(major: number, minor: number, build: number = 0, revision: number = 0) {
|
||||||
|
this.major = major;
|
||||||
|
this.minor = minor;
|
||||||
|
this.build = build;
|
||||||
|
this.revision = revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
compareTo(obj: Version): number {
|
||||||
|
if (this.major > obj.major) return 1;
|
||||||
|
if (this.major < obj.major) return -1;
|
||||||
|
if (this.minor > obj.minor) return 1;
|
||||||
|
if (this.minor < obj.minor) return -1;
|
||||||
|
if (this.build > obj.build) return 1;
|
||||||
|
if (this.build < obj.build) return -1;
|
||||||
|
if (this.revision > obj.revision) return 1;
|
||||||
|
if (this.revision < obj.revision) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVersion(verStr: string): Version {
|
||||||
|
const arr = verStr.split(".").map(c => parseInt(c));
|
||||||
|
if (arr.length < 2 || arr.length > 4)
|
||||||
|
throw new Error("无效的版本号");
|
||||||
|
|
||||||
|
return new Version(arr[0], arr[1], arr[2] || 0, arr[3] || 0);
|
||||||
|
}
|
14
src/utils/index.ts
Normal file
14
src/utils/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @package @mes-processors/libs/utils
|
||||||
|
* @author CZY
|
||||||
|
* @description 帮助类库,这个导出仅供内部使用,不在打包中暴露,在这个文件中使用导出时,不要在路径中使用'@',否则模块无法加载
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
export * from "./corsWorker";
|
||||||
|
export * from "./helper.array";
|
||||||
|
export * from "./helper.async";
|
||||||
|
export * from "./helper.lang";
|
||||||
|
export * from "./helper.string";
|
||||||
|
export * from "./helper.version";
|
||||||
|
export * from "./utilTypes";
|
27
src/utils/utilTypes.ts
Normal file
27
src/utils/utilTypes.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 读取泛型T中的KEY作为类型,支持嵌套属性
|
||||||
|
*/
|
||||||
|
export type NestedKey<T extends object> = {
|
||||||
|
[K in keyof T & string]: T[K] extends object ? `${K}` | `${K}.${NestedKey<T[K]>}` : `${K}`;
|
||||||
|
}[keyof T & string];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取泛型T中的VALUE作为类型,支持嵌套属性,你可能需要把类型T设为标量(`as const`)
|
||||||
|
*/
|
||||||
|
export type NestedValue<T extends object> = {
|
||||||
|
[K in keyof T & string]: T[K] extends object ? T[K] | NestedValue<T[K]> : T[K];
|
||||||
|
}[keyof T & string];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将泛型T中的属性P设置为必选
|
||||||
|
*/
|
||||||
|
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归地将泛型T的所有属性变为只读
|
||||||
|
*/
|
||||||
|
export type DeepReadonly<T> = T extends object
|
||||||
|
? {
|
||||||
|
readonly [P in keyof T]: DeepReadonly<T[P]>;
|
||||||
|
}
|
||||||
|
: T;
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
39
tsconfig.app.json
Normal file
39
tsconfig.app.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
// TODO: Warn: 在进行TS类型检查的时候会检查到workflow工作区中的文件,原因未知
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"ES2021",
|
||||||
|
"ESNext",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"noEmit": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
// 为了保证导入不与被引入项目冲突,不应该配置'@/*'别名
|
||||||
|
"@libs/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"erasableSyntaxOnly": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/__tests__/*",
|
||||||
|
"dist",
|
||||||
|
"node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
34
vite.config.ts
Normal file
34
vite.config.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/// <reference types="vitest/config" />
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
let basePath = process.env.basePath ?? '';
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
base: basePath,
|
||||||
|
plugins: [
|
||||||
|
nodePolyfills(),
|
||||||
|
dts({rollupTypes: true, tsconfigPath: './tsconfig.app.json',insertTypesEntry: true}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
modulePreload: {
|
||||||
|
resolveDependencies() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
|
name: 'MesProcessors',
|
||||||
|
fileName(format) {
|
||||||
|
return `mes-processors.${format}.js`
|
||||||
|
},
|
||||||
|
formats: ['es', 'umd', 'iife']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
|
||||||
|
},
|
||||||
|
})
|
Reference in New Issue
Block a user