CAM: Fix pure vertical linking move (#26195)

This commit is contained in:
sliptonic
2025-12-23 12:19:32 -06:00
committed by GitHub
parent a139cef80f
commit 065fea473f
3 changed files with 88 additions and 18 deletions

View File

@@ -82,6 +82,63 @@ class TestGetLinkingMoves(PathTestUtils.PathTestBase):
solids=[blocking_box],
)
def test_plunge_to_zero_depth(self):
"""Test that plunge moves correctly go to Z=0 (regression test for depth==0 bug)"""
start = FreeCAD.Vector(0, 0, 1) # Start below clearance
target = FreeCAD.Vector(10, 10, 0) # Target depth is 0
cmds = generator.get_linking_moves(
start_position=start,
target_position=target,
local_clearance=self.local_clearance,
global_clearance=self.global_clearance,
tool_shape=self.tool,
solids=[],
)
# Verify we got commands
self.assertGreater(len(cmds), 0)
# All commands should have complete XYZ coordinates
for cmd in cmds:
self.assertIn("X", cmd.Parameters, "Command missing X coordinate")
self.assertIn("Y", cmd.Parameters, "Command missing Y coordinate")
self.assertIn("Z", cmd.Parameters, "Command missing Z coordinate")
# The last command should be the plunge to target depth (Z=0)
last_cmd = cmds[-1]
self.assertAlmostEqual(last_cmd.Parameters["X"], target.x, places=5)
self.assertAlmostEqual(last_cmd.Parameters["Y"], target.y, places=5)
self.assertAlmostEqual(
last_cmd.Parameters["Z"],
target.z,
places=5,
msg="Final plunge should go to target Z=0, not clearance height",
)
def test_plunge_to_negative_depth(self):
"""Test that plunge moves correctly go to negative Z depths"""
start = FreeCAD.Vector(0, 0, 1) # Start below clearance
target = FreeCAD.Vector(10, 10, -2) # Target depth is negative
cmds = generator.get_linking_moves(
start_position=start,
target_position=target,
local_clearance=self.local_clearance,
global_clearance=self.global_clearance,
tool_shape=self.tool,
solids=[],
)
# The last command should be the plunge to target depth (Z=-2)
last_cmd = cmds[-1]
self.assertAlmostEqual(
last_cmd.Parameters["Z"],
target.z,
places=5,
msg="Final plunge should go to target Z=-2",
)
@unittest.skip("not yet implemented")
def test_zero_retract_offset_uses_local_clearance(self):
cmds = generator.get_linking_moves(

View File

@@ -79,9 +79,23 @@ def get_linking_moves(
wire = make_linking_wire(start_position, target_position, height)
if is_wire_collision_free(wire, collision_model):
cmds = Path.fromShape(wire).Commands
for cmd in cmds:
cmd.Name = "G0"
return cmds
# Ensure all commands have complete XYZ coordinates
# Path.fromShape() may omit coordinates that don't change
current_pos = start_position
complete_cmds = []
for i, cmd in enumerate(cmds):
params = dict(cmd.Parameters)
# Fill in missing coordinates from current position
x = params.get("X", current_pos.x)
y = params.get("Y", current_pos.y)
# For the last command (plunge to target), use target.z if Z is missing
if "Z" not in params and i == len(cmds) - 1:
z = target_position.z
else:
z = params.get("Z", current_pos.z)
complete_cmds.append(Path.Command("G0", {"X": x, "Y": y, "Z": z}))
current_pos = Vector(x, y, z)
return complete_cmds
raise RuntimeError("No collision-free path found between start and target positions")

View File

@@ -508,13 +508,10 @@ class ObjectMillFacing(PathOp.ObjectOp):
for k in ("X", "Y")
):
# But if Z is different, keep it (it's a plunge or retract)
z_changed = (
abs(
new_params.get("Z", 0)
- last_params.get("Z", new_params.get("Z", 0) + 1)
)
> 1e-9
)
# Use sentinel values that won't conflict with depth == 0
z_new = new_params.get("Z", float("inf"))
z_last = last_params.get("Z", float("-inf"))
z_changed = abs(z_new - z_last) > 1e-9
if not z_changed:
continue
self.commandlist.append(Path.Command(cmd.Name, new_params))
@@ -596,11 +593,11 @@ class ObjectMillFacing(PathOp.ObjectOp):
# Append linking moves, ensuring full XYZ continuity
current = last_position
for lc in link_commands:
params = dict(lc.Parameters)
X = params.get("X", current.x)
Y = params.get("Y", current.y)
Z = params.get("Z", current.z)
# Skip zero-length
params = lc.Parameters
X = params["X"]
Y = params["Y"]
Z = params["Z"]
# Skip zero-length moves
if not (
abs(X - current.x) <= 1e-9
and abs(Y - current.y) <= 1e-9
@@ -609,7 +606,7 @@ class ObjectMillFacing(PathOp.ObjectOp):
self.commandlist.append(
Path.Command(lc.Name, {"X": X, "Y": Y, "Z": Z})
)
current = FreeCAD.Vector(X, Y, Z)
current = FreeCAD.Vector(X, Y, Z)
# Remove the entire initial G0 bundle (up, XY, down) from the copy
del copy_commands[bundle_start:bundle_end]
@@ -631,11 +628,13 @@ class ObjectMillFacing(PathOp.ObjectOp):
cp["Z"] = depth # Cutting moves at depth
else:
cp.setdefault("Z", last.get("Z"))
# Skip zero-length
# Skip zero-length moves
if self.commandlist:
last = self.commandlist[-1].Parameters
# Use sentinel values that won't conflict with depth == 0
if all(
abs(cp[k] - last.get(k, cp[k] + 1)) <= 1e-9 for k in ("X", "Y", "Z")
abs(cp.get(k, float("inf")) - last.get(k, float("-inf"))) <= 1e-9
for k in ("X", "Y", "Z")
):
continue
self.commandlist.append(Path.Command(cc.Name, cp))