提交:1、导XML格式的CNC 2、NCwriter功能填充

This commit is contained in:
2025-09-03 17:24:28 +08:00
commit f4bb32d32a
30 changed files with 12398 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
mes-processors-libs-*.tgz
node_modules
dist/*

111
README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

68
src/cncTest.test.ts Normal file
View 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
View File

@@ -0,0 +1,5 @@
// processors
export * from "./processors/processCodeConverter/processCodeConverter";
export * from "./processors/NcConverter/NcConverter"

2
src/modules/README.md Normal file
View File

@@ -0,0 +1,2 @@
### react-layout
矩形优化算法(陈总新优化)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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);
});

View 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;
}

View 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];
}

View 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
}

View File

@@ -0,0 +1,5 @@
import { test } from "vitest";
test('NcConverterTest',()=>{
})

View 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);
}
}

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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
View 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;

View 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());
}

View 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
View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

39
tsconfig.app.json Normal file
View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View 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
View 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'] : [],
},
})