|
| 1 | +import initOpenCascade, { OpenCascadeInstance, TopoDS_Edge, TopoDS_Shape } from "../../../bitbybit-dev-occt/bitbybit-dev-occt"; |
| 2 | +import { OccHelper } from "../../occ-helper"; |
| 3 | +import { VectorHelperService } from "../../api/vector-helper.service"; |
| 4 | +import { ShapesHelperService } from "../../api/shapes-helper.service"; |
| 5 | +import { OCCTWire, OCCTEdge } from "../shapes"; |
| 6 | +import { DxfService } from "./dxf.service"; |
| 7 | +import * as Inputs from "../../api/inputs/inputs"; |
| 8 | + |
| 9 | +describe("DxfService unit tests", () => { |
| 10 | + let occt: OpenCascadeInstance; |
| 11 | + let occHelper: OccHelper; |
| 12 | + let dxfService: DxfService; |
| 13 | + let wire: OCCTWire; |
| 14 | + let edge: OCCTEdge; |
| 15 | + |
| 16 | + beforeAll(async () => { |
| 17 | + occt = await initOpenCascade(); |
| 18 | + const vec = new VectorHelperService(); |
| 19 | + const s = new ShapesHelperService(); |
| 20 | + occHelper = new OccHelper(vec, s, occt); |
| 21 | + dxfService = occHelper.dxfService; |
| 22 | + wire = new OCCTWire(occt, occHelper); |
| 23 | + edge = new OCCTEdge(occt, occHelper); |
| 24 | + }); |
| 25 | + |
| 26 | + describe("tryCreatePolylineWithBulges", () => { |
| 27 | + // Helper to access private method |
| 28 | + const callTryCreatePolylineWithBulges = ( |
| 29 | + service: DxfService, |
| 30 | + edges: TopoDS_Edge[], |
| 31 | + startIndex: number, |
| 32 | + shouldBeClosed: boolean |
| 33 | + ) => { |
| 34 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 35 | + return (service as any).tryCreatePolylineWithBulges(edges, startIndex, shouldBeClosed); |
| 36 | + }; |
| 37 | + |
| 38 | + it("should return null for a single linear edge", () => { |
| 39 | + const lineEdge = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 40 | + const edges = [lineEdge]; |
| 41 | + |
| 42 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 43 | + |
| 44 | + // Returns null because we need at least 2 edges |
| 45 | + expect(result).toBeNull(); |
| 46 | + |
| 47 | + lineEdge.delete(); |
| 48 | + }); |
| 49 | + |
| 50 | + it("should return null for a full circle edge", () => { |
| 51 | + const circleEdge = edge.createCircleEdge({ |
| 52 | + radius: 5, |
| 53 | + center: [0, 0, 0], |
| 54 | + direction: [0, 1, 0] |
| 55 | + }); |
| 56 | + const edges = [circleEdge]; |
| 57 | + |
| 58 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 59 | + |
| 60 | + // Full circles are handled separately |
| 61 | + expect(result).toBeNull(); |
| 62 | + |
| 63 | + circleEdge.delete(); |
| 64 | + }); |
| 65 | + |
| 66 | + it("should create a polyline from two consecutive linear edges", () => { |
| 67 | + const edge1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 68 | + const edge2 = occHelper.edgesService.lineEdge({ start: [10, 0, 0], end: [10, 0, 10] }); |
| 69 | + const edges = [edge1, edge2]; |
| 70 | + |
| 71 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 72 | + |
| 73 | + expect(result).not.toBeNull(); |
| 74 | + expect(result.nextIndex).toBe(2); |
| 75 | + expect(result.polyline.points.length).toBe(3); // Start + mid + end |
| 76 | + expect(result.polyline.closed).toBe(false); |
| 77 | + |
| 78 | + // All bulges should be 0 for linear edges |
| 79 | + result.polyline.bulges.forEach((bulge: number) => { |
| 80 | + expect(bulge).toBe(0); |
| 81 | + }); |
| 82 | + |
| 83 | + edge1.delete(); |
| 84 | + edge2.delete(); |
| 85 | + }); |
| 86 | + |
| 87 | + it("should create a polyline from two consecutive linear edges with closed flag", () => { |
| 88 | + const edge1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 89 | + const edge2 = occHelper.edgesService.lineEdge({ start: [10, 0, 0], end: [0, 0, 0] }); |
| 90 | + const edges = [edge1, edge2]; |
| 91 | + |
| 92 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, true); |
| 93 | + |
| 94 | + expect(result).not.toBeNull(); |
| 95 | + expect(result.polyline.closed).toBe(true); |
| 96 | + expect(result.polyline.points.length).toBe(3); |
| 97 | + |
| 98 | + edge1.delete(); |
| 99 | + edge2.delete(); |
| 100 | + }); |
| 101 | + |
| 102 | + it("should create a polyline with non-zero bulges for arc edges", () => { |
| 103 | + const lineEdge = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 104 | + const arcEdge = occHelper.edgesService.arcThroughThreePoints({ |
| 105 | + start: [10, 0, 0], |
| 106 | + middle: [12, 0, 5], |
| 107 | + end: [10, 0, 10] |
| 108 | + }); |
| 109 | + const edges = [lineEdge, arcEdge]; |
| 110 | + |
| 111 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 112 | + |
| 113 | + expect(result).not.toBeNull(); |
| 114 | + expect(result.nextIndex).toBe(2); |
| 115 | + expect(result.polyline.points.length).toBe(3); |
| 116 | + |
| 117 | + // First bulge should be 0 (line), second should be non-zero (arc) |
| 118 | + expect(result.polyline.bulges[0]).toBe(0); |
| 119 | + expect(Math.abs(result.polyline.bulges[1])).toBeCloseTo(0.399999999); |
| 120 | + |
| 121 | + lineEdge.delete(); |
| 122 | + arcEdge.delete(); |
| 123 | + }); |
| 124 | + |
| 125 | + it("should stop at complex edge", () => { |
| 126 | + const lineEdge1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [5, 0, 0] }); |
| 127 | + const lineEdge2 = occHelper.edgesService.lineEdge({ start: [5, 0, 0], end: [10, 0, 0] }); |
| 128 | + |
| 129 | + // Create a bezier/spline edge (complex edge) |
| 130 | + const bezierWire = wire.interpolatePoints({ |
| 131 | + points: [[10, 0, 0], [12, 0, 3], [15, 0, 5]] as Inputs.Base.Point3[], |
| 132 | + periodic: false, |
| 133 | + tolerance: 0.0001 |
| 134 | + }); |
| 135 | + const bezierEdges = occHelper.edgesService.getEdgesAlongWire({ shape: bezierWire }); |
| 136 | + |
| 137 | + const lineEdge3 = occHelper.edgesService.lineEdge({ start: [15, 0, 5], end: [20, 0, 5] }); |
| 138 | + |
| 139 | + const edges = [lineEdge1, lineEdge2, ...bezierEdges, lineEdge3]; |
| 140 | + |
| 141 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 142 | + |
| 143 | + expect(result).not.toBeNull(); |
| 144 | + // Should stop at the complex bezier edge |
| 145 | + expect(result.nextIndex).toBe(2); |
| 146 | + expect(result.polyline.points.length).toBe(3); |
| 147 | + |
| 148 | + lineEdge1.delete(); |
| 149 | + lineEdge2.delete(); |
| 150 | + bezierWire.delete(); |
| 151 | + lineEdge3.delete(); |
| 152 | + }); |
| 153 | + |
| 154 | + it("should return null when starting with a complex edge", () => { |
| 155 | + const bezierWire = wire.interpolatePoints({ |
| 156 | + points: [[0, 0, 0], [5, 0, 3], [10, 0, 0]] as Inputs.Base.Point3[], |
| 157 | + periodic: false, |
| 158 | + tolerance: 0.0001 |
| 159 | + }); |
| 160 | + const bezierEdges = occHelper.edgesService.getEdgesAlongWire({ shape: bezierWire }); |
| 161 | + |
| 162 | + const result = callTryCreatePolylineWithBulges(dxfService, bezierEdges, 0, false); |
| 163 | + |
| 164 | + // Complex edge at start should return null |
| 165 | + expect(result).toBeNull(); |
| 166 | + |
| 167 | + bezierWire.delete(); |
| 168 | + }); |
| 169 | + |
| 170 | + it("should handle starting from a middle index", () => { |
| 171 | + const edge1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [5, 0, 0] }); |
| 172 | + const edge2 = occHelper.edgesService.lineEdge({ start: [5, 0, 0], end: [10, 0, 0] }); |
| 173 | + const edge3 = occHelper.edgesService.lineEdge({ start: [10, 0, 0], end: [15, 0, 0] }); |
| 174 | + const edge4 = occHelper.edgesService.lineEdge({ start: [15, 0, 0], end: [20, 0, 0] }); |
| 175 | + const edges = [edge1, edge2, edge3, edge4]; |
| 176 | + |
| 177 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 2, false); |
| 178 | + |
| 179 | + expect(result).not.toBeNull(); |
| 180 | + expect(result.nextIndex).toBe(4); |
| 181 | + expect(result.polyline.points.length).toBe(3); // edge3 start, edge4 start, edge4 end |
| 182 | + |
| 183 | + // First point should be start of edge3 |
| 184 | + expect(result.polyline.points[0][0]).toBeCloseTo(10, 2); // X |
| 185 | + expect(result.polyline.points[0][1]).toBeCloseTo(0, 2); // Z (mapped to Y in DXF) |
| 186 | + |
| 187 | + edge1.delete(); |
| 188 | + edge2.delete(); |
| 189 | + edge3.delete(); |
| 190 | + edge4.delete(); |
| 191 | + }); |
| 192 | + |
| 193 | + it("should create polyline with correct point mapping from XZ to XY", () => { |
| 194 | + // Points at different Z values (which become Y in DXF) |
| 195 | + const edge1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 5] }); |
| 196 | + const edge2 = occHelper.edgesService.lineEdge({ start: [10, 0, 5], end: [20, 0, 10] }); |
| 197 | + const edges = [edge1, edge2]; |
| 198 | + |
| 199 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 200 | + |
| 201 | + expect(result).not.toBeNull(); |
| 202 | + // Check X coordinates |
| 203 | + expect(result.polyline.points[0][0]).toBeCloseTo(0, 2); |
| 204 | + expect(result.polyline.points[1][0]).toBeCloseTo(10, 2); |
| 205 | + expect(result.polyline.points[2][0]).toBeCloseTo(20, 2); |
| 206 | + |
| 207 | + // Check Y coordinates (originally Z in 3D) |
| 208 | + expect(result.polyline.points[0][1]).toBeCloseTo(0, 2); |
| 209 | + expect(result.polyline.points[1][1]).toBeCloseTo(5, 2); |
| 210 | + expect(result.polyline.points[2][1]).toBeCloseTo(10, 2); |
| 211 | + |
| 212 | + edge1.delete(); |
| 213 | + edge2.delete(); |
| 214 | + }); |
| 215 | + |
| 216 | + it("should handle mixed linear and arc edges", () => { |
| 217 | + const line1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 218 | + const arc = occHelper.edgesService.arcThroughThreePoints({ |
| 219 | + start: [10, 0, 0], |
| 220 | + middle: [15, 0, 5], |
| 221 | + end: [10, 0, 10] |
| 222 | + }); |
| 223 | + const line2 = occHelper.edgesService.lineEdge({ start: [10, 0, 10], end: [0, 0, 10] }); |
| 224 | + const edges = [line1, arc, line2]; |
| 225 | + |
| 226 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 227 | + |
| 228 | + expect(result).not.toBeNull(); |
| 229 | + expect(result.nextIndex).toBe(3); |
| 230 | + expect(result.polyline.points.length).toBe(4); |
| 231 | + |
| 232 | + // Check bulges: line=0, arc=non-zero, line=0, last=0 |
| 233 | + expect(result.polyline.bulges[0]).toBe(0); |
| 234 | + expect(Math.abs(result.polyline.bulges[1])).toBeCloseTo(1); |
| 235 | + expect(result.polyline.bulges[2]).toBe(0); |
| 236 | + expect(result.polyline.bulges[3]).toBe(0); |
| 237 | + |
| 238 | + line1.delete(); |
| 239 | + arc.delete(); |
| 240 | + line2.delete(); |
| 241 | + }); |
| 242 | + |
| 243 | + it("should stop when encountering a full circle in the middle", () => { |
| 244 | + const line1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 245 | + const line2 = occHelper.edgesService.lineEdge({ start: [10, 0, 0], end: [20, 0, 0] }); |
| 246 | + const circleEdge = edge.createCircleEdge({ |
| 247 | + radius: 3, |
| 248 | + center: [25, 0, 0], |
| 249 | + direction: [0, 1, 0] |
| 250 | + }); |
| 251 | + const line3 = occHelper.edgesService.lineEdge({ start: [30, 0, 0], end: [40, 0, 0] }); |
| 252 | + const edges = [line1, line2, circleEdge, line3]; |
| 253 | + |
| 254 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 255 | + |
| 256 | + expect(result).not.toBeNull(); |
| 257 | + // Should stop at the full circle |
| 258 | + expect(result.nextIndex).toBe(2); |
| 259 | + expect(result.polyline.points.length).toBe(3); |
| 260 | + |
| 261 | + line1.delete(); |
| 262 | + line2.delete(); |
| 263 | + circleEdge.delete(); |
| 264 | + line3.delete(); |
| 265 | + }); |
| 266 | + |
| 267 | + it("should handle a closed rectangle with 4 edges", () => { |
| 268 | + const edge1 = occHelper.edgesService.lineEdge({ start: [0, 0, 0], end: [10, 0, 0] }); |
| 269 | + const edge2 = occHelper.edgesService.lineEdge({ start: [10, 0, 0], end: [10, 0, 5] }); |
| 270 | + const edge3 = occHelper.edgesService.lineEdge({ start: [10, 0, 5], end: [0, 0, 5] }); |
| 271 | + const edge4 = occHelper.edgesService.lineEdge({ start: [0, 0, 5], end: [0, 0, 0] }); |
| 272 | + const edges = [edge1, edge2, edge3, edge4]; |
| 273 | + |
| 274 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, true); |
| 275 | + |
| 276 | + expect(result).not.toBeNull(); |
| 277 | + expect(result.nextIndex).toBe(4); |
| 278 | + expect(result.polyline.closed).toBe(true); |
| 279 | + expect(result.polyline.points.length).toBe(5); // 4 corners + end point |
| 280 | + |
| 281 | + // All bulges should be 0 |
| 282 | + result.polyline.bulges.forEach((bulge: number) => { |
| 283 | + expect(bulge).toBe(0); |
| 284 | + }); |
| 285 | + |
| 286 | + edge1.delete(); |
| 287 | + edge2.delete(); |
| 288 | + edge3.delete(); |
| 289 | + edge4.delete(); |
| 290 | + }); |
| 291 | + |
| 292 | + it("should calculate correct bulge for a 90-degree arc", () => { |
| 293 | + // Create a 90-degree arc from (5, 0, 0) to (0, 0, 5) with center at origin, radius 5 |
| 294 | + // This is a CCW 90-degree arc in the XZ plane |
| 295 | + const arcEdge = occHelper.edgesService.arcThroughThreePoints({ |
| 296 | + start: [5, 0, 0], |
| 297 | + middle: [5 * Math.cos(Math.PI / 4), 0, 5 * Math.sin(Math.PI / 4)], // 45 degrees |
| 298 | + end: [0, 0, 5] |
| 299 | + }); |
| 300 | + const lineEdge = occHelper.edgesService.lineEdge({ start: [0, 0, 5], end: [0, 0, 10] }); |
| 301 | + const edges = [arcEdge, lineEdge]; |
| 302 | + |
| 303 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 304 | + |
| 305 | + expect(result).not.toBeNull(); |
| 306 | + |
| 307 | + // For a 90-degree arc, bulge = tan(90/4) = tan(22.5°) ≈ 0.414 |
| 308 | + // Sign depends on arc direction |
| 309 | + expect(Math.abs(result.polyline.bulges[0])).toBeCloseTo(0.414, 1); |
| 310 | + |
| 311 | + arcEdge.delete(); |
| 312 | + lineEdge.delete(); |
| 313 | + }); |
| 314 | + |
| 315 | + it("should calculate correct bulge for a 180-degree arc (semicircle)", () => { |
| 316 | + // Create a semicircle arc |
| 317 | + const arcEdge = occHelper.edgesService.arcThroughThreePoints({ |
| 318 | + start: [0, 0, 0], |
| 319 | + middle: [5, 0, 5], |
| 320 | + end: [10, 0, 0] |
| 321 | + }); |
| 322 | + const lineEdge = occHelper.edgesService.lineEdge({ start: [10, 0, 0], end: [20, 0, 0] }); |
| 323 | + const edges = [arcEdge, lineEdge]; |
| 324 | + |
| 325 | + const result = callTryCreatePolylineWithBulges(dxfService, edges, 0, false); |
| 326 | + |
| 327 | + expect(result).not.toBeNull(); |
| 328 | + |
| 329 | + // For a 180-degree arc, bulge = tan(180/4) = tan(45°) = 1.0 |
| 330 | + expect(Math.abs(result.polyline.bulges[0])).toBeCloseTo(1.0, 1); |
| 331 | + |
| 332 | + arcEdge.delete(); |
| 333 | + lineEdge.delete(); |
| 334 | + }); |
| 335 | + }); |
| 336 | +}); |
0 commit comments