Skip to content

Commit 065fea4

Browse files
authored
CAM: Fix pure vertical linking move (FreeCAD#26195)
1 parent a139cef commit 065fea4

File tree

3 files changed

+88
-18
lines changed

3 files changed

+88
-18
lines changed

src/Mod/CAM/CAMTests/TestLinkingGenerator.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,63 @@ def test_path_blocked_by_solid(self):
8282
solids=[blocking_box],
8383
)
8484

85+
def test_plunge_to_zero_depth(self):
86+
"""Test that plunge moves correctly go to Z=0 (regression test for depth==0 bug)"""
87+
start = FreeCAD.Vector(0, 0, 1) # Start below clearance
88+
target = FreeCAD.Vector(10, 10, 0) # Target depth is 0
89+
90+
cmds = generator.get_linking_moves(
91+
start_position=start,
92+
target_position=target,
93+
local_clearance=self.local_clearance,
94+
global_clearance=self.global_clearance,
95+
tool_shape=self.tool,
96+
solids=[],
97+
)
98+
99+
# Verify we got commands
100+
self.assertGreater(len(cmds), 0)
101+
102+
# All commands should have complete XYZ coordinates
103+
for cmd in cmds:
104+
self.assertIn("X", cmd.Parameters, "Command missing X coordinate")
105+
self.assertIn("Y", cmd.Parameters, "Command missing Y coordinate")
106+
self.assertIn("Z", cmd.Parameters, "Command missing Z coordinate")
107+
108+
# The last command should be the plunge to target depth (Z=0)
109+
last_cmd = cmds[-1]
110+
self.assertAlmostEqual(last_cmd.Parameters["X"], target.x, places=5)
111+
self.assertAlmostEqual(last_cmd.Parameters["Y"], target.y, places=5)
112+
self.assertAlmostEqual(
113+
last_cmd.Parameters["Z"],
114+
target.z,
115+
places=5,
116+
msg="Final plunge should go to target Z=0, not clearance height",
117+
)
118+
119+
def test_plunge_to_negative_depth(self):
120+
"""Test that plunge moves correctly go to negative Z depths"""
121+
start = FreeCAD.Vector(0, 0, 1) # Start below clearance
122+
target = FreeCAD.Vector(10, 10, -2) # Target depth is negative
123+
124+
cmds = generator.get_linking_moves(
125+
start_position=start,
126+
target_position=target,
127+
local_clearance=self.local_clearance,
128+
global_clearance=self.global_clearance,
129+
tool_shape=self.tool,
130+
solids=[],
131+
)
132+
133+
# The last command should be the plunge to target depth (Z=-2)
134+
last_cmd = cmds[-1]
135+
self.assertAlmostEqual(
136+
last_cmd.Parameters["Z"],
137+
target.z,
138+
places=5,
139+
msg="Final plunge should go to target Z=-2",
140+
)
141+
85142
@unittest.skip("not yet implemented")
86143
def test_zero_retract_offset_uses_local_clearance(self):
87144
cmds = generator.get_linking_moves(

src/Mod/CAM/Path/Base/Generator/linking.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,23 @@ def get_linking_moves(
7979
wire = make_linking_wire(start_position, target_position, height)
8080
if is_wire_collision_free(wire, collision_model):
8181
cmds = Path.fromShape(wire).Commands
82-
for cmd in cmds:
83-
cmd.Name = "G0"
84-
return cmds
82+
# Ensure all commands have complete XYZ coordinates
83+
# Path.fromShape() may omit coordinates that don't change
84+
current_pos = start_position
85+
complete_cmds = []
86+
for i, cmd in enumerate(cmds):
87+
params = dict(cmd.Parameters)
88+
# Fill in missing coordinates from current position
89+
x = params.get("X", current_pos.x)
90+
y = params.get("Y", current_pos.y)
91+
# For the last command (plunge to target), use target.z if Z is missing
92+
if "Z" not in params and i == len(cmds) - 1:
93+
z = target_position.z
94+
else:
95+
z = params.get("Z", current_pos.z)
96+
complete_cmds.append(Path.Command("G0", {"X": x, "Y": y, "Z": z}))
97+
current_pos = Vector(x, y, z)
98+
return complete_cmds
8599

86100
raise RuntimeError("No collision-free path found between start and target positions")
87101

src/Mod/CAM/Path/Op/MillFacing.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -508,13 +508,10 @@ def opExecute(self, obj):
508508
for k in ("X", "Y")
509509
):
510510
# But if Z is different, keep it (it's a plunge or retract)
511-
z_changed = (
512-
abs(
513-
new_params.get("Z", 0)
514-
- last_params.get("Z", new_params.get("Z", 0) + 1)
515-
)
516-
> 1e-9
517-
)
511+
# Use sentinel values that won't conflict with depth == 0
512+
z_new = new_params.get("Z", float("inf"))
513+
z_last = last_params.get("Z", float("-inf"))
514+
z_changed = abs(z_new - z_last) > 1e-9
518515
if not z_changed:
519516
continue
520517
self.commandlist.append(Path.Command(cmd.Name, new_params))
@@ -596,11 +593,11 @@ def opExecute(self, obj):
596593
# Append linking moves, ensuring full XYZ continuity
597594
current = last_position
598595
for lc in link_commands:
599-
params = dict(lc.Parameters)
600-
X = params.get("X", current.x)
601-
Y = params.get("Y", current.y)
602-
Z = params.get("Z", current.z)
603-
# Skip zero-length
596+
params = lc.Parameters
597+
X = params["X"]
598+
Y = params["Y"]
599+
Z = params["Z"]
600+
# Skip zero-length moves
604601
if not (
605602
abs(X - current.x) <= 1e-9
606603
and abs(Y - current.y) <= 1e-9
@@ -609,7 +606,7 @@ def opExecute(self, obj):
609606
self.commandlist.append(
610607
Path.Command(lc.Name, {"X": X, "Y": Y, "Z": Z})
611608
)
612-
current = FreeCAD.Vector(X, Y, Z)
609+
current = FreeCAD.Vector(X, Y, Z)
613610

614611
# Remove the entire initial G0 bundle (up, XY, down) from the copy
615612
del copy_commands[bundle_start:bundle_end]
@@ -631,11 +628,13 @@ def opExecute(self, obj):
631628
cp["Z"] = depth # Cutting moves at depth
632629
else:
633630
cp.setdefault("Z", last.get("Z"))
634-
# Skip zero-length
631+
# Skip zero-length moves
635632
if self.commandlist:
636633
last = self.commandlist[-1].Parameters
634+
# Use sentinel values that won't conflict with depth == 0
637635
if all(
638-
abs(cp[k] - last.get(k, cp[k] + 1)) <= 1e-9 for k in ("X", "Y", "Z")
636+
abs(cp.get(k, float("inf")) - last.get(k, float("-inf"))) <= 1e-9
637+
for k in ("X", "Y", "Z")
639638
):
640639
continue
641640
self.commandlist.append(Path.Command(cc.Name, cp))

0 commit comments

Comments
 (0)