Skip to content

Commit a194058

Browse files
bug fix for dxf generator to check arc direction when it's perfectly aligned on vertical/horizontal axis.
1 parent e35eeb0 commit a194058

2 files changed

Lines changed: 277 additions & 2 deletions

File tree

packages/dev/occt/lib/services/base/dxf.service.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ export class DxfService {
151151

152152
points.push([startPt[0], startPt[2]]);
153153

154+
// DXF bulge is the tangent of 1/4 of the included angle
155+
// Positive bulge = arc curves to the left when traveling from start to end
156+
// Negative bulge = arc curves to the right when traveling from start to end
157+
154158
// Calculate the included angle
155159
const startAngle = Math.atan2(startPt[2] - center[2], startPt[0] - center[0]);
156160
const endAngle = Math.atan2(endPt[2] - center[2], endPt[0] - center[0]);
@@ -161,8 +165,42 @@ export class DxfService {
161165
while (includedAngle > Math.PI) includedAngle -= 2 * Math.PI;
162166
while (includedAngle < -Math.PI) includedAngle += 2 * Math.PI;
163167

164-
// Bulge = tan(included_angle / 4)
165-
const bulge = Math.tan(includedAngle / 4);
168+
// Determine the sign by checking which side of the chord the center is on
169+
// Using 2D cross product in the XZ plane (bird's eye view, Y removed)
170+
const dx = endPt[0] - startPt[0];
171+
const dz = endPt[2] - startPt[2];
172+
173+
// Vector from start point to center
174+
const centerDx = center[0] - startPt[0];
175+
const centerDz = center[2] - startPt[2];
176+
177+
// Cross product in 2D: determines which side of the chord vector the center is on
178+
// Positive = center is to the left of chord (when facing from start to end)
179+
// Negative = center is to the right of chord
180+
const crossProduct = dx * centerDz - dz * centerDx;
181+
182+
// Bulge formula: sign * tan(|includedAngle| / 4)
183+
const absIncludedAngle = Math.abs(includedAngle);
184+
185+
let sign: number;
186+
if (Math.abs(crossProduct) < 1e-10) {
187+
// Degenerate case: center is on the chord line
188+
// Sample a point on the arc at the midpoint angle to determine which side
189+
const midAngle = (startAngle + endAngle) / 2;
190+
const radius = this.edgesService.getCircularEdgeRadius({ shape: currentEdge });
191+
const midPt = [
192+
center[0] + radius * Math.cos(midAngle),
193+
center[2] + radius * Math.sin(midAngle)
194+
];
195+
196+
// Check which side of the chord this midpoint is on
197+
const midCrossProduct = dx * (midPt[1] - startPt[2]) - dz * (midPt[0] - startPt[0]);
198+
sign = Math.sign(midCrossProduct);
199+
} else {
200+
sign = Math.sign(crossProduct);
201+
}
202+
203+
const bulge = sign * Math.tan(absIncludedAngle / 4);
166204
bulges.push(bulge);
167205
} else {
168206
// Complex edge: tessellate and add points with bulge 0

packages/dev/occt/lib/services/io.test.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,4 +323,241 @@ describe("OCCT io unit tests", () => {
323323
expect(polyline.closed).toBe(true);
324324
expect(polyline.points.length).toBeGreaterThan(3);
325325
});
326+
327+
it("should correctly export oblong slot with opposing semicircular arcs", () => {
328+
const radius = 5;
329+
const leftArcCenterX = 10;
330+
const rightArcCenterX = 30;
331+
const arcCenterZ = 20;
332+
333+
// Top straight line
334+
const topLine = wire.createPolylineWire({
335+
points: [
336+
[leftArcCenterX, 0, arcCenterZ + radius],
337+
[rightArcCenterX, 0, arcCenterZ + radius]
338+
]
339+
});
340+
341+
// Right semicircle arc (from top to bottom, curving right)
342+
const rightArc = occHelper.edgesService.arcThroughTwoPointsAndTangent({
343+
start: [rightArcCenterX, 0, arcCenterZ + radius],
344+
end: [rightArcCenterX, 0, arcCenterZ - radius],
345+
tangentVec: [1, 0, 0]
346+
});
347+
348+
// Bottom straight line
349+
const bottomLine = wire.createPolylineWire({
350+
points: [
351+
[rightArcCenterX, 0, arcCenterZ - radius],
352+
[leftArcCenterX, 0, arcCenterZ - radius]
353+
]
354+
});
355+
356+
// Left semicircle arc (from bottom to top, curving left)
357+
const leftArc = occHelper.edgesService.arcThroughTwoPointsAndTangent({
358+
start: [leftArcCenterX, 0, arcCenterZ - radius],
359+
end: [leftArcCenterX, 0, arcCenterZ + radius],
360+
tangentVec: [-1, 0, 0]
361+
});
362+
363+
const slotWire = wire.combineEdgesAndWiresIntoAWire({
364+
shapes: [topLine, rightArc, bottomLine, leftArc]
365+
});
366+
367+
const dxfPathOpt = new Inputs.OCCT.ShapeToDxfPathsDto<TopoDS_Shape>(slotWire);
368+
const dxfPaths = io.shapeToDxfPaths(dxfPathOpt);
369+
370+
expect(dxfPaths.length).toBe(1);
371+
expect(dxfPaths[0].segments.length).toBe(1);
372+
373+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
374+
const polyline = dxfPaths[0].segments[0] as any;
375+
expect(polyline.points).toBeDefined();
376+
expect(polyline.bulges).toBeDefined();
377+
expect(polyline.closed).toBe(true);
378+
379+
// Find bulge indices for the arcs
380+
const bulges = polyline.bulges;
381+
382+
// Both arcs should have significant non-zero bulges (semicircles ≈ ±1)
383+
const significantBulges = bulges.filter((b: number) => Math.abs(b) > 0.9);
384+
expect(significantBulges.length).toBe(2); // Two semicircular arcs
385+
386+
// The two arcs should have opposite signs (one CW, one CCW)
387+
// Find the two significant bulges
388+
const bulgeIndices: number[] = [];
389+
for (let i = 0; i < bulges.length; i++) {
390+
if (Math.abs(bulges[i]) > 0.9) {
391+
bulgeIndices.push(i);
392+
}
393+
}
394+
expect(bulgeIndices.length).toBe(2);
395+
396+
// Check that they have opposite signs
397+
const bulge1 = bulges[bulgeIndices[0]];
398+
const bulge2 = bulges[bulgeIndices[1]];
399+
expect(bulge1 * bulge2).toBeLessThan(0); // Opposite signs
400+
401+
slotWire.delete();
402+
topLine.delete();
403+
bottomLine.delete();
404+
rightArc.delete();
405+
leftArc.delete();
406+
});
407+
408+
it("should correctly export semicircular arc curving right with positive bulge", () => {
409+
const radius = 5;
410+
const centerX = 20;
411+
const centerZ = 10;
412+
413+
// Semicircle arc from top to bottom with tangent pointing right
414+
// This should create an arc that curves to the RIGHT = positive bulge
415+
const rightArc = occHelper.edgesService.arcThroughTwoPointsAndTangent({
416+
start: [centerX, 0, centerZ + radius],
417+
end: [centerX, 0, centerZ - radius],
418+
tangentVec: [1, 0, 0] // Tangent pointing right
419+
});
420+
421+
const arcWire = wire.combineEdgesAndWiresIntoAWire({ shapes: [rightArc] });
422+
423+
const startPt = occHelper.edgesService.startPointOnEdge({ shape: rightArc });
424+
const endPt = occHelper.edgesService.endPointOnEdge({ shape: rightArc });
425+
426+
// Verify the arc geometry
427+
expect(startPt[2]).toBeGreaterThan(endPt[2]); // Start is higher than end
428+
429+
const dxfPathOpt = new Inputs.OCCT.ShapeToDxfPathsDto<TopoDS_Shape>(arcWire);
430+
const dxfPaths = io.shapeToDxfPaths(dxfPathOpt);
431+
432+
expect(dxfPaths.length).toBe(1);
433+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
434+
const polyline = dxfPaths[0].segments[0] as any;
435+
436+
// For a semicircular arc, the bulge should be close to ±1
437+
// The actual sign depends on the direction OCCT creates the arc
438+
expect(Math.abs(polyline.bulges[0])).toBeGreaterThan(0.9); // Semicircle ≈ ±1
439+
440+
arcWire.delete();
441+
rightArc.delete();
442+
});
443+
444+
it("should correctly export semicircular arc curving left with negative bulge", () => {
445+
const radius = 5;
446+
const centerX = 20;
447+
const centerZ = 10;
448+
449+
// Semicircle arc from bottom to top with tangent pointing left
450+
// This should create an arc that curves to the LEFT = negative bulge
451+
const leftArc = occHelper.edgesService.arcThroughTwoPointsAndTangent({
452+
start: [centerX, 0, centerZ - radius],
453+
end: [centerX, 0, centerZ + radius],
454+
tangentVec: [-1, 0, 0] // Tangent pointing left
455+
});
456+
457+
const arcWire = wire.combineEdgesAndWiresIntoAWire({ shapes: [leftArc] });
458+
459+
const startPt = occHelper.edgesService.startPointOnEdge({ shape: leftArc });
460+
const endPt = occHelper.edgesService.endPointOnEdge({ shape: leftArc });
461+
462+
// Verify the arc geometry
463+
expect(startPt[2]).toBeLessThan(endPt[2]); // Start is lower than end
464+
465+
const dxfPathOpt = new Inputs.OCCT.ShapeToDxfPathsDto<TopoDS_Shape>(arcWire);
466+
const dxfPaths = io.shapeToDxfPaths(dxfPathOpt);
467+
468+
expect(dxfPaths.length).toBe(1);
469+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
470+
const polyline = dxfPaths[0].segments[0] as any;
471+
472+
// For a semicircular arc, the bulge should be close to ±1
473+
expect(Math.abs(polyline.bulges[0])).toBeGreaterThan(0.9); // Semicircle ≈ ±1
474+
475+
arcWire.delete();
476+
leftArc.delete();
477+
});
478+
479+
it("should correctly export horizontal arc curving upward (center above chord)", () => {
480+
const startX = 10;
481+
const endX = 20;
482+
const chordZ = 15;
483+
484+
// Create arc with center above the chord
485+
// For a horizontal chord, center above means positive Z offset
486+
const centerX = (startX + endX) / 2;
487+
const centerZ = chordZ + 5; // Center 5 units above the chord
488+
const radius = Math.sqrt(Math.pow((endX - startX) / 2, 2) + Math.pow(5, 2)); // Calculate radius
489+
490+
// Create arc using arcThroughThreePoints
491+
// Middle point should be on the arc itself, at the peak
492+
const middleX = centerX;
493+
const middleZ = centerZ + radius; // Top of the arc
494+
495+
const arc = occHelper.edgesService.arcThroughThreePoints({
496+
start: [startX, 0, chordZ],
497+
middle: [middleX, 0, middleZ],
498+
end: [endX, 0, chordZ]
499+
});
500+
501+
const arcWire = wire.combineEdgesAndWiresIntoAWire({ shapes: [arc] });
502+
503+
const center = occHelper.edgesService.getCircularEdgeCenterPoint({ shape: arc });
504+
505+
const dxfPathOpt = new Inputs.OCCT.ShapeToDxfPathsDto<TopoDS_Shape>(arcWire);
506+
const dxfPaths = io.shapeToDxfPaths(dxfPathOpt);
507+
508+
expect(dxfPaths.length).toBe(1);
509+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
510+
const polyline = dxfPaths[0].segments[0] as any;
511+
512+
// Verify center is actually above the chord
513+
expect(center[2]).toBeGreaterThan(chordZ);
514+
515+
// For a horizontal chord, center above = positive bulge
516+
expect(polyline.bulges[0]).toBeGreaterThan(0.1);
517+
518+
arcWire.delete();
519+
arc.delete();
520+
});
521+
522+
it("should correctly export horizontal arc curving downward (center below chord)", () => {
523+
const startX = 10;
524+
const endX = 20;
525+
const chordZ = 15;
526+
527+
// Create arc with center below the chord
528+
const centerX = (startX + endX) / 2;
529+
const centerZ = chordZ - 5; // Center 5 units below the chord
530+
const radius = Math.sqrt(Math.pow((endX - startX) / 2, 2) + Math.pow(5, 2)); // Calculate radius
531+
532+
// Create arc using arcThroughThreePoints
533+
// Middle point should be on the arc itself, at the lowest point
534+
const middleX = centerX;
535+
const middleZ = centerZ - radius; // Bottom of the arc
536+
537+
const arc = occHelper.edgesService.arcThroughThreePoints({
538+
start: [startX, 0, chordZ],
539+
middle: [middleX, 0, middleZ],
540+
end: [endX, 0, chordZ]
541+
});
542+
543+
const arcWire = wire.combineEdgesAndWiresIntoAWire({ shapes: [arc] });
544+
545+
const center = occHelper.edgesService.getCircularEdgeCenterPoint({ shape: arc });
546+
547+
const dxfPathOpt = new Inputs.OCCT.ShapeToDxfPathsDto<TopoDS_Shape>(arcWire);
548+
const dxfPaths = io.shapeToDxfPaths(dxfPathOpt);
549+
550+
expect(dxfPaths.length).toBe(1);
551+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
552+
const polyline = dxfPaths[0].segments[0] as any;
553+
554+
// Verify center is actually below the chord
555+
expect(center[2]).toBeLessThan(chordZ);
556+
557+
// For a horizontal chord, center below = negative bulge
558+
expect(polyline.bulges[0]).toBeLessThan(-0.1);
559+
560+
arcWire.delete();
561+
arc.delete();
562+
});
326563
});

0 commit comments

Comments
 (0)