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 = [] /** 当前刀具 */ 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 = {}; 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) { 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(code: TCode, params: Partial): void { switch (code) { case 'G0': case 'G1': case 'G2': case 'G3': { this.code(code, params); break; } // 自定义代码 case 'CArc': { // 凸度转GCode const cParam = params as Partial; 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); } }