From adc659ff5e7351cac90a92a7e2b5d1e432c936cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AD=90=E6=B6=B5?= <2067519648@qq.com> Date: Thu, 4 Jul 2024 07:55:19 +0000 Subject: [PATCH] =?UTF-8?q?!2710=20=E5=8A=9F=E8=83=BD:=20=E9=92=88?= =?UTF-8?q?=E5=AF=B9=E5=9C=86=E5=BC=A7=E6=9D=BF=E7=9A=84=E7=BA=BF=E6=80=A7?= =?UTF-8?q?=E5=88=87=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __test__/Booloperate/ArcBoardCutting.test.ts | 120 ++ src/Add-on/BoardCutting/LinearCutting.ts | 99 +- .../BoardCutting/LinearCuttingForSweep.ts | 1059 +++++++++++++++++ src/Add-on/BoardCutting/SplitPolyline.ts | 11 +- 4 files changed, 1278 insertions(+), 11 deletions(-) create mode 100644 __test__/Booloperate/ArcBoardCutting.test.ts create mode 100644 src/Add-on/BoardCutting/LinearCuttingForSweep.ts diff --git a/__test__/Booloperate/ArcBoardCutting.test.ts b/__test__/Booloperate/ArcBoardCutting.test.ts new file mode 100644 index 000000000..854d2270d --- /dev/null +++ b/__test__/Booloperate/ArcBoardCutting.test.ts @@ -0,0 +1,120 @@ +import { LinearCuttingForSweep, ViewManager, ViewType } from "../../src/Add-on/BoardCutting/LinearCuttingForSweep"; +import { Polyline } from "../../src/DatabaseServices/Entity/Polyline"; +import { LoadBoardsFromFileData, LoadEntityFromFileData } from "../Utils/LoadEntity.util"; +import "../Utils/jest.util"; + +// 测试命令(用于复制):npm run test -- ArcBoardCutting.test.ts + +/** 批量测试 */ +function TestAll() +{ + /** 测试集 */ + const testSet = new Set([ + { + question: { + brData: { "file": [1, "Board", 10, 2, 101, 0, 1, 2, 71, [-1, 6.123233995736766e-17, 0, 0, -6.123233995736766e-17, -1, 2.4492935982947064e-16, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, [6.123233995736766e-17, 1, -2.4492935982947064e-16, 0, -1, 6.123233995736766e-17, 0, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, 3, 600, 1400, 18, true, "Polyline", 10, 2, 0, 0, 0, 7, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [1400, 0], 0, [1400, 600], 0, [0, 600], 0, true, 0, 3, 0, 0, 0, 0, 0, 19, 0, "层板", "", "", "", "", "", 0, 0, "三合一", 2, 0, "1", "1", "1", "1", "", "", "", 4, "三合一", "三合一", "三合一", "三合一", true, true, 0, 0, 0, 0, 0, 0, 0, 0, true, 0, 0, null, 0, 0, "", "", "", "", 0, false, 0, "Polyline", 10, 2, 0, 0, 0, 8, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1829.130340136119, -3186.5844897958577, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [600.0000000000002, 0], -0.9999999999999999, [600.0000000000002, -400], 0, [484.8671984825753, -400], 0, false, 0, 0, true, 2, -1, 0, 6, 6, 2, 0, 0, 0, 3, 0, 1, 628.31853, 6, 6, 2, 0, 0, 0, 3, 0], "basePt": { "x": 2313.3906173705936, "y": 938.6530296283928, "z": -1418 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + plData: { "file": [1, "Polyline", 10, 2, 273, 0, 1, 1, 0, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 2, [2008.0513946721885, 1238.653029628393], 0, [3469.88022397831, 1238.653029628393], 0, false], "basePt": { "x": 2008.0513946721885, "y": 1238.653029628393, "z": 0 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + viewStr: ViewType.Down + }, + answer: { + count: 2, + areas: [420000, 420000], + lengths: [1343.4513322353837, 1343.4513322353837] + } + }, + { + question: { + brData: { "file": [1, "Board", 10, 2, 101, 0, 1, 2, 71, [-1, 6.123233995736766e-17, 0, 0, -6.123233995736766e-17, -1, 2.4492935982947064e-16, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, [6.123233995736766e-17, 1, -2.4492935982947064e-16, 0, -1, 6.123233995736766e-17, 0, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, 3, 600, 1400, 18, true, "Polyline", 10, 2, 0, 0, 0, 7, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [1400, 0], 0, [1400, 600], 0, [0, 600], 0, true, 0, 3, 0, 0, 0, 0, 0, 19, 0, "层板", "", "", "", "", "", 0, 0, "三合一", 2, 0, "1", "1", "1", "1", "", "", "", 4, "三合一", "三合一", "三合一", "三合一", true, true, 0, 0, 0, 0, 0, 0, 0, 0, true, 0, 0, null, 0, 0, "", "", "", "", 0, false, 0, "Polyline", 10, 2, 0, 0, 0, 8, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1829.130340136119, -3186.5844897958577, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [600.0000000000002, 0], -0.9999999999999999, [600.0000000000002, -400], 0, [484.8671984825753, -400], 0, false, 0, 0, true, 2, -1, 0, 6, 6, 2, 0, 0, 0, 3, 0, 1, 628.31853, 6, 6, 2, 0, 0, 0, 3, 0], "basePt": { "x": 2313.3906173705936, "y": 938.6530296283928, "z": -1418 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + plData: { "file": [1, "Polyline", 10, 2, 226, 0, 1, 7, 0, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 2, [2980.2949542257415, 1785.8711020734017], 0, [2779.229252050603, 693.9296733376492], 0, false], "basePt": { "x": 2779.229252050603, "y": 693.9296733376492, "z": 0 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + viewStr: ViewType.Down + }, + answer: { + count: 2, + areas: [151114.91863862606, 688885.0813613739], + lengths: [307.09899093217655, 1146.8339277054733] + } + }, + { + question: { + brData: { "file": [1, "Board", 10, 2, 101, 0, 1, 2, 71, [-1, 6.123233995736766e-17, 0, 0, -6.123233995736766e-17, -1, 2.4492935982947064e-16, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, [6.123233995736766e-17, 1, -2.4492935982947064e-16, 0, -1, 6.123233995736766e-17, 0, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, 3, 600, 1400, 18, true, "Polyline", 10, 2, 0, 0, 0, 7, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [1400, 0], 0, [1400, 600], 0, [0, 600], 0, true, 0, 3, 0, 0, 0, 0, 0, 19, 0, "层板", "", "", "", "", "", 0, 0, "三合一", 2, 0, "1", "1", "1", "1", "", "", "", 4, "三合一", "三合一", "三合一", "三合一", true, true, 0, 0, 0, 0, 0, 0, 0, 0, true, 0, 0, null, 0, 0, "", "", "", "", 0, false, 0, "Polyline", 10, 2, 0, 0, 0, 8, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1829.130340136119, -3186.5844897958577, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [600.0000000000002, 0], -0.9999999999999999, [600.0000000000002, -400], 0, [484.8671984825753, -400], 0, false, 0, 0, true, 2, -1, 0, 6, 6, 2, 0, 0, 0, 3, 0, 1, 628.31853, 6, 6, 2, 0, 0, 0, 3, 0], "basePt": { "x": 2313.3906173705936, "y": 938.6530296283928, "z": -1418 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + plData: { "file": [1, "Polyline", 10, 2, 229, 0, 1, 7, 0, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 2, [2423.497625125358, 1699.2581842133418], 0, [2494.643950510407, 734.1428137726766], 0, false], "basePt": { "x": 2423.497625125358, "y": 734.1428137726766, "z": 0 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + viewStr: ViewType.Down + }, + answer: { + count: 3, + areas: [405352.90503024973, 320214.5090290456, 114432.58594070456], + lengths: [691.2561564438456, 532.3117909155627, 206.38895796127028] + } + }, + { + question: { + brData: { "file": [1, "Board", 10, 2, 101, 0, 1, 2, 71, [-1, 6.123233995736766e-17, 0, 0, -6.123233995736766e-17, -1, 2.4492935982947064e-16, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, [6.123233995736766e-17, 1, -2.4492935982947064e-16, 0, -1, 6.123233995736766e-17, 0, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, 3, 600, 1400, 18, true, "Polyline", 10, 2, 0, 0, 0, 7, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [1400, 0], 0, [1400, 600], 0, [0, 600], 0, true, 0, 3, 0, 0, 0, 0, 0, 19, 0, "层板", "", "", "", "", "", 0, 0, "三合一", 2, 0, "1", "1", "1", "1", "", "", "", 4, "三合一", "三合一", "三合一", "三合一", true, true, 0, 0, 0, 0, 0, 0, 0, 0, true, 0, 0, null, 0, 0, "", "", "", "", 0, false, 0, "Polyline", 10, 2, 0, 0, 0, 8, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1829.130340136119, -3186.5844897958577, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [600.0000000000002, 0], -0.9999999999999999, [600.0000000000002, -400], 0, [484.8671984825753, -400], 0, false, 0, 0, true, 2, -1, 0, 6, 6, 2, 0, 0, 0, 3, 0, 1, 628.31853, 6, 6, 2, 0, 0, 0, 3, 0], "basePt": { "x": 2313.3906173705936, "y": 938.6530296283928, "z": -1418 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + plData: { "file": [1, "Polyline", 10, 2, 232, 0, 1, 7, 71, [0, -1, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 2, [-1287.5623052437588, -738.1797530758489], 0, [-1129.9916386341743, -1642.7520984271673], 0, false], "basePt": { "x": 0, "y": 1129.9916386341743, "z": -1642.7520984271673 }, "ucs": [0, -1, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 1] }, + viewStr: ViewType.Left + }, + answer: { + count: 2, + areas: [445744.72443803976, 394255.27556196053], + lengths: [1343.4513322353837, 1343.4513322353837] + } + }, + { + question: { + brData: { "file": [1, "Board", 10, 2, 101, 0, 1, 2, 71, [-1, 6.123233995736766e-17, 0, 0, -6.123233995736766e-17, -1, 2.4492935982947064e-16, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, [6.123233995736766e-17, 1, -2.4492935982947064e-16, 0, -1, 6.123233995736766e-17, 0, 0, 1.4997597826618576e-32, 2.4492935982947064e-16, 1, 0, 3131.3906173705936, 1538.653029628393, -1000, 1], 0, 0, 1, 3, 600, 1400, 18, true, "Polyline", 10, 2, 0, 0, 0, 7, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [1400, 0], 0, [1400, 600], 0, [0, 600], 0, true, 0, 3, 0, 0, 0, 0, 0, 19, 0, "层板", "", "", "", "", "", 0, 0, "三合一", 2, 0, "1", "1", "1", "1", "", "", "", 4, "三合一", "三合一", "三合一", "三合一", true, true, 0, 0, 0, 0, 0, 0, 0, 0, true, 0, 0, null, 0, 0, "", "", "", "", 0, false, 0, "Polyline", 10, 2, 0, 0, 0, 8, 71, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1829.130340136119, -3186.5844897958577, 0, 1], 0, 0, 1, 2, 4, [0, 0], 0, [600.0000000000002, 0], -0.9999999999999999, [600.0000000000002, -400], 0, [484.8671984825753, -400], 0, false, 0, 0, true, 2, -1, 0, 6, 6, 2, 0, 0, 0, 3, 0, 1, 628.31853, 6, 6, 2, 0, 0, 0, 3, 0], "basePt": { "x": 2313.3906173705936, "y": 938.6530296283928, "z": -1418 }, "ucs": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, + plData: { "file": [1, "Polyline", 10, 2, 262, 0, 1, 7, 0, [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1], 0, 0, 1, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 0, 0, 1, 2, 2, [2431.3906173705936, -832.5194311579985], 0, [2431.3906173705936, -1547.4552636415062], 0, false], "basePt": { "x": 2431.3906173705936, "y": 0, "z": -1547.4552636415062 }, "ucs": [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1] }, + viewStr: ViewType.Front + }, + answer: { + count: 3, + areas: [422332.63627688476, 286255.04653577565, 131412.31718733965], + lengths: [695.3098414019644, 437.6988479140293, 210.44264291938927] + } + } + ]); + // 遍历测试集 + testSet.forEach(e => + { + TestOne(e.question.brData, e.question.plData, e.question.viewStr, e.answer.count, e.answer.areas, e.answer.lengths); + }); +} +TestAll(); + +/** 测试单个案例 + * @param brData 板数据 + * @param plData 刀数据 + * @param viewStr 视图类型 + * @param count 切割后的数量 + * @param areas 板轮廓面积 + * @param lengths 放样路径长度 + */ +function TestOne(brData: Record, plData: Record, viewType: ViewType, count: number, areas: number[], lengths: number[]) +{ + /** 视图 */ + const viewMatrix = ViewManager.GetViewMatrix(viewType); + /** 肉 */ + const br = LoadBoardsFromFileData(brData)[0]; + br.ArcBuild.ParseSweepCurves(); + /** 刀 */ + const splitPl = LoadEntityFromFileData(plData)[0] as Polyline; + /** 切割器 */ + const linearCuttingForSweep = new LinearCuttingForSweep(br, splitPl); + // 单元测试 + test(`切割圆弧板-${viewType}`, () => + { + const [cus, sweepPaths] = linearCuttingForSweep.Split(viewMatrix); + // 板轮廓 + expect(cus.length).toBe(count); + for (let i = 0; i < cus.length; i++) + { + const cu = cus[i]; + expect(cu.Area).toBeCloseTo(areas[i], 0.1); + } + // 放样路径 + expect(sweepPaths.length).toBe(count); + for (let i = 0; i < sweepPaths.length; i++) + { + const sweepPath = sweepPaths[i]; + expect(sweepPath.Length).toBeCloseTo(lengths[i], 0.1); + } + }); +} diff --git a/src/Add-on/BoardCutting/LinearCutting.ts b/src/Add-on/BoardCutting/LinearCutting.ts index 6fe1a7e01..20115ec53 100644 --- a/src/Add-on/BoardCutting/LinearCutting.ts +++ b/src/Add-on/BoardCutting/LinearCutting.ts @@ -1,12 +1,12 @@ import { Intent } from "@blueprintjs/core"; -import { Line3, Matrix4, Vector3 } from "three"; +import { Line3, Matrix4, Quaternion, Vector3 } from "three"; import { app } from "../../ApplicationServices/Application"; import { arrayLast, arrayRemoveIf } from "../../Common/ArrayExt"; import { AlignLineGroupRecord } from "../../DatabaseServices/AlignLine/AlignLineGroupRecord"; import { Contour } from "../../DatabaseServices/Contour"; import { Board, I2DModeling } from "../../DatabaseServices/Entity/Board"; import { ExtrudeContourCurve } from "../../DatabaseServices/Entity/Extrude"; -import { Polyline } from "../../DatabaseServices/Entity/Polyline"; +import { BUL_IS_LINE_FUZZ, Polyline } from "../../DatabaseServices/Entity/Polyline"; import { HardwareCompositeEntity } from "../../DatabaseServices/Hardware/HardwareCompositeEntity"; import { Command } from "../../Editor/CommandMachine"; import { PromptStatus } from "../../Editor/PromptResult"; @@ -15,6 +15,7 @@ import { AsVector2, equaln, isParallelTo } from "../../Geometry/GeUtils"; import { PlaneExt } from "../../Geometry/Plane"; import { IntersectOption } from "../../GraphicsSystem/IntersectWith"; import { AppToaster } from "../../UI/Components/Toaster"; +import { LinearCuttingForSweep } from "./LinearCuttingForSweep"; import { SplitPolyline } from "./SplitPolyline"; export class LinearCutting implements Command @@ -56,12 +57,22 @@ export class LinearCutting implements Command let ucsInv = app.Editor.UCSMatrixInv; let ucs = app.Editor.UCSMatrix; + /** 刀(多段线) */ + const splitPl = LinearCuttingForSweep.CreateSplitPl(pts); let ptsWcs = pts;//缓存世界坐标系,支持UCS不同时使用捕捉. pts = pts.map(p => p.clone().applyMatrix4(ucsInv).setZ(0).applyMatrix4(ucs)); for (let br of brs) { + /** 切割后的板轮廓 */ + let cus: Polyline[] = []; + /** 切割后的放样路径 */ + let sweepPaths: Polyline[] = []; + /** 用于修正轮廓位置的偏移值 */ + let contourBiases: { x: number; y: number; }[] = []; + const sweepPath = br.GetSweepPath(); + // 原本的处理方式(针对普通板) let brNormal = br.Normal; let isVertical = equaln(brNormal.dot(ucsDir), 0); let brOcs = br.OCS; @@ -97,7 +108,8 @@ export class LinearCutting implements Command } } if (!bHasInterset) - continue; + if (!sweepPath) + continue;; } } else @@ -143,21 +155,82 @@ export class LinearCutting implements Command fix2dPath.SetBoardPath2d(br); br.HandleSpliteEntitys(splitBoards); - continue; + if (!sweepPath) + continue; } - - let cus = SplitPolyline(brContour, clipPls); + // 针对圆弧板进行特殊处理 + if (sweepPath) + { + const linearCuttingForSweep = new LinearCuttingForSweep(br, splitPl); + [cus, sweepPaths, contourBiases] = linearCuttingForSweep.Split(ucs, clipPls); + } + else + cus = SplitPolyline(brContour, clipPls); if (cus.length === 0) continue; - this.SplitBoard(br, cus); + this.SplitBoard(br, cus, sweepPaths, contourBiases); } app.Editor.UCSMatrix = oldUcs; } - //根据轮廓曲线分裂板件 - protected SplitBoard(br: Board, cus: ExtrudeContourCurve[]) + /** 针对圆弧板的分裂处理 */ + private ProcessForSweepInSplitBoard(br: Board, SweepPaths: Polyline[], contourBiases: { x: number, y: number; }[], index = 0) + { + // 针对圆弧板-修正板的位置,板的Position采用contourBiases中的偏移值 + if (contourBiases.length > 0) + { + const bias = contourBiases[index]; + const ocs = new Matrix4().extractRotation(br.OCS); + const v = new Vector3(bias.x, bias.y, 0).applyMatrix4(ocs); + br.Move(v); + } + // 针对圆弧板-对放样路径进行切割 + if (SweepPaths.length > 0) + { + const sweepPath = SweepPaths[index] || SweepPaths[0]; + // 若放样路径是一条直线,那就不再是圆弧板了 + if (sweepPath.LineData.length === 2 && equaln(sweepPath.LineData[0].bul, BUL_IS_LINE_FUZZ)) + { + const sweepAngle = br.SweepAngle; + const sweepPathInWCS = sweepPath.Clone().ApplyMatrix(br.GetSweepPathInWCS().OCS); + // 变成普通板(之后坐标系会错乱,故需要修正) + br.ClearSweepPath(); + // 平移修正 + const startPoint = sweepPath.StartPoint; + sweepPath.Move(new Vector3(-startPoint.x, -startPoint.y, 0)); + const rotateMatrix = new Matrix4().extractRotation(br.OCS); + const biasV = new Vector3(startPoint.x, 0, startPoint.y).applyMatrix4(rotateMatrix); + const biasMatrix = new Matrix4().makeTranslation(biasV.x, biasV.y, biasV.z); + br.ApplyMatrix(biasMatrix); + // 旋转修正 + const brStartPoint = new Vector3(0, 0, 0).applyMatrix4(br.OCS); + let brEndPoint = new Vector3(br.Width, 0, 0).applyMatrix4(br.OCS); + // 针对放样角度的处理 + if (equaln(sweepAngle, Math.PI / 2)) + brEndPoint = new Vector3(0, br.Height, 0).applyMatrix4(br.OCS); + const v1 = brEndPoint.clone().sub(brStartPoint).normalize(); + const v2 = sweepPathInWCS.EndPoint.clone().sub(sweepPathInWCS.StartPoint).normalize(); + const angle = v1.angleTo(v2); + const rotationAxis = v1.cross(v2).normalize(); + const quaternion = new Quaternion().setFromAxisAngle(rotationAxis, angle); + const angleMatrix = new Matrix4().makeRotationFromQuaternion(quaternion); + const moveMatrix = new Matrix4().makeTranslation(br.Position.x, br.Position.y, br.Position.z); + br.OCS = rotateMatrix.premultiply(angleMatrix).premultiply(moveMatrix); + } + else + br.SetSweepPath(sweepPath, br.SweepAngle); + } + } + + /** 根据轮廓曲线分裂板件 + * @param br 被切割的板件 + * @param cus 切割后的轮廓曲线 + * @param SweepPaths 切割后的放样路径(针对圆弧板) + * @param contourBiases 用于修正轮廓位置的偏移值(针对圆弧板) + */ + protected SplitBoard(br: Board, cus: ExtrudeContourCurve[], SweepPaths: Polyline[], contourBiases: { x: number, y: number; }[]) { let spliteEnts: Board[] = []; @@ -172,6 +245,10 @@ export class LinearCutting implements Command let br2 = br.Clone(); br2.Template = br.Template; // 切割后 模板跟随 br2.ContourCurve = cus[i]; + + // 针对圆弧板的特殊处理 + this.ProcessForSweepInSplitBoard(br2, SweepPaths, contourBiases, i); + app.Database.ModelSpace.Append(br2); br2.RepairRelevance(); br2.GrooveCheckAllAutoSplit(); @@ -193,6 +270,10 @@ export class LinearCutting implements Command } br.ContourCurve = cus[0]; + + // 针对圆弧板的特殊处理 + this.ProcessForSweepInSplitBoard(br, SweepPaths, contourBiases); + br.HandleSpliteEntitys(spliteEnts); br.GrooveCheckAllAutoSplit(); diff --git a/src/Add-on/BoardCutting/LinearCuttingForSweep.ts b/src/Add-on/BoardCutting/LinearCuttingForSweep.ts new file mode 100644 index 000000000..129286d2b --- /dev/null +++ b/src/Add-on/BoardCutting/LinearCuttingForSweep.ts @@ -0,0 +1,1059 @@ +import { Box3, Matrix4, Vector2, Vector3 } from "three"; +import { ComputerCurvesNormalOCS } from "../../Common/CurveUtils"; +import { Arc } from "../../DatabaseServices/Entity/Arc"; +import { Board } from "../../DatabaseServices/Entity/Board"; +import { Entity } from "../../DatabaseServices/Entity/Entity"; +import { Line } from "../../DatabaseServices/Entity/Line"; +import { Polyline, PolylineProps } from "../../DatabaseServices/Entity/Polyline"; +import { Box3Ext } from "../../Geometry/Box"; +import { GetArcDrawCount } from "../../Geometry/ExtrudeMeshGeomBuilder/SplitCurveParams"; +import { AsVector2, AsVector3, equaln, equalv3 } from "../../Geometry/GeUtils"; +import { IntersectOption } from "../../GraphicsSystem/IntersectWith"; +import { ArcBoardBuild } from "../ArcBoard/ArcBoardBuild"; +import { FaceDirection } from "../DrawDrilling/DrillType"; +import { BoxLine } from "../testEntity/BoxLine"; +import { SplitPolyline } from "./SplitPolyline"; + +/** 针对圆弧板的线性切割 */ +export class LinearCuttingForSweep +{ + /** 点集生成二维刀 */ + static CreateSplitPl(pts: Vector3[]) + { + /** 刀(线段簇) */ + const splitLines: Line[] = CurveManager.PtsToLines(pts); + /** 刀(多段线) */ + const splitPl = CurveManager.LinesToPl(splitLines); + return splitPl; + }; + + /** 生成圆弧板的切割器*/ + constructor(public br: Board, public splitPl: Polyline) + { + this.br = br; + this.splitPl = splitPl.Clone(); + } + + /** + * 进行切割 + * @param {Polyline[]} clipPls 用于切割的刀 + * @returns {[Polyline[], Polyline[], {x:number; y:number}[]]} [切割后轮廓, 切割后路径, 切割后轮廓的偏移值] + * @第一步 判断当前切割的视图类型 + * @第二步 根据视图类型判断切割的是路径还是面 + * @第三步 把被切割对象(路径与板包围盒)平移到对应视图的平面 + * @第四步 旋转被切割对象到XOY平面 + * @第五步 在XOY平面下进行切割 + */ + Split(ucs: Matrix4, clipPls?: Polyline[]): [Polyline[], Polyline[], { x: number; y: number; }[]] + { + /** 当前切割的视图类型 */ + const viewStr = this.GetViewStr(ucs); + if (viewStr === ViewType.Normal && clipPls && clipPls.length > 0) + { + // 暂时采用旧的切割方案 + return this.SplitBySweep(this.br, clipPls); + } + const isPathOrPlane = this.IsPathOrPlane(viewStr); + if (isPathOrPlane === "路径") + { + // 平移 + const sweepPathInWCS = this.MovePathToView(viewStr); + // 切割 + return this.SplitInPath(sweepPathInWCS); + } + if (isPathOrPlane === "面") + { + // 统一 + const brBox = this.UnifyBrBoxAndPathBox(); + // 平移 + const sweepPathInWCS = this.MovePathToView(viewStr); + const brBoxLines = this.MoveBrBoxToView(viewStr, brBox); + // 旋转 + const pathBoxPl = this.RotateToXOY(sweepPathInWCS, brBoxLines, new Matrix4().getInverse(ucs)); + // 切割 + return this.SplitInPlane(sweepPathInWCS, brBoxLines, pathBoxPl); + } + // 其他情况返回空数组,表示不做切割 + return [[], [], []]; + } + + /** + * 切割面 + * @param {Polyline} sweepPathInWCS 放样路径 + * @param {Line[]} brBoxLines 板包围盒 + * @param {Polyline} pathBoxPl 路径包围盒 + * @第一步 计算刀与板包围盒的交点,以及在盒内的顶点,记住采样点 + * @第二步 记录采样点在刀上的位置,并移动到路径所在的平面 + * @第三步 根据采样点生成垂直于XOY平面的直线 + * @第四步 计算这些直线与路径的交点 + * @第五步 将这些交点映射到展开后的路径上 + * @第六步 根据记录的位置排序这些点,并生成新的刀 + */ + SplitInPlane(sweepPathInWCS: Polyline, brBoxLines: Line[], pathBoxPl: Polyline) + { + const splitPl = this.splitPl; + /** 采样点(一定在包围盒内) */ + const ptsInbrBox = this.GetPtsInbrBox(brBoxLines); + /** 路径所在平面与X轴垂直否 */ + const isVerticalWithX = equaln(sweepPathInWCS.Normal.dot(new Vector3(1, 0, 0)), 0); + // 细化 + ptsInbrBox.push(...this.Tessellate(sweepPathInWCS, brBoxLines, isVerticalWithX)); + // 规范超出的部分 + const [yLineMax, yLineMin] = this.AdjustOverBox(sweepPathInWCS, pathBoxPl); + /** 构成新刀的数据集 */ + const splitDatasList: { pt: Vector3; index: number; }[][] = []; + /** 路径上的点(用于偏移后求交) */ + const pathPt = pathBoxPl.GetPointAtParam(0); + /** 展开后的路径 */ + const expandPath = CurveManager.CreateExpandPl(this.GetSweepPathInOCSWithBr()); + this.ToSplitPosition(expandPath); + let minLength = Infinity; + const inptsData: { pts: Vector3[], index: number, bias: number; }[] = []; + // 移动到路径所在的平面,然后作垂线并与路径求交 + for (const ptInbrBox of ptsInbrBox) + { + /** 在刀上的位置 */ + const ptInbrBoxIndex = splitPl.GetParamAtPoint(ptInbrBox); + let biasX = ptInbrBox.x - pathPt.x; + // 往刀所在的平面移动 + if (isVerticalWithX) + { + biasX = ptInbrBox.y - pathPt.y; + ptInbrBox.y = pathPt.y; + } + else + ptInbrBox.x = pathPt.x; + const zLine = CurveManager.CreateZLine(ptInbrBox); + const inptsInPath = zLine.IntersectWith(sweepPathInWCS, IntersectOption.ExtendThis, 0.01).reverse(); + minLength = Math.min(minLength, inptsInPath.length); + inptsData.push({ pts: inptsInPath, index: ptInbrBoxIndex, bias: biasX }); + } + for (const data of inptsData) + { + // 点映射 + for (let i = 0; i < minLength; i++) + { + const inptInPath = data.pts[i]; + const param = sweepPathInWCS.GetParamAtPoint(inptInPath); + const ptInExpand = expandPath.GetPointAtParam(param); + // 偏移 + ptInExpand.y += Math.abs(data.bias); + // 初始化 + if (!splitDatasList[i]) splitDatasList[i] = []; + // 添加 + splitDatasList[i].push({ pt: ptInExpand, index: data.index }); + } + } + for (let splitDatas of splitDatasList) + { + splitDatas = splitDatas.sort((a, b) => a.index - b.index); + // 修正刀 + for (let i = 0; i < splitDatas.length; i++) + { + const splitData = splitDatas[i]; + const basePt = splitData.pt; + const backPt = splitDatas[i - 1]?.pt; + const nextPt = splitDatas[i + 1]?.pt; + /** @todo 这里先按水平延伸来修正 */ + if (yLineMax.PtOnCurve(basePt)) + { + let sign = 1; + if (backPt) sign = Math.sign(basePt.x - backPt.x); + else if (nextPt) sign = Math.sign(basePt.x - nextPt.x); + const pt = basePt.clone().add(new Vector3(sign * 2000, 0, 0)); + const index = splitData.index + ((backPt && !yLineMax.PtOnCurve(backPt)) ? 0.01 : -0.01); + splitDatas.push({ pt, index }); + } + else if (yLineMin.PtOnCurve(basePt)) + { + let sign = 1; + if (backPt) sign = Math.sign(basePt.x - backPt.x); + else if (nextPt) sign = Math.sign(basePt.x - nextPt.x); + const pt = basePt.clone().add(new Vector3(sign * 2000, 0, 0)); + const index = splitData.index + ((backPt && !yLineMin.PtOnCurve(backPt)) ? 0.01 : -0.01); + splitDatas.push({ pt, index }); + } + } + // 排序 + splitDatas = splitDatas.sort((a, b) => a.index - b.index); + } + /** 新刀 */ + const newSplitPls: Polyline[] = []; + for (const splitDatas of splitDatasList) + { + const newSplitPl = CurveManager.CreatePolyline(splitDatas.map(e => AsVector2(e.pt))); + newSplitPls.push(newSplitPl); + } + // 切割板 + return this.SplitBrByKnifes(newSplitPls); + } + + /** 判断视图类型 */ + private GetViewStr(ucs: Matrix4) + { + const viewType = ViewManager.MatrixIsView(ucs); + if (viewType === ViewType.Unknown) + { + const axisUsc = CurveManager.CreateAxis(ucs); + const axisBr = CurveManager.CreateAxis(this.br.OCS); + const brZArrow = axisBr[2]; + const brZAxis = brZArrow.EndPoint.clone().sub(brZArrow.StartPoint); + const uscZArrow = axisUsc[2]; + const uscZAxis = uscZArrow.EndPoint.clone().sub(uscZArrow.StartPoint); + const isInPositive = equaln(brZAxis.angleTo(uscZAxis), 0) || equaln(brZAxis.angleTo(uscZAxis), Math.PI); + if (isInPositive) return "正视图"; + } + return viewType; + } + + /** 判断切割的是路径还是面 */ + private IsPathOrPlane(viewStr: ViewType | "正视图") + { + /** 放样路径的包围盒 */ + const sweepBoxInWCS = this.br.GetSweepPathInWCS().BoundingBox; + // 前视图下没有Y,说明切割的就直接是路径 + if (viewStr === ViewType.Front) + return equaln(sweepBoxInWCS.min.y, sweepBoxInWCS.max.y) ? "路径" : "面"; + // 左视图下没有X,说明切割的就直接是路径 + if (viewStr === ViewType.Left) + return equaln(sweepBoxInWCS.min.x, sweepBoxInWCS.max.x) ? "路径" : "面"; + // 俯视图下没有Z,说明切割的就直接是路径 + if (viewStr === ViewType.Down) + return equaln(sweepBoxInWCS.min.z, sweepBoxInWCS.max.z) ? "路径" : "面"; + return undefined; + } + + /** 获取调整角度(用于处理放样角度带来的影响) */ + private GetAdjustAngle() + { + /** @todo 角度 < 0 或 角度 > 90 会有问题 */ + const sweepAngle = this.br.SweepAngle; + /** 调整的角度 */ + let angle = sweepAngle; + if (equaln(sweepAngle, Math.PI)) + angle = 0; + else if (equaln(sweepAngle, -Math.PI / 2)) + angle = 0; + else if (sweepAngle > Math.PI / 4) + angle = sweepAngle - Math.PI / 2; + return angle; + } + + /** 统一板包围盒与路径包围盒一致 */ + private UnifyBrBoxAndPathBox() + { + const br = this.br; + /** 调整的角度 */ + const adjustAngle = this.GetAdjustAngle(); + const sweepPathInWCS = br.GetSweepPathInWCS(); + // 处理旋转情况下,路径的包围盒不在正交平面内 + EntityManager.RotateEntity(sweepPathInWCS, -adjustAngle); + const brBox = br.BoundingBox; + const pathBox = sweepPathInWCS.BoundingBox; + // 约束二者的包围盒 + if (!equaln(pathBox.max.x, pathBox.min.x)) + { + brBox.max.x = pathBox.max.x; + brBox.min.x = pathBox.min.x; + } + if (!equaln(pathBox.max.y, pathBox.min.y)) + { + brBox.max.y = pathBox.max.y; + brBox.min.y = pathBox.min.y; + } + if (!equaln(pathBox.max.z, pathBox.min.z)) + { + brBox.max.z = pathBox.max.z; + brBox.min.z = pathBox.min.z; + } + return brBox; + } + + /** 平移路径到对应视图平面 */ + private MovePathToView(viewStr: ViewType | "正视图") + { + /** 放样路径(世界坐标系下的) */ + const sweepPathInWCS = this.br.GetSweepPathInWCS(); + /** 偏移向量 */ + const biasV = new Vector3(0, 0, 0); + switch (viewStr) + { + case ViewType.Front: + biasV.y = -sweepPathInWCS.BoundingBox.min.y; + break; + case ViewType.Left: + biasV.x = -sweepPathInWCS.BoundingBox.min.x; + break; + case ViewType.Down: + biasV.z = -sweepPathInWCS.BoundingBox.max.z; + break; + } + // 路径-基点归零(在世界坐标系下) + sweepPathInWCS.Move(biasV); + return sweepPathInWCS; + } + + /** 平移板包围盒到对应视图平面 */ + private MoveBrBoxToView(viewStr: ViewType | "正视图", brBox: Box3Ext) + { + // 板包围盒-基点归零(在世界坐标系下) + const brBoxLines = BoxLine(brBox); + const biasV = new Vector3(0, 0, 0); + switch (viewStr) + { + case ViewType.Front: + biasV.y = -brBox.min.y; + break; + case ViewType.Left: + biasV.x = -brBox.min.x; + break; + case ViewType.Down: + biasV.z = -brBox.max.z; + break; + } + brBoxLines.forEach(e => e.Move(biasV)); + return brBoxLines; + } + + /** 旋转到俯视图下(即XOY平面) */ + private RotateToXOY(sweepPathInWCS: Polyline, brBoxLines: Line[], ucsInv: Matrix4) + { + const splitPl = this.splitPl; + // 修改放样路径的坐标系(有时会导致sweepPathInWCS的形状都发生改变,所以先注释了,其实大概率也用不上) + // sweepPathInWCS.UpdateOCSTo(new Matrix4()); + // 包围盒 + const pathBoxPl = EntityManager.GetEnBoxPl(sweepPathInWCS); + // 转换到俯视图下 + sweepPathInWCS.ApplyMatrix(ucsInv); + pathBoxPl.ApplyMatrix(ucsInv); + splitPl.ApplyMatrix(ucsInv); + splitPl.UpdateOCSTo(new Matrix4()); + brBoxLines.forEach(e => e.ApplyMatrix(ucsInv)); + // 依角度调整,使得路径垂直于XOY平面 + this.AdjustForSweepAngle(sweepPathInWCS, brBoxLines, pathBoxPl); + return pathBoxPl; + } + + /** 修正放样角度带来的影响(比如求不到交点) */ + private AdjustForSweepAngle(sweepPathInWCS: Polyline, brBoxLines: Line[], pathBoxPl: Polyline) + { + const splitPl = this.splitPl; + /** 调整的角度 */ + const adjustAngle = this.GetAdjustAngle(); + const pathOCSInv = sweepPathInWCS.OCSInv; + const pathOCS = sweepPathInWCS.OCS; + /** 逆旋转矩阵 */ + const invRotateMatrix = new Matrix4().makeRotationY(-adjustAngle); + // 在Three默认矩阵左乘(作行变换),故越靠右的矩阵越先运算 + // 所以,这里的运算顺序是先pathOCSInv,再invRotateMatrix,最后再pathOCS + // en.ApplyMatrix(mtx) = en.ApplyMatrix(pathOCSInv).ApplyMatrix(invRotateMatrix).ApplyMatrix(pathOCS) + const mtx = pathOCS.clone().multiply(invRotateMatrix).multiply(pathOCSInv); + sweepPathInWCS.ApplyMatrix(mtx); + pathBoxPl.ApplyMatrix(mtx); + splitPl.ApplyMatrix(mtx); + brBoxLines.forEach(e => e.ApplyMatrix(mtx)); + } + + /** 对路径进行细化 */ + private Tessellate(sweepPathInWCS: Polyline, brBoxLines: Line[], isVerticalWithX: boolean) + { + const splitPl = this.splitPl; + const sweepCusInWCS = sweepPathInWCS.Explode(); + const brBox = EntityManager.GetEntitysBox(brBoxLines); + const ptsInbrBox: Vector3[] = []; + for (const sweepCuInWCS of sweepCusInWCS) + { + if (sweepCuInWCS instanceof Arc) + { + const SPET = 20; + if (isVerticalWithX) + { + const splitPlLen = splitPl.Length; + const cuBox = sweepCuInWCS.BoundingBox; + for (let dist = 0; dist < splitPlLen; dist += SPET) + { + const pt = splitPl.GetPointAtDistance(dist); + if (pt.x > cuBox.min.x && pt.x < cuBox.max.x) + { + if (pt.y > brBox.min.y && pt.y < brBox.max.y) + ptsInbrBox.push(pt); + } + } + } + else + { + const splitPlLen = splitPl.Length; + const cuBox = sweepCuInWCS.BoundingBox; + for (let dist = 0; dist < splitPlLen; dist += SPET) + { + const pt = splitPl.GetPointAtDistance(dist); + if (pt.x > brBox.min.x && pt.x < brBox.max.x) + { + if (pt.y > cuBox.min.y && pt.y < cuBox.max.y) + ptsInbrBox.push(pt); + } + } + } + } + } + return ptsInbrBox; + } + + /** 规范超出的部分 */ + private AdjustOverBox(sweepPathInWCS: Polyline, pathBoxPl: Polyline) + { + const pathBoxMax = { x: -Infinity, y: -Infinity }; + const pathBoxMin = { x: Infinity, y: Infinity }; + for (let param = 0; param < pathBoxPl.LineData.length; param++) + { + const pt = pathBoxPl.GetPointAtParam(param); + pathBoxMax.x = Math.max(pathBoxMax.x, pt.x); + pathBoxMax.y = Math.max(pathBoxMax.y, pt.y); + pathBoxMin.x = Math.min(pathBoxMin.x, pt.x); + pathBoxMin.y = Math.min(pathBoxMin.y, pt.y); + } + const pathMaxPt = new Vector3(pathBoxMax.x, pathBoxMax.y, 0); + const pathMinPt = new Vector3(pathBoxMin.x, pathBoxMin.y, 0); + const zLineMax = CurveManager.CreateZLine(pathMaxPt); + const zLineMin = CurveManager.CreateZLine(pathMinPt); + const inptMax = zLineMax.IntersectWith(sweepPathInWCS, IntersectOption.ExtendThis, 0.01)[0]; + const inptMin = zLineMin.IntersectWith(sweepPathInWCS, IntersectOption.ExtendThis, 0.01)[0]; + if (!inptMax || !inptMin) + { + return [zLineMax, zLineMin]; + } + const paramMax = sweepPathInWCS.GetParamAtPoint(inptMax); + const paramMin = sweepPathInWCS.GetParamAtPoint(inptMin); + /** 展开后的路径 */ + const expandPath = CurveManager.CreateExpandPl(this.GetSweepPathInOCS()); + let maxPoint = expandPath.GetPointAtParam(paramMax); + let minPoint = expandPath.GetPointAtParam(paramMin); + if (maxPoint.x < minPoint.x) [maxPoint, minPoint] = [minPoint, maxPoint]; + const yLineMax = CurveManager.CreateYLine(maxPoint); + const yLineMin = CurveManager.CreateYLine(minPoint); + return [yLineMax, yLineMin]; + } + + /** 获取板件坐标系下的放样路径 */ + private GetSweepPathInOCS() + { + const br = this.br; + /** 放样路径(世界坐标系下的) */ + const sweepPathInWCS = br.GetSweepPathInWCS(); + /** 放样路径(板件坐标系下的) */ + const sweepPathInOCS = sweepPathInWCS.Clone().ApplyMatrix(sweepPathInWCS.OCSInv); + return sweepPathInOCS; + } + + /** 获取板件坐标系下的放样路径并与板件的边对齐 */ + private GetSweepPathInOCSWithBr() + { + const br = this.br; + /** 放样路径(世界坐标系下的) */ + const sweepPathInWCS = br.GetSweepPathInWCS(); + /** 放样路径(板件坐标系下的) */ + const sweepPathInOCS = sweepPathInWCS.Clone().ApplyMatrix(br.OCSInv); + return sweepPathInOCS; + } + + /** 求得的板包围盒的交点与盒内的顶点 */ + private GetPtsInbrBox(brBoxLines: Line[]) + { + const splitPl = this.splitPl; + const brBox = EntityManager.GetEntitysBox(brBoxLines); + const inptsInbrBox: Vector3[] = []; + // 求包围盒的交 + for (const brBoxLine of brBoxLines) + { + inptsInbrBox.push(...brBoxLine.IntersectWith(splitPl, IntersectOption.ExtendNone, 0.01)); + } + // 包围盒内的顶点 + for (let i = 0; i < splitPl.LineData.length; i++) + { + const pt = splitPl.LineData[i].pt; + if (pt.x > brBox.min.x && pt.x < brBox.max.x && pt.y > brBox.min.y && pt.y < brBox.max.y) + { + inptsInbrBox.push(splitPl.GetPointAtParam(i)); + } + } + return inptsInbrBox; + } + + /** 切割路径 */ + SplitInPath(sweepPathInWCS: Polyline) + { + const splitPl = this.splitPl; + // 求交 + const inptsInPath = sweepPathInWCS.IntersectWith(splitPl, IntersectOption.ExtendNone); + /** 新刀 */ + const newSplitPls: Polyline[] = []; + /** 展开后的路径 */ + const expandPath = CurveManager.CreateExpandPl(this.GetSweepPathInOCSWithBr()); + this.ToSplitPosition(expandPath); + // 生成刀 + for (const inptInPath of inptsInPath) + { + const param = sweepPathInWCS.GetParamAtPoint(inptInPath); + const basePoint = expandPath.GetPointAtParam(param); + const newSplitPl = CurveManager.CreatePolyline([new Vector2(basePoint.x, basePoint.y - 1000), new Vector2(basePoint.x, basePoint.y + 1000)]); + newSplitPls.push(newSplitPl); + } + return this.SplitBrByKnifes(newSplitPls); + } + + /** 获取原始位置与切割位置的变换信息 */ + private GetPositionData() + { + const br = this.br; + /** 放样路径的旋转角度 */ + const sweepAngle = br.SweepAngle; + /** 旋转矩阵 */ + const rotateMatrix = new Matrix4().makeRotationZ(sweepAngle); + /** 逆旋转矩阵 */ + const invRotateMatrix = new Matrix4().makeRotationZ(-sweepAngle); + /** 板轮廓(肉) */ + const brContour = br.ContourCurve.Clone() as Polyline; + brContour.ApplyMatrix(invRotateMatrix); + const biasV = new Vector3(-brContour.BoundingBox.min.x, 0, 0); + const biasV2 = new Vector3(brContour.BoundingBox.min.x, 0, 0); + brContour.ApplyMatrix(rotateMatrix); + return { to: { m: invRotateMatrix, v: biasV }, re: { m: rotateMatrix, v: biasV2 } }; + } + + /** 旋转平移到切割位置 */ + private ToSplitPosition(pl: Polyline | Polyline[]) + { + const { to, re } = this.GetPositionData(); + if (pl instanceof Polyline) + { + pl.ApplyMatrix(to.m); + pl.Move(to.v); + } + else + { + pl = pl.map(e => + { + e.ApplyMatrix(to.m); + e.Move(to.v); + return e; + }); + } + } + + /** 平移旋转到原始位置 */ + private ReSplitPosition(pl: Polyline | Polyline[]) + { + const { to, re } = this.GetPositionData(); + if (pl instanceof Polyline) + { + pl.Move(re.v); + pl.ApplyMatrix(re.m); + } + else + { + pl = pl.map(e => + { + e.Move(re.v); + e.ApplyMatrix(re.m); + return e; + }); + } + } + + /** 用刀去切割板(包括轮廓与路径) */ + private SplitBrByKnifes(knifes: Polyline[]) + { + const br = this.br; + /** 放样路径(板件坐标系下的) */ + const sweepPathInOCS = this.GetSweepPathInOCS(); + /** 板轮廓(肉) */ + const brContour = br.ContourCurve.Clone() as Polyline; + // 旋转平移到切割位置 + this.ToSplitPosition(brContour); + // 平移旋转到原始位置 + this.ReSplitPosition(brContour); + this.ReSplitPosition(knifes); + // 切割板 + const cus = SplitPolyline(brContour, knifes, true); + const contours = []; + // 旋转平移到切割位置 + for (const cu of cus) + { + const contour = cu.Clone(); + this.ToSplitPosition(contour); + contours.push(contour); + } + // 修正路径起点不为0的情况 + const firstPt = sweepPathInOCS.LineData[0].pt.clone(); + sweepPathInOCS.Move(AsVector3(firstPt.clone().negate())); + /* ---- [2] 获取正确的放样路径集 ---- */ + const [sweepPaths, contourBiases] = this.GetSweepPaths(br, contours, sweepPathInOCS); + return [cus, sweepPaths, contourBiases] as [Polyline[], Polyline[], { x: number; y: number; }[]]; + } + + /** + * @todo 这个是旧方案,先临时用一用,后面再重构 + * 针对圆弧板进行切割 + * @param br 被切割的板 + * @param clipPls 用于切割的线 + * @问 圆弧板的切割与普通板有何区别? + * @答 切割位置应变长(因为板弯曲了,而板轮廓没有),且放样路径也应被切割 + * @着手点 [1]修正板轮廓的切割位置 [2]获取正确的放样路径集 + * @修正位置 通过param进行逆映射,从而得到正确的切割位置 + * @获取路径 首先要明确一件事,路径需要改变,是因为切割后的板的基点(aabb中X最小的点)可能发生变化 + * 所以,我们需要根据新的基点生成新的路径,新基点沿路径方向与旧基点的差值(恒正)作为生成新路径的切割线(垂直X轴) + * 若切割线与路径平行,则新旧基点一致,无需修改 + */ + private SplitBySweep(br: Board, clipPls: Polyline[]) + { + /* ---- 前置处理:获取实际的放样路径 ---- */ + const SP2OP = (path: Polyline) => ArcBoardBuild.OffsetPolyline(path, -br.Thickness); + const path = br.GetSweepPath(); + /** 路径 */ + const sweepPath = br.SweepVisibleFace === FaceDirection.Front ? SP2OP(path) : path; + // 修正路径起点不为0的情况 + const firstPt = sweepPath.LineData[0].pt.clone(); + sweepPath.Move(AsVector3(firstPt.clone().negate())); + /* ---- [1] 修正板轮廓的切割位置 ---- */ + /** 新的刀 */ + const newSplitPls = this.GetSplitPls(br, clipPls, sweepPath); + /** 板轮廓(肉) */ + const brContour = br.ContourCurve.Clone() as Polyline; + const cus = SplitPolyline(brContour, newSplitPls, true); + /* ---- [2] 获取正确的放样路径集 ---- */ + /** 放样路径的旋转角度 */ + const sweepAngle = br.SweepAngle; + /** 逆旋转矩阵 */ + const invRotateMatrix = new Matrix4().makeRotationZ(-sweepAngle); + const contours: Polyline[] = []; + for (const cu of cus) + { + const contour = cu.Clone(); + contour.ApplyMatrix(invRotateMatrix); + contours.push(contour); + } + const [sweepPaths, contourBiases] = this.GetSweepPaths(br, contours, sweepPath); + return [cus, sweepPaths, contourBiases] as [Polyline[], Polyline[], { x: number; y: number; }[]];; + } + + /** 获得新的刀(多段线) */ + private GetSplitPls(br: Board, splitPls: Polyline[], sweepPath: Polyline) + { + /** 新的刀 */ + const newSplitPls: Polyline[] = []; + /* ---- [2] 旋转:把肉和路径旋转负的放样角度 ---- */ + /** 放样角度 */ + const sweepAngle = br.SweepAngle; + const rotateMatrix = new Matrix4().makeRotationZ(sweepAngle); + const invRotateMatrix = new Matrix4().makeRotationZ(-sweepAngle); + /* ---- [3] 展开:把路径展开 ---- */ + const expandPath = CurveManager.CreateExpandPl(sweepPath); + /* ---- [4] 范围:生成路径X轴的包围线,并把肉包围盒收缩至包围线内 ---- */ + /** 包围线 */ + const pathBox = sweepPath.BoundingBox; + const pathArcs: Arc[] = []; + const cus = sweepPath.Explode(); + for (const cu of cus) + { + if (cu instanceof Arc) + { + pathArcs.push(cu); + } + } + const xInPathByBox = pathBox.max.clone().sub(pathBox.min).x; + const diffLen = expandPath.Length - xInPathByBox; + const GetBoxContour = (box: Box3Ext, diffLen?: number) => + { + const max = box.max.sub(box.min); + const min = box.min.sub(box.min); + if (diffLen) + { + max.x -= diffLen * Math.cos(sweepAngle); + max.y -= diffLen * Math.sin(sweepAngle); + } + const pl = new Polyline([ + { pt: new Vector2(min.x, min.y), bul: 0 }, + { pt: new Vector2(max.x, min.y), bul: 0 }, + { pt: new Vector2(max.x, max.y), bul: 0 }, + { pt: new Vector2(min.x, max.y), bul: 0 }, + { pt: new Vector2(min.x, min.y), bul: 0 }, + ]); + return pl; + }; + /** 包围盒 */ + const brBoxContour = GetBoxContour(br.ContourCurve.BoundingBoxInOCS, diffLen); + brBoxContour.ApplyMatrix(invRotateMatrix); + const pathMin = pathBox.min.clone(); + pathMin.y = 0; + brBoxContour.Move(pathMin); + /* ---- [5] 求交:求得刀在包围盒和包围线上的交点 ---- */ + for (const splitPl of splitPls) + { + splitPl.ApplyMatrix(invRotateMatrix); + /** 在包围线内的顶点 */ + const pts: Vector3[] = []; + for (let i = 0; i < splitPl.LineData.length; i++) + { + const pt = AsVector3(splitPl.GetPointAtParam(i)); + if (pathBox.min.x - 1e-3 < pt.x && pt.x < pathBox.max.x + 1e-3) + pts.push(pt); + // 不是首尾点的顶点也算进去 + else if (!equalv3(pt, splitPl.StartPoint) && !equalv3(pt, splitPl.EndPoint)) + pts.push(pt); + } + /** 在包围盒的交点 */ + const ipts = brBoxContour.IntersectWith(splitPl, IntersectOption.ExtendNone); + ipts.push(...pts); + // 紧密贴合 + for (const pathArc of pathArcs) + { + const MAX_SPLIT_COUNT = 36; + const SPLIT_RATE = 0.5; + const splitCount = Math.min(GetArcDrawCount(pathArc) * SPLIT_RATE, MAX_SPLIT_COUNT); + const divDist = pathArc.Length / splitCount; + for (let i = 0; i < splitCount; i++) + { + const pInArc = pathArc.GetPointAtDistance(i * divDist); + const vertical = CurveManager.CreateYLine(pInArc); + const iptsInArc = splitPl.IntersectWith(vertical, IntersectOption.ExtendArg); + for (const iptInArc of iptsInArc) + { + ipts.push(iptInArc); + } + } + } + /** 用于连成新的刀的点 */ + const splitPts: { index: number; value: Vector3; }[] = []; + for (const ipt of ipts) + { + const index = splitPl.GetParamAtPoint(ipt); + /* ---- [6] 垂直:交点作垂线垂直映射到路径上 ---- */ + const vertical2 = CurveManager.CreateYLine(ipt); + const ipts2 = sweepPath.IntersectWith(vertical2, IntersectOption.ExtendArg); + // 对于多值函数的临时处理(只去最后一次的值,强行变成单射) + const ipt2 = ipts2[ipts2.length - 1]; + if (ipt2) + { + /* ---- [7] 长度:根据点在路径上的长度位置,映射到展开后的路径上 ---- */ + const param = sweepPath.GetParamAtPoint(ipt2); + const ept = expandPath.GetPointAtParam(param); + /* ---- [8] 水平:根据展开后的点与展开前的距离差,进行水平偏移 ---- */ + const biasPt = ipt.clone(); + const len = ept.x - ipt.x; + biasPt.x += len; + splitPts.push({ index: index, value: biasPt }); + // 超出则添加水平偏移点 + if (ipt2.x > pathBox.max.x - 1e-3) + { + const otherPt = biasPt.clone(); + otherPt.x += 1000; + const line = splitPl.GetCurveAtParam(index); + // 根据方向判断index的值 + const dir = line.EndPoint.x - line.StartPoint.x; + if (dir > 0) + { + if (equalv3(line.EndPoint, splitPl.EndPoint)) + splitPts.push({ index: index + 1, value: otherPt }); + } + else + { + if (equalv3(line.StartPoint, splitPl.StartPoint)) + splitPts.push({ index: index - 1, value: otherPt }); + } + } + // 镜像情况(todo定稿后可以与上面的if何在一起) + else if (ipt2.x < pathBox.min.x + 1e-3) + { + const otherPt = biasPt.clone(); + otherPt.x -= 1000; + const line = splitPl.GetCurveAtParam(index); + // 根据方向判断index的值 + const dir = line.EndPoint.x - line.StartPoint.x; + if (dir < 0) + { + if (equalv3(line.EndPoint, splitPl.EndPoint)) + splitPts.push({ index: index + 1, value: otherPt }); + } + else + { + if (equalv3(line.StartPoint, splitPl.StartPoint)) + splitPts.push({ index: index - 1, value: otherPt }); + } + } + } + else + splitPts.push({ index: index, value: ipt }); + } + /* ---- [9] 连线:将偏移后的点连成线(就是新的刀) ---- */ + const arr = splitPts.sort((a, b) => a.index - b.index); + const newSplitPl = CurveManager.CreatePolyline(arr.map(e => AsVector2(e.value))); + newSplitPl.ApplyMatrix(rotateMatrix); + newSplitPls.push(newSplitPl); + } + return newSplitPls; + } + + /** 获取被切割后的放样路径集 */ + private GetSweepPaths(br: Board, contours: Polyline[], sweepPath: Polyline): [Polyline[], { x: number; y: number; }[]] + { + /** 切割后的放样曲线 */ + const sweepPaths: Polyline[] = []; + /** 用于修正轮廓位置的偏移值 */ + const contourBiases: { x: number, y: number; }[] = []; + /** 放样角度 */ + const sweepAngle = br.SweepAngle; + const firstPt = sweepPath.LineData[0].pt.clone(); + sweepPath.Move(AsVector3(firstPt)); + /** 展开的点映射成弯曲的点 */ + const FlatToCurved = (ipt: Vector3) => + { + const dist = ipt.x; + const p = sweepPath.GetPointAtDistance(dist); + const param = sweepPath.GetParamAtDist(dist); + // 这里多返回一些,可能会用到 + return [p, param, dist] as [Vector3, number, number]; + }; + const OP2SP = (offsetPath: Polyline) => ArcBoardBuild.OffsetPolyline(offsetPath, br.Thickness); + /** 限制向量的最大值(用于修复可能产生的精度问题,如首尾相连时) */ + const LimitMaxInVector3 = (vec: Vector3, max: { x?: number, y?: number, z?: number; }) => + { + const v = vec.clone(); + max.x !== undefined && (v.x = Math.min(v.x, max.x)); + max.y !== undefined && (v.y = Math.min(v.y, max.y)); + max.z !== undefined && (v.z = Math.min(v.z, max.z)); + return v; + }; + for (const contour of contours) + { + // 切割路径 + const cu = contour.Clone(); + const box = cu.BoundingBox; + const [pA, paramA, distA] = FlatToCurved(box.min); + const [pB, paramB, distB] = FlatToCurved(LimitMaxInVector3(box.max, { x: sweepPath.Length })); + const sweepPathCopy = sweepPath.Clone(); + const partPath = CurveManager.Get_Pl_InParamAtoParamB(sweepPathCopy, pA, pB); + // 修正轮廓 + let baseLen = pA.x - box.min.x - firstPt.x; + const biasX = Math.cos(sweepAngle) * baseLen; + const biasY = Math.sin(sweepAngle) * baseLen; + contourBiases.push({ x: biasX, y: biasY }); + // 修正路径(保证放样路径都是从(0,0)开始),todo也不一定是(0,0)因为见光面的原因 + const bias = -partPath.LineData[0].pt.x + firstPt.x; + partPath.LineData.forEach(e => { e.pt.x += bias; }); + const factPath = br.SweepVisibleFace === FaceDirection.Front ? OP2SP(partPath) : partPath; + sweepPaths.push(factPath); + } + return [sweepPaths, contourBiases]; + }; +} + +/** 视图类型 */ +export enum ViewType +{ + Normal = "正视图", + Front = "前视图", + Left = "左视图", + Right = "右视图", + Down = "俯视图", + Unknown = "未知视图", +} + +/** 视图管理器 */ +export class ViewManager +{ + /** 获取视图对应的矩阵 */ + static GetViewMatrix(viewType: ViewType) + { + if (viewType === ViewType.Front) + { + const frontMatrix = new Matrix4(); + frontMatrix.elements = [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]; + return frontMatrix; + } + if (viewType === ViewType.Left) + { + const leftMatrix = new Matrix4(); + leftMatrix.elements = [0, -1, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, 0, 0, 0, 1]; + return leftMatrix; + } + if (viewType === ViewType.Down) + { + const downMatrix = new Matrix4(); + downMatrix.elements = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + return downMatrix; + } + if (viewType === ViewType.Right) + { + const rightMatrix = new Matrix4(); + rightMatrix.elements = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]; + return rightMatrix; + } + return new Matrix4(); + }; + + /** 判断矩阵位于哪个视图 */ + static MatrixIsView(m: Matrix4) + { + const frontMatrix = this.GetViewMatrix(ViewType.Front); + const leftMatrix = this.GetViewMatrix(ViewType.Left); + const downMatrix = this.GetViewMatrix(ViewType.Down); + const rightMatrix = this.GetViewMatrix(ViewType.Right); + const EqualnMatrix = (m1: Matrix4, m2: Matrix4) => + { + for (let i = 0; i < m1.elements.length; i++) + { + const e1 = m1.elements[i]; + const e2 = m2.elements[i]; + if (!equaln(e1, e2)) + return false; + } + return true; + }; + const isFrontView = EqualnMatrix(m, frontMatrix); + const isLeftView = EqualnMatrix(m, leftMatrix); + const isDownView = EqualnMatrix(m, downMatrix); + const isRightView = EqualnMatrix(m, rightMatrix); + return isFrontView ? ViewType.Front : isLeftView ? ViewType.Left : isDownView ? ViewType.Down : isRightView ? ViewType.Right : ViewType.Unknown; + }; +} + +/** 线段管理器 */ +class CurveManager +{ + /** X轴的平行线 */ + static CreateXLine = (p: Vector3) => new Line(new Vector3(p.x - 1000, p.y, p.z), new Vector3(p.x + 1000, p.y, p.z)); + /** Y轴的平行线 */ + static CreateYLine = (p: Vector3) => new Line(new Vector3(p.x, p.y - 1000, p.z), new Vector3(p.x, p.y + 1000, p.z)); + /** Z轴的平行线 */ + static CreateZLine = (p: Vector3) => new Line(new Vector3(p.x, p.y, p.z - 1000), new Vector3(p.x, p.y, p.z + 1000)); + /** 通过点集生成多段线 */ + static CreatePolyline = (ps: Vector2[]) => new Polyline(ps.map(p => { return { pt: p, bul: 0 } as PolylineProps; })); + /* 多段线展开直线(类型依旧是多段线) */ + static CreateExpandPl(pl: Polyline) + { + const cus = pl.Explode(); + let len = 0; + const ps: Vector2[] = []; + for (const cu of cus) + { + ps.push(new Vector2(len, 0)); + len += cu.Length; + } + ps.push(new Vector2(len, 0)); + return this.CreatePolyline(ps).ApplyMatrix(pl.OCS); + }; + /** 绘制矩阵所表示的坐标系 */ + static CreateAxis(m: Matrix4) + { + const len = 100; + const xArrow = new Line(new Vector3(0, 0, 0), new Vector3(len, 0, 0)); + const yArrow = new Line(new Vector3(0, 0, 0), new Vector3(0, len, 0)); + const zArrow = new Line(new Vector3(0, 0, 0), new Vector3(0, 0, len)); + xArrow.ColorIndex = 1; + yArrow.ColorIndex = 3; + zArrow.ColorIndex = 5; + xArrow.ApplyMatrix(m); + yArrow.ApplyMatrix(m); + zArrow.ApplyMatrix(m); + return [xArrow, yArrow, zArrow]; + }; + /** 点集转线段簇 */ + static PtsToLines(pts: Vector3[]): Line[] + { + const lines: Line[] = []; + for (let i = 1; i < pts.length; i++) + { + const p1 = pts[i - 1]; + const p2 = pts[i]; + const line = new Line(p1, p2); + lines.push(line); + } + return lines; + }; + /** 线段簇转多段线 */ + static LinesToPl(lines: Line[]): Polyline + { + const pl = new Polyline(); + // 确保坐标系不要沿着直线方向 + const lines2: Line[] = []; + if (lines.length === 1) + { + lines2.push(lines[0]); + const p1 = lines[0].EndPoint; + const p2 = p1.clone().add(p1.clone().normalize().multiplyScalar(1)); + const line = new Line(p1, p2); + lines2.push(line); + pl.OCS = ComputerCurvesNormalOCS(lines2); + } + else + { + pl.OCS = ComputerCurvesNormalOCS(lines); + } + // 转成多段线 + pl.ColorIndex = lines[0].ColorIndex; + for (const splitLine of lines) + { + pl.Join(splitLine, false, 0.01); + } + return pl; + }; + /** 获取pA-pB之间的曲线 */ + static Get_Pl_InParamAtoParamB(pl: Polyline, pA: Vector3, pB: Vector3) + { + const paramA = pl.GetParamAtPoint(pA); + const pls = pl.GetSplitCurves(paramA); + pl = pls[1] || pls[0]; + const paramB = pl.GetParamAtPoint(pB); + const pls2 = pl.GetSplitCurves(paramB); + return pls2[0]; + }; +} + +/** 实体管理器 */ +class EntityManager +{ + /** 旋转实体 */ + static RotateEntity(en: Entity, angle: number) + { + const oldOCS = en.OCS; + const rotateMatrix = new Matrix4().makeRotationY(angle); + en.ApplyMatrix(en.OCSInv).ApplyMatrix(rotateMatrix).ApplyMatrix(oldOCS); + }; + /** 求实体集的包围盒 */ + static GetEntitysBox(ens: Entity[]) + { + const box = new Box3; + for (const en of ens) + box.union(en.BoundingBox); + return box; + }; + /** 获取实体的包围盒线 */ + static GetEnBoxPl(en: Entity) + { + const box = en.Clone().ApplyMatrix(en.OCSInv).BoundingBox; + const boxPl = new Polyline([ + { + pt: new Vector2(box.min.x, box.min.y), + bul: 0 + }, + { + pt: new Vector2(box.max.x, box.min.y), + bul: 0 + }, + { + pt: new Vector2(box.max.x, box.max.y), + bul: 0 + }, + { + pt: new Vector2(box.min.x, box.max.y), + bul: 0 + }, + { + pt: new Vector2(box.min.x, box.min.y), + bul: 0 + }, + ]); + boxPl.ApplyMatrix(en.OCS); + return boxPl; + }; +} diff --git a/src/Add-on/BoardCutting/SplitPolyline.ts b/src/Add-on/BoardCutting/SplitPolyline.ts index 484af34c0..dd2440bf4 100644 --- a/src/Add-on/BoardCutting/SplitPolyline.ts +++ b/src/Add-on/BoardCutting/SplitPolyline.ts @@ -9,9 +9,10 @@ import { IntersectOption } from "../../GraphicsSystem/IntersectWith"; * 线性切割多线段 * @param {Polyline} meatPl 被切割的曲线 * @param {Polyline[]} knifePls 刀曲线 - * @return + * @param {boolean} [isSweep = false] 是否为圆弧板(被切割的曲线是否为圆弧板的轮廓) + * @return {Polyline[]} 切割后的多线段 */ -export function SplitPolyline(meatPl: Polyline, knifePls: Polyline[]): Polyline[] +export function SplitPolyline(meatPl: Polyline, knifePls: Polyline[], isSweep = false): Polyline[] { let allSplitPls: Polyline[] = []; let allIntersePts: Vector3[] = []; @@ -65,6 +66,12 @@ export function SplitPolyline(meatPl: Polyline, knifePls: Polyline[]): Polyline[ pl.Join(route.curve); if (pl.Area2 < 0) pl.Reverse(); + // 针对圆弧板特殊处理(去除多余的控制点) + if (isSweep) + { + pl = pl.GetOffsetCurves(10)[0]; + pl = pl.GetOffsetCurves(-10)[0]; + } return pl; }); return cus;