* CAM: Drilling - Fix regression after #26584 * CAM: Linking generator - Fix machinestate handling Fix linking generator for drilling * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add remaining logic to terminate canned cycles appropriately (#2) * sorted heights * Refactor buildPostList Introduces early tool prep to the buildpostables. Allows emitting a T command early to give tool changer time to index the tool before the M6 tool change command is emitted. updates unit testing for this use case Not implemented in postprocessors yet. fix * Helper and tests for terminating canned cycles fix cycle termination * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * CAM: setFeedRate now aware of drilling semantics (#3) Remove retract plane property machinestate properly handling z position * next bugs (#4) * setFeedRate now aware of drilling semantics Remove retract plane property machinestate properly handling z position * unnecessary import. Improved linking * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix rendering of canned cycles in G99 mode (#5) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix for collision (#6) * collision bug fix * fix local variable * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: sliptonic <shopinthewoods@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -129,6 +129,7 @@ void PathSegmentVisitor::g38(int id, const Base::Vector3d& last, const Base::Vec
|
||||
|
||||
PathSegmentWalker::PathSegmentWalker(const Toolpath& tp_)
|
||||
: tp(tp_)
|
||||
, retract_mode(98) // Default to G98 (retract to initial Z)
|
||||
{}
|
||||
|
||||
|
||||
@@ -331,6 +332,18 @@ void PathSegmentWalker::walk(PathSegmentVisitor& cb, const Base::Vector3d& start
|
||||
|| (name == "G83") || (name == "G84") || (name == "G85") || (name == "G86")
|
||||
|| (name == "G89")) {
|
||||
// drill,tap,bore
|
||||
|
||||
// Check for RetractMode annotation (G98 or G99)
|
||||
if (cmd.hasAnnotation("RetractMode")) {
|
||||
std::string mode = cmd.getAnnotationString("RetractMode");
|
||||
if (mode == "G99") {
|
||||
retract_mode = 99;
|
||||
}
|
||||
else if (mode == "G98") {
|
||||
retract_mode = 98;
|
||||
}
|
||||
}
|
||||
|
||||
double r = 0;
|
||||
if (cmd.has("R")) {
|
||||
r = cmd.getValue("R");
|
||||
|
||||
@@ -30,6 +30,7 @@ import Path
|
||||
import Path.Post.Command as PathCommand
|
||||
import Path.Post.Processor as PathPost
|
||||
import Path.Post.Utils as PostUtils
|
||||
import Path.Post.UtilsExport as PostUtilsExport
|
||||
import Path.Main.Job as PathJob
|
||||
import Path.Tool.Controller as PathToolController
|
||||
import difflib
|
||||
@@ -503,6 +504,232 @@ class TestPathPostUtils(unittest.TestCase):
|
||||
# self.assertTrue(len(results.Commands) == 117)
|
||||
self.assertTrue(len([c for c in results.Commands if c.Name in ["G2", "G3"]]) == 0)
|
||||
|
||||
def test020(self):
|
||||
"""Test Termination of Canned Cycles"""
|
||||
# Test basic cycle termination when parameters change
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -1.0, "R": 0.2, "F": 10.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
test_path = Path.Path(
|
||||
[
|
||||
Path.Command("G0", {"Z": 1.0}),
|
||||
cmd1,
|
||||
cmd2, # Different Z depth
|
||||
Path.Command("G1", {"X": 3.0, "Y": 3.0}),
|
||||
]
|
||||
)
|
||||
|
||||
expected_path = Path.Path(
|
||||
[
|
||||
Path.Command("G0", {"Z": 1.0}),
|
||||
Path.Command("G98"), # Retract mode for first cycle
|
||||
Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}),
|
||||
Path.Command("G80"), # Terminate due to parameter change
|
||||
Path.Command("G98"), # Retract mode for second cycle
|
||||
Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -1.0, "R": 0.2, "F": 10.0}),
|
||||
Path.Command("G80"), # Final termination
|
||||
Path.Command("G1", {"X": 3.0, "Y": 3.0}),
|
||||
]
|
||||
)
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
|
||||
self.assertEqual(len(result.Commands), len(expected_path.Commands))
|
||||
for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)):
|
||||
self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch")
|
||||
self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch")
|
||||
|
||||
def test030_canned_cycle_termination_with_non_cycle_commands(self):
|
||||
"""Test cycle termination when non-cycle commands are encountered"""
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
cmd2 = Path.Command("G82", {"X": 3.0, "Y": 3.0, "Z": -1.0, "R": 0.2, "P": 1.0, "F": 10.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
test_path = Path.Path(
|
||||
[
|
||||
cmd1,
|
||||
Path.Command("G0", {"X": 2.0, "Y": 2.0}), # Non-cycle command
|
||||
cmd2,
|
||||
]
|
||||
)
|
||||
|
||||
expected_path = Path.Path(
|
||||
[
|
||||
Path.Command("G98"), # Retract mode for first cycle
|
||||
Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}),
|
||||
Path.Command("G80"), # Terminate before non-cycle command
|
||||
Path.Command("G0", {"X": 2.0, "Y": 2.0}),
|
||||
Path.Command("G98"), # Retract mode for second cycle
|
||||
Path.Command("G82", {"X": 3.0, "Y": 3.0, "Z": -1.0, "R": 0.2, "P": 1.0, "F": 10.0}),
|
||||
Path.Command("G80"), # Final termination
|
||||
]
|
||||
)
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
self.assertEqual(len(result.Commands), len(expected_path.Commands))
|
||||
for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)):
|
||||
self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch")
|
||||
self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch")
|
||||
|
||||
def test040_canned_cycle_modal_same_parameters(self):
|
||||
"""Test modal cycles with same parameters don't get terminated"""
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
cmd3 = Path.Command("G81", {"X": 3.0, "Y": 3.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd3.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
test_path = Path.Path(
|
||||
[
|
||||
cmd1,
|
||||
cmd2, # Modal - same parameters
|
||||
cmd3, # Modal - same parameters
|
||||
]
|
||||
)
|
||||
|
||||
expected_path = Path.Path(
|
||||
[
|
||||
Path.Command("G98"), # Retract mode at start of cycle
|
||||
Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}),
|
||||
Path.Command(
|
||||
"G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 10.0}
|
||||
), # No termination - same params
|
||||
Path.Command(
|
||||
"G81", {"X": 3.0, "Y": 3.0, "Z": -0.5, "R": 0.1, "F": 10.0}
|
||||
), # No termination - same params
|
||||
Path.Command("G80"), # Final termination
|
||||
]
|
||||
)
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
self.assertEqual(len(result.Commands), len(expected_path.Commands))
|
||||
for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)):
|
||||
self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch")
|
||||
self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch")
|
||||
|
||||
def test050_canned_cycle_feed_rate_change(self):
|
||||
"""Test cycle termination when feed rate changes"""
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 20.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
test_path = Path.Path(
|
||||
[
|
||||
cmd1,
|
||||
cmd2, # Different feed rate
|
||||
]
|
||||
)
|
||||
|
||||
expected_path = Path.Path(
|
||||
[
|
||||
Path.Command("G98"), # Retract mode for first cycle
|
||||
Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}),
|
||||
Path.Command("G80"), # Terminate due to feed rate change
|
||||
Path.Command("G98"), # Retract mode for second cycle
|
||||
Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 20.0}),
|
||||
Path.Command("G80"), # Final termination
|
||||
]
|
||||
)
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
self.assertEqual(len(result.Commands), len(expected_path.Commands))
|
||||
for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)):
|
||||
self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch")
|
||||
self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch")
|
||||
|
||||
def test060_canned_cycle_retract_plane_change(self):
|
||||
"""Test cycle termination when retract plane changes"""
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.2, "F": 10.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
test_path = Path.Path(
|
||||
[
|
||||
cmd1,
|
||||
cmd2, # Different R plane
|
||||
]
|
||||
)
|
||||
|
||||
expected_path = Path.Path(
|
||||
[
|
||||
Path.Command("G98"), # Retract mode for first cycle
|
||||
Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}),
|
||||
Path.Command("G80"), # Terminate due to R plane change
|
||||
Path.Command("G98"), # Retract mode for second cycle
|
||||
Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.2, "F": 10.0}),
|
||||
Path.Command("G80"), # Final termination
|
||||
]
|
||||
)
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
self.assertEqual(len(result.Commands), len(expected_path.Commands))
|
||||
for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)):
|
||||
self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch")
|
||||
self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch")
|
||||
|
||||
def test070_canned_cycle_mixed_cycle_types(self):
|
||||
"""Test termination between different cycle types"""
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
cmd2 = Path.Command("G82", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "P": 1.0, "F": 10.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
test_path = Path.Path(
|
||||
[
|
||||
cmd1,
|
||||
cmd2, # Different cycle type
|
||||
]
|
||||
)
|
||||
|
||||
expected_path = Path.Path(
|
||||
[
|
||||
Path.Command("G98"), # Retract mode for first cycle
|
||||
Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0}),
|
||||
Path.Command("G80"), # Terminate due to different cycle type (different parameters)
|
||||
Path.Command("G98"), # Retract mode for second cycle
|
||||
Path.Command("G82", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "P": 1.0, "F": 10.0}),
|
||||
Path.Command("G80"), # Final termination
|
||||
]
|
||||
)
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
self.assertEqual(len(result.Commands), len(expected_path.Commands))
|
||||
for i, (res, exp) in enumerate(zip(result.Commands, expected_path.Commands)):
|
||||
self.assertEqual(res.Name, exp.Name, f"Command {i}: name mismatch")
|
||||
self.assertEqual(res.Parameters, exp.Parameters, f"Command {i}: parameters mismatch")
|
||||
|
||||
def test080_canned_cycle_retract_mode_change(self):
|
||||
"""Test cycle termination and retract mode insertion when RetractMode annotation changes"""
|
||||
# Create commands with RetractMode annotations
|
||||
cmd1 = Path.Command("G81", {"X": 1.0, "Y": 1.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd1.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
cmd2 = Path.Command("G81", {"X": 2.0, "Y": 2.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd2.Annotations = {"RetractMode": "G98"}
|
||||
|
||||
cmd3 = Path.Command("G81", {"X": 3.0, "Y": 3.0, "Z": -0.5, "R": 0.1, "F": 10.0})
|
||||
cmd3.Annotations = {"RetractMode": "G99"} # Mode change
|
||||
|
||||
test_path = Path.Path([cmd1, cmd2, cmd3])
|
||||
|
||||
result = PostUtils.cannedCycleTerminator(test_path)
|
||||
|
||||
# Expected: G98, G81, G81 (modal), G80 (terminate), G99, G81, G80 (final)
|
||||
self.assertEqual(result.Commands[0].Name, "G98")
|
||||
self.assertEqual(result.Commands[1].Name, "G81")
|
||||
self.assertEqual(result.Commands[2].Name, "G81")
|
||||
self.assertEqual(result.Commands[3].Name, "G80") # Terminate due to mode change
|
||||
self.assertEqual(result.Commands[4].Name, "G99") # New retract mode
|
||||
self.assertEqual(result.Commands[5].Name, "G81")
|
||||
self.assertEqual(result.Commands[6].Name, "G80") # Final termination
|
||||
self.assertEqual(len(result.Commands), 7)
|
||||
|
||||
|
||||
class TestBuildPostList(unittest.TestCase):
|
||||
"""
|
||||
@@ -523,6 +750,110 @@ class TestBuildPostList(unittest.TestCase):
|
||||
|
||||
"""
|
||||
|
||||
# Set to True to enable verbose debug output for test validation
|
||||
debug = False
|
||||
|
||||
@classmethod
|
||||
def _format_postables(cls, postables, title="Postables"):
|
||||
"""Format postables for readable debug output, following dumper_post.py pattern."""
|
||||
output = []
|
||||
output.append("=" * 80)
|
||||
output.append(title)
|
||||
output.append("=" * 80)
|
||||
output.append("")
|
||||
|
||||
for idx, postable in enumerate(postables, 1):
|
||||
group_key = postable[0]
|
||||
objects = postable[1]
|
||||
|
||||
# Format the group key display
|
||||
if group_key == "":
|
||||
display_key = "(empty string)"
|
||||
elif group_key == "allitems":
|
||||
display_key = '"allitems" (combined output)'
|
||||
else:
|
||||
display_key = f'"{group_key}"'
|
||||
|
||||
output.append(f"[{idx}] Group: {display_key}")
|
||||
output.append(f" Objects: {len(objects)}")
|
||||
output.append("")
|
||||
|
||||
for obj_idx, obj in enumerate(objects, 1):
|
||||
obj_label = getattr(obj, "Label", str(type(obj).__name__))
|
||||
output.append(f" [{obj_idx}] {obj_label}")
|
||||
|
||||
# Determine object type/role
|
||||
obj_type = type(obj).__name__
|
||||
if obj_type == "_FixtureSetupObject":
|
||||
output.append(f" Type: Fixture Setup")
|
||||
if hasattr(obj, "Path") and obj.Path and len(obj.Path.Commands) > 0:
|
||||
fixture_cmd = obj.Path.Commands[0]
|
||||
output.append(f" Fixture: {fixture_cmd.Name}")
|
||||
elif obj_type == "_CommandObject":
|
||||
output.append(f" Type: Command Object")
|
||||
if hasattr(obj, "Path") and obj.Path and len(obj.Path.Commands) > 0:
|
||||
cmd = obj.Path.Commands[0]
|
||||
params = " ".join(
|
||||
f"{k}:{v}"
|
||||
for k, v in zip(
|
||||
cmd.Parameters.keys() if hasattr(cmd.Parameters, "keys") else [],
|
||||
(
|
||||
cmd.Parameters.values()
|
||||
if hasattr(cmd.Parameters, "values")
|
||||
else cmd.Parameters
|
||||
),
|
||||
)
|
||||
)
|
||||
output.append(f" Command: {cmd.Name} {params}")
|
||||
elif hasattr(obj, "TypeId"):
|
||||
# Check if it's a tool controller
|
||||
if hasattr(obj, "Proxy") and hasattr(obj.Proxy, "__class__"):
|
||||
proxy_name = obj.Proxy.__class__.__name__
|
||||
if "ToolController" in proxy_name:
|
||||
output.append(f" Type: Tool Controller")
|
||||
if hasattr(obj, "ToolNumber"):
|
||||
output.append(f" Tool Number: {obj.ToolNumber}")
|
||||
if hasattr(obj, "Path") and obj.Path and obj.Path.Commands:
|
||||
for cmd in obj.Path.Commands:
|
||||
if cmd.Name == "M6":
|
||||
params = " ".join(
|
||||
f"{k}:{v}"
|
||||
for k, v in zip(
|
||||
(
|
||||
cmd.Parameters.keys()
|
||||
if hasattr(cmd.Parameters, "keys")
|
||||
else []
|
||||
),
|
||||
(
|
||||
cmd.Parameters.values()
|
||||
if hasattr(cmd.Parameters, "values")
|
||||
else cmd.Parameters
|
||||
),
|
||||
)
|
||||
)
|
||||
output.append(f" M6 Command: {cmd.Name} {params}")
|
||||
else:
|
||||
output.append(f" Type: Operation")
|
||||
if hasattr(obj, "ToolController") and obj.ToolController:
|
||||
tc = obj.ToolController
|
||||
output.append(
|
||||
f" ToolController: {tc.Label} (T{tc.ToolNumber})"
|
||||
)
|
||||
else:
|
||||
output.append(f" Type: {obj.TypeId}")
|
||||
else:
|
||||
output.append(f" Type: {obj_type}")
|
||||
|
||||
output.append("")
|
||||
|
||||
output.append("=" * 80)
|
||||
output.append(f"Total Groups: {len(postables)}")
|
||||
total_objects = sum(len(p[1]) for p in postables)
|
||||
output.append(f"Total Objects: {total_objects}")
|
||||
output.append("=" * 80)
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
|
||||
@@ -560,6 +891,10 @@ class TestBuildPostList(unittest.TestCase):
|
||||
tc2.Label = 'TC: 7/16" two flute' # Same label as first tool controller
|
||||
cls.job.Proxy.addToolController(tc2)
|
||||
|
||||
# Recompute tool controllers to populate their Path.Commands with M6 commands
|
||||
cls.job.Tools.Group[0].recompute()
|
||||
cls.job.Tools.Group[1].recompute()
|
||||
|
||||
# Create mock operations to match original file structure
|
||||
# Original had 3 operations: outsideprofile, DrillAllHoles, Comment
|
||||
# The Comment operation has no tool controller
|
||||
@@ -638,8 +973,8 @@ class TestBuildPostList(unittest.TestCase):
|
||||
postlist = self.pp._buildPostList()
|
||||
firstoutputitem = postlist[0]
|
||||
firstoplist = firstoutputitem[1]
|
||||
print(f"DEBUG test030: postlist length={len(firstoplist)}, expected=14")
|
||||
print(f"DEBUG test030: firstoplist={[str(item) for item in firstoplist]}")
|
||||
if self.debug:
|
||||
print(self._format_postables(postlist, "test030: No splitting, order by Operation"))
|
||||
self.assertEqual(len(firstoplist), 14)
|
||||
|
||||
def test040(self):
|
||||
@@ -652,13 +987,12 @@ class TestBuildPostList(unittest.TestCase):
|
||||
postlist = self.pp._buildPostList()
|
||||
|
||||
firstoutputitem = postlist[0]
|
||||
print(f"DEBUG test040: firstoutputitem[0]={firstoutputitem[0]}, expected='5'")
|
||||
print(f"DEBUG test040: tool numbers={[tc.ToolNumber for tc in self.job.Tools.Group]}")
|
||||
if self.debug:
|
||||
print(self._format_postables(postlist, "test040: Split by tool, order by Tool"))
|
||||
self.assertTrue(firstoutputitem[0] == str(5))
|
||||
|
||||
# check length of output
|
||||
firstoplist = firstoutputitem[1]
|
||||
print(f"DEBUG test040: postlist length={len(firstoplist)}, expected=5")
|
||||
self.assertEqual(len(firstoplist), 5)
|
||||
|
||||
def test050(self):
|
||||
@@ -684,3 +1018,142 @@ class TestBuildPostList(unittest.TestCase):
|
||||
firstoplist = firstoutputitem[1]
|
||||
self.assertEqual(len(firstoplist), 6)
|
||||
self.assertTrue(firstoutputitem[0] == "G54")
|
||||
|
||||
def test070(self):
|
||||
self.job.SplitOutput = True
|
||||
self.job.PostProcessorOutputFile = "%T.nc"
|
||||
self.job.OrderOutputBy = "Tool"
|
||||
postables = self.pp._buildPostList(early_tool_prep=True)
|
||||
_, sublist = postables[0]
|
||||
|
||||
if self.debug:
|
||||
print(self._format_postables(postables, "test070: Early tool prep, split by tool"))
|
||||
|
||||
# Extract all commands from the postables
|
||||
commands = []
|
||||
if self.debug:
|
||||
print("\n=== Extracting commands from postables ===")
|
||||
for item in sublist:
|
||||
if self.debug:
|
||||
item_type = type(item).__name__
|
||||
has_path = hasattr(item, "Path")
|
||||
path_exists = item.Path if has_path else None
|
||||
has_commands = path_exists and item.Path.Commands if path_exists else False
|
||||
print(
|
||||
f"Item: {getattr(item, 'Label', item_type)}, Type: {item_type}, HasPath: {has_path}, PathExists: {path_exists is not None}, HasCommands: {bool(has_commands)}"
|
||||
)
|
||||
if has_commands:
|
||||
print(f" Commands: {[cmd.Name for cmd in item.Path.Commands]}")
|
||||
if hasattr(item, "Path") and item.Path and item.Path.Commands:
|
||||
commands.extend(item.Path.Commands)
|
||||
|
||||
if self.debug:
|
||||
print(f"\nTotal commands extracted: {len(commands)}")
|
||||
print("=" * 40)
|
||||
|
||||
# Should have M6 command with tool parameter
|
||||
m6_commands = [cmd for cmd in commands if cmd.Name == "M6"]
|
||||
self.assertTrue(len(m6_commands) > 0, "Should have M6 command")
|
||||
|
||||
# First M6 should have T parameter for tool 5
|
||||
first_m6 = m6_commands[0]
|
||||
self.assertTrue("T" in first_m6.Parameters, "First M6 should have T parameter")
|
||||
self.assertEqual(first_m6.Parameters["T"], 5.0, "First M6 should be for tool 5")
|
||||
|
||||
# Should have T2 prep command (early prep for next tool)
|
||||
t2_commands = [cmd for cmd in commands if cmd.Name == "T2"]
|
||||
self.assertTrue(len(t2_commands) > 0, "Should have T2 early prep command")
|
||||
|
||||
# T2 prep should come after first M6
|
||||
first_m6_index = next((i for i, cmd in enumerate(commands) if cmd.Name == "M6"), None)
|
||||
t2_index = next((i for i, cmd in enumerate(commands) if cmd.Name == "T2"), None)
|
||||
self.assertIsNotNone(first_m6_index, "M6 should exist")
|
||||
self.assertIsNotNone(t2_index, "T2 should exist")
|
||||
self.assertLess(first_m6_index, t2_index, "M6 should come before T2 prep")
|
||||
|
||||
def test080(self):
|
||||
self.job.SplitOutput = False
|
||||
self.job.OrderOutputBy = "Tool"
|
||||
|
||||
postables = self.pp._buildPostList(early_tool_prep=True)
|
||||
_, sublist = postables[0]
|
||||
|
||||
if self.debug:
|
||||
print(self._format_postables(postables, "test080: Early tool prep, combined output"))
|
||||
|
||||
# Extract all commands from the postables
|
||||
commands = []
|
||||
if self.debug:
|
||||
print("\n=== Extracting commands from postables ===")
|
||||
for item in sublist:
|
||||
if self.debug:
|
||||
item_type = type(item).__name__
|
||||
has_path = hasattr(item, "Path")
|
||||
path_exists = item.Path if has_path else None
|
||||
has_commands = path_exists and item.Path.Commands if path_exists else False
|
||||
print(
|
||||
f"Item: {getattr(item, 'Label', item_type)}, Type: {item_type}, HasPath: {has_path}, PathExists: {path_exists is not None}, HasCommands: {bool(has_commands)}"
|
||||
)
|
||||
if has_commands:
|
||||
print(f" Commands: {[cmd.Name for cmd in item.Path.Commands]}")
|
||||
if hasattr(item, "Path") and item.Path and item.Path.Commands:
|
||||
commands.extend(item.Path.Commands)
|
||||
|
||||
if self.debug:
|
||||
print(f"\nTotal commands extracted: {len(commands)}")
|
||||
|
||||
# Expected command sequence with early_tool_prep=True:
|
||||
# M6 T5 <- change to tool 5 (standard format)
|
||||
# T2 <- prep next tool immediately (early prep)
|
||||
# (ops with T5...)
|
||||
# M6 T2 <- change to tool 2 (was prepped early)
|
||||
# (ops with T2...)
|
||||
|
||||
if self.debug:
|
||||
print("\n=== Command Sequence ===")
|
||||
for i, cmd in enumerate(commands):
|
||||
params = " ".join(
|
||||
f"{k}:{v}"
|
||||
for k, v in zip(
|
||||
cmd.Parameters.keys() if hasattr(cmd.Parameters, "keys") else [],
|
||||
(
|
||||
cmd.Parameters.values()
|
||||
if hasattr(cmd.Parameters, "values")
|
||||
else cmd.Parameters
|
||||
),
|
||||
)
|
||||
)
|
||||
print(f"{i:3d}: {cmd.Name} {params}")
|
||||
print("=" * 40)
|
||||
|
||||
# Find M6 and T2 commands
|
||||
m6_commands = [(i, cmd) for i, cmd in enumerate(commands) if cmd.Name == "M6"]
|
||||
t2_commands = [(i, cmd) for i, cmd in enumerate(commands) if cmd.Name == "T2"]
|
||||
|
||||
self.assertTrue(len(m6_commands) >= 2, "Should have at least 2 M6 commands")
|
||||
self.assertTrue(len(t2_commands) >= 1, "Should have at least 1 T2 early prep command")
|
||||
|
||||
first_m6_idx, first_m6_cmd = m6_commands[0]
|
||||
second_m6_idx, second_m6_cmd = m6_commands[1] if len(m6_commands) >= 2 else (None, None)
|
||||
first_t2_idx = t2_commands[0][0]
|
||||
|
||||
# First M6 should have T parameter for tool 5
|
||||
self.assertTrue("T" in first_m6_cmd.Parameters, "First M6 should have T parameter")
|
||||
self.assertEqual(first_m6_cmd.Parameters["T"], 5.0, "First M6 should be for tool 5")
|
||||
|
||||
# Second M6 should have T parameter for tool 2
|
||||
if second_m6_cmd is not None:
|
||||
self.assertTrue("T" in second_m6_cmd.Parameters, "Second M6 should have T parameter")
|
||||
self.assertEqual(second_m6_cmd.Parameters["T"], 2.0, "Second M6 should be for tool 2")
|
||||
|
||||
# T2 (early prep) should come shortly after first M6 (within a few commands)
|
||||
self.assertLess(first_m6_idx, first_t2_idx, "T2 prep should come after first M6")
|
||||
self.assertLess(
|
||||
first_t2_idx - first_m6_idx, 5, "T2 prep should be within a few commands of first M6"
|
||||
)
|
||||
|
||||
# T2 early prep should come before second M6
|
||||
if second_m6_idx is not None:
|
||||
self.assertLess(
|
||||
first_t2_idx, second_m6_idx, "T2 early prep should come before second M6"
|
||||
)
|
||||
|
||||
@@ -713,12 +713,13 @@ class TestPostGCodes(PathTestUtils.PathTestBase):
|
||||
|
||||
def test10730(self):
|
||||
"""Test G73 command Generation."""
|
||||
cmd = Path.Command("G73 X1 Y2 Z0 F123 Q1.5 R5")
|
||||
cmd.Annotations = {"RetractMode": "G99"}
|
||||
path = [
|
||||
Path.Command("G0 X1 Y2"),
|
||||
Path.Command("G0 Z8"),
|
||||
Path.Command("G90"),
|
||||
Path.Command("G99"),
|
||||
Path.Command("G73 X1 Y2 Z0 F123 Q1.5 R5"),
|
||||
cmd,
|
||||
Path.Command("G80"),
|
||||
Path.Command("G90"),
|
||||
]
|
||||
@@ -905,12 +906,14 @@ G90
|
||||
|
||||
def test10810(self):
|
||||
"""Test G81 command Generation."""
|
||||
cmd = Path.Command("G81 X1 Y2 Z0 F123 R5")
|
||||
cmd.Annotations = {"RetractMode": "G99"}
|
||||
path = [
|
||||
Path.Command("G0 X1 Y2"),
|
||||
Path.Command("G0 Z8"),
|
||||
Path.Command("G90"),
|
||||
Path.Command("G99"),
|
||||
Path.Command("G81 X1 Y2 Z0 F123 R5"),
|
||||
cmd,
|
||||
Path.Command("G80"),
|
||||
Path.Command("G90"),
|
||||
]
|
||||
@@ -1061,12 +1064,14 @@ G90
|
||||
|
||||
def test10820(self):
|
||||
"""Test G82 command Generation."""
|
||||
cmd = Path.Command("G82 X1 Y2 Z0 F123 R5 P1.23456")
|
||||
cmd.Annotations = {"RetractMode": "G99"}
|
||||
path = [
|
||||
Path.Command("G0 X1 Y2"),
|
||||
Path.Command("G0 Z8"),
|
||||
Path.Command("G90"),
|
||||
Path.Command("G99"),
|
||||
Path.Command("G82 X1 Y2 Z0 F123 R5 P1.23456"),
|
||||
cmd,
|
||||
Path.Command("G80"),
|
||||
Path.Command("G90"),
|
||||
]
|
||||
@@ -1221,12 +1226,14 @@ G90
|
||||
|
||||
def test10830(self):
|
||||
"""Test G83 command Generation."""
|
||||
cmd = Path.Command("G83 X1 Y2 Z0 F123 Q1.5 R5")
|
||||
cmd.Annotations = {"RetractMode": "G99"}
|
||||
path = [
|
||||
Path.Command("G0 X1 Y2"),
|
||||
Path.Command("G0 Z8"),
|
||||
Path.Command("G90"),
|
||||
Path.Command("G99"),
|
||||
Path.Command("G83 X1 Y2 Z0 F123 Q1.5 R5"),
|
||||
cmd,
|
||||
Path.Command("G80"),
|
||||
Path.Command("G90"),
|
||||
]
|
||||
|
||||
@@ -202,12 +202,14 @@ G54
|
||||
|
||||
def test00125(self) -> None:
|
||||
"""Test chipbreaking amount."""
|
||||
cmd = Path.Command("G73 X1 Y2 Z0 F123 Q1.5 R5")
|
||||
cmd.Annotations = {"RetractMode": "G99"}
|
||||
test_path = [
|
||||
Path.Command("G0 X1 Y2"),
|
||||
Path.Command("G0 Z8"),
|
||||
Path.Command("G90"),
|
||||
Path.Command("G99"),
|
||||
Path.Command("G73 X1 Y2 Z0 F123 Q1.5 R5"),
|
||||
cmd,
|
||||
Path.Command("G80"),
|
||||
Path.Command("G90"),
|
||||
]
|
||||
|
||||
@@ -295,6 +295,7 @@ SET(PathPythonToolsShapeUi_SRCS
|
||||
SET(PathPythonPost_SRCS
|
||||
Path/Post/__init__.py
|
||||
Path/Post/Command.py
|
||||
Path/Post/PostList.py
|
||||
Path/Post/Processor.py
|
||||
Path/Post/Utils.py
|
||||
Path/Post/UtilsArguments.py
|
||||
|
||||
@@ -153,23 +153,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="4">
|
||||
<widget class="QLabel" name="retractLabel">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Retract</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="6">
|
||||
<widget class="Gui::QuantitySpinBox" name="peckRetractHeight">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="chipBreakEnabled">
|
||||
<property name="text">
|
||||
|
||||
@@ -24,6 +24,7 @@ import FreeCAD
|
||||
import Path
|
||||
import Path.Base.MachineState as PathMachineState
|
||||
import Part
|
||||
from Path.Geom import CmdMoveDrill
|
||||
|
||||
__title__ = "Feed Rate Helper Utility"
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
@@ -63,7 +64,12 @@ def setFeedRate(commandlist, ToolController):
|
||||
if command.Name not in Path.Geom.CmdMoveAll:
|
||||
continue
|
||||
|
||||
if _isVertical(machine.getPosition(), command):
|
||||
# Canned drill cycles (G73, G81, G82, G83, G85) are vertical cutting operations
|
||||
# The F word in a drill cycle specifies the feed rate for the vertical cutting component
|
||||
# The positioning move to XY is done at rapid speed (not controlled by F word)
|
||||
if command.Name in Path.Geom.CmdMoveDrill:
|
||||
rate = ToolController.VertFeed.Value
|
||||
elif _isVertical(machine.getPosition(), command):
|
||||
rate = (
|
||||
ToolController.VertRapid.Value
|
||||
if command.Name in Path.Geom.CmdMoveRapid
|
||||
|
||||
@@ -34,6 +34,37 @@ else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
def check_collision(
|
||||
start_position: Vector,
|
||||
target_position: Vector,
|
||||
solids: Optional[List[Part.Shape]] = None,
|
||||
tolerance: float = 0.001,
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a direct move from start to target would collide with solids.
|
||||
Returns True if collision detected, False if path is clear.
|
||||
"""
|
||||
if start_position == target_position:
|
||||
return False
|
||||
|
||||
# Build collision model
|
||||
collision_model = None
|
||||
if solids:
|
||||
solids = [s for s in solids if s]
|
||||
if len(solids) == 1:
|
||||
collision_model = solids[0]
|
||||
elif len(solids) > 1:
|
||||
collision_model = Part.makeCompound(solids)
|
||||
|
||||
if not collision_model:
|
||||
return False
|
||||
|
||||
# Create direct path wire
|
||||
wire = Part.Wire([Part.makeLine(start_position, target_position)])
|
||||
distance = wire.distToShape(collision_model)[0]
|
||||
return distance < tolerance
|
||||
|
||||
|
||||
def get_linking_moves(
|
||||
start_position: Vector,
|
||||
target_position: Vector,
|
||||
@@ -42,10 +73,23 @@ def get_linking_moves(
|
||||
tool_shape: Part.Shape, # required placeholder
|
||||
solids: Optional[List[Part.Shape]] = None,
|
||||
retract_height_offset: Optional[float] = None,
|
||||
skip_if_no_collision: bool = False,
|
||||
) -> list:
|
||||
"""
|
||||
Generate linking moves from start to target position.
|
||||
|
||||
If skip_if_no_collision is True and the direct path at the current height
|
||||
is collision-free, returns empty list (useful for canned drill cycles that
|
||||
handle their own retraction).
|
||||
"""
|
||||
if start_position == target_position:
|
||||
return []
|
||||
|
||||
# For canned cycles: if we're already at a safe height and can move directly, skip linking
|
||||
if skip_if_no_collision:
|
||||
if not check_collision(start_position, target_position, solids):
|
||||
return []
|
||||
|
||||
if local_clearance > global_clearance:
|
||||
raise ValueError("Local clearance must not exceed global clearance")
|
||||
|
||||
@@ -59,7 +103,7 @@ def get_linking_moves(
|
||||
if len(solids) == 1:
|
||||
collision_model = solids[0]
|
||||
elif len(solids) > 1:
|
||||
collision_model = Part.makeFuse(solids)
|
||||
collision_model = Part.makeCompound(solids)
|
||||
|
||||
# Determine candidate heights
|
||||
if retract_height_offset is not None:
|
||||
@@ -72,7 +116,7 @@ def get_linking_moves(
|
||||
else:
|
||||
candidate_heights = {local_clearance, global_clearance}
|
||||
|
||||
heights = sorted(candidate_heights, reverse=True)
|
||||
heights = sorted(candidate_heights)
|
||||
|
||||
# Try each height
|
||||
for height in heights:
|
||||
@@ -103,10 +147,21 @@ def get_linking_moves(
|
||||
def make_linking_wire(start: Vector, target: Vector, z: float) -> Part.Wire:
|
||||
p1 = Vector(start.x, start.y, z)
|
||||
p2 = Vector(target.x, target.y, z)
|
||||
e1 = Part.makeLine(start, p1)
|
||||
e2 = Part.makeLine(p1, p2)
|
||||
e3 = Part.makeLine(p2, target)
|
||||
return Part.Wire([e1, e2, e3])
|
||||
edges = []
|
||||
|
||||
# Only add retract edge if there's actual movement
|
||||
if not start.isEqual(p1, 1e-6):
|
||||
edges.append(Part.makeLine(start, p1))
|
||||
|
||||
# Only add traverse edge if there's actual movement
|
||||
if not p1.isEqual(p2, 1e-6):
|
||||
edges.append(Part.makeLine(p1, p2))
|
||||
|
||||
# Only add plunge edge if there's actual movement
|
||||
if not p2.isEqual(target, 1e-6):
|
||||
edges.append(Part.makeLine(p2, target))
|
||||
|
||||
return Part.Wire(edges) if edges else Part.Wire([Part.makeLine(start, target)])
|
||||
|
||||
|
||||
def is_wire_collision_free(
|
||||
|
||||
@@ -92,24 +92,40 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
return PathOp.FeatureBaseGeometry | PathOp.FeatureLocations | PathOp.FeatureCoolant
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
if not hasattr(obj, "chipBreakEnabled"):
|
||||
if hasattr(obj, "chipBreakEnabled"):
|
||||
obj.renameProperty("chipBreakEnabled", "ChipBreakEnabled")
|
||||
elif not hasattr(obj, "ChipBreakEnabled"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"chipBreakEnabled",
|
||||
"ChipBreakEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use chipbreaking"),
|
||||
)
|
||||
|
||||
if not hasattr(obj, "feedRetractEnabled"):
|
||||
if hasattr(obj, "feedRetractEnabled"):
|
||||
obj.renameProperty("feedRetractEnabled", "FeedRetractEnabled")
|
||||
elif not hasattr(obj, "FeedRetractEnabled"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"feedRetractEnabled",
|
||||
"FeedRetractEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use G85 boring cycle with feed out"),
|
||||
)
|
||||
|
||||
if hasattr(obj, "RetractMode"):
|
||||
obj.removeProperty("RetractMode")
|
||||
|
||||
# Migration: Remove RetractHeight property and adjust StartDepth if needed
|
||||
if hasattr(obj, "RetractHeight"):
|
||||
# If RetractHeight was higher than StartDepth, migrate to StartDepth
|
||||
if obj.RetractHeight.Value > obj.StartDepth.Value:
|
||||
Path.Log.warning(
|
||||
f"Migrating RetractHeight ({obj.RetractHeight.Value}) to StartDepth. "
|
||||
f"Old StartDepth was {obj.StartDepth.Value}"
|
||||
)
|
||||
obj.StartDepth = obj.RetractHeight.Value
|
||||
obj.removeProperty("RetractHeight")
|
||||
|
||||
if not hasattr(obj, "KeepToolDown"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
@@ -117,7 +133,7 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Apply G99 retraction: only retract to RetractHeight between holes in this operation",
|
||||
"Apply G99 retraction: only retract to StartDepth between holes in this operation",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -140,7 +156,7 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"chipBreakEnabled",
|
||||
"ChipBreakEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use chipbreaking"),
|
||||
)
|
||||
@@ -165,15 +181,6 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
"Calculate the tip length and subtract from final depth",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyDistance",
|
||||
"RetractHeight",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The height where cutting feed rate starts and retract height for peck operation",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyEnumeration",
|
||||
"ExtraOffset",
|
||||
@@ -186,23 +193,36 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Apply G99 retraction: only retract to RetractHeight between holes in this operation",
|
||||
"Apply G99 retraction: only retract to StartDepth between holes in this operation",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"feedRetractEnabled",
|
||||
"FeedRetractEnabled",
|
||||
"Drill",
|
||||
QT_TRANSLATE_NOOP("App::Property", "Use G85 boring cycle with feed out"),
|
||||
)
|
||||
|
||||
for n in self.propertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
|
||||
def circularHoleExecute(self, obj, holes):
|
||||
"""circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes."""
|
||||
Path.Log.track()
|
||||
machinestate = PathMachineState.MachineState()
|
||||
# We should be at clearance height.
|
||||
|
||||
mode = "G99" if obj.KeepToolDown else "G98"
|
||||
|
||||
# Validate that SafeHeight doesn't exceed ClearanceHeight
|
||||
safe_height = obj.SafeHeight.Value
|
||||
if safe_height > obj.ClearanceHeight.Value:
|
||||
Path.Log.warning(
|
||||
f"SafeHeight ({safe_height}) is above ClearanceHeight ({obj.ClearanceHeight.Value}). "
|
||||
f"Using ClearanceHeight instead."
|
||||
)
|
||||
safe_height = obj.ClearanceHeight.Value
|
||||
|
||||
# Calculate offsets to add to target edge
|
||||
endoffset = 0.0
|
||||
if obj.ExtraOffset == "Drill Tip":
|
||||
@@ -220,7 +240,7 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
# build list of solids for collision detection.
|
||||
# Include base objects from job
|
||||
solids = []
|
||||
for base in job.BaseObjects:
|
||||
for base in self.job.Model.Group:
|
||||
solids.append(base.Shape)
|
||||
|
||||
# http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g98-g99
|
||||
@@ -235,6 +255,8 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
|
||||
# Make sure tool is at a clearance height
|
||||
command = Path.Command("G0", {"Z": obj.ClearanceHeight.Value})
|
||||
machinestate.addCommand(command)
|
||||
|
||||
# machine.addCommand(command)
|
||||
self.commandlist.append(command)
|
||||
|
||||
@@ -251,31 +273,49 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
command = Path.Command("G0", {"X": startPoint.x, "Y": startPoint.y})
|
||||
self.commandlist.append(command)
|
||||
machinestate.addCommand(command)
|
||||
command = Path.Command("G0", {"Z": obj.SafeHeight.Value})
|
||||
command = Path.Command("G0", {"Z": safe_height})
|
||||
self.commandlist.append(command)
|
||||
machinestate.addCommand(command)
|
||||
firstMove = False
|
||||
|
||||
else: # Use get_linking_moves generator
|
||||
linking_moves = linking.get_linking_moves(
|
||||
machinestate.getPosition(),
|
||||
startPoint,
|
||||
obj.ClearanceHeight.Value,
|
||||
obj.SafeHeight.Value,
|
||||
self.tool,
|
||||
solids,
|
||||
obj.RetractHeight.Value,
|
||||
else: # Check if we need linking moves
|
||||
# For G99 mode, tool is at StartDepth (R-plane) after previous hole
|
||||
# Check if direct move at retract plane would collide with model
|
||||
current_pos = machinestate.getPosition()
|
||||
target_at_retract_plane = FreeCAD.Vector(startPoint.x, startPoint.y, current_pos.z)
|
||||
|
||||
# Check collision at the retract plane (current Z height)
|
||||
collision_detected = linking.check_collision(
|
||||
start_position=current_pos,
|
||||
target_position=target_at_retract_plane,
|
||||
solids=solids,
|
||||
)
|
||||
|
||||
if collision_detected:
|
||||
# Cannot traverse at retract plane - need to break cycle group
|
||||
# Retract to safe height, traverse, then plunge to safe height for new cycle
|
||||
target_at_safe_height = FreeCAD.Vector(startPoint.x, startPoint.y, safe_height)
|
||||
linking_moves = linking.get_linking_moves(
|
||||
start_position=current_pos,
|
||||
target_position=target_at_safe_height,
|
||||
local_clearance=safe_height,
|
||||
global_clearance=obj.ClearanceHeight.Value,
|
||||
tool_shape=self.tool.Shape,
|
||||
solids=solids,
|
||||
)
|
||||
if len(linking_moves) == 1: # straight move possible. Do nothing.
|
||||
pass
|
||||
else:
|
||||
self.commandlist.extend(linking_moves)
|
||||
for move in linking_moves:
|
||||
machinestate.addCommand(move)
|
||||
# else: no collision - G99 cycle continues, tool stays at retract plane
|
||||
|
||||
# Perform drilling
|
||||
dwelltime = obj.DwellTime if obj.DwellEnabled else 0.0
|
||||
peckdepth = obj.PeckDepth.Value if obj.PeckEnabled else 0.0
|
||||
repeat = 1 # technical debt: Add a repeat property for user control
|
||||
chipBreak = obj.chipBreakEnabled and obj.PeckEnabled
|
||||
chipBreak = obj.ChipBreakEnabled and obj.PeckEnabled
|
||||
|
||||
# Save Z position before canned cycle for G98 retract
|
||||
z_before_cycle = machinestate.Z
|
||||
|
||||
try:
|
||||
drillcommands = drill.generate(
|
||||
@@ -283,9 +323,9 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
dwelltime,
|
||||
peckdepth,
|
||||
repeat,
|
||||
obj.RetractHeight.Value,
|
||||
obj.StartDepth.Value,
|
||||
chipBreak=chipBreak,
|
||||
feedRetract=obj.feedRetractEnabled,
|
||||
feedRetract=obj.FeedRetractEnabled,
|
||||
)
|
||||
|
||||
except ValueError as e: # any targets that fail the generator are ignored
|
||||
@@ -298,24 +338,24 @@ class ObjectDrilling(PathCircularHoleBase.ObjectOp):
|
||||
annotations["RetractMode"] = mode
|
||||
command.Annotations = annotations
|
||||
self.commandlist.append(command)
|
||||
# machine.addCommand(command)
|
||||
machinestate.addCommand(command)
|
||||
|
||||
# Update Z position based on RetractMode
|
||||
# G98: retract to initial Z (Z before cycle started)
|
||||
# G99: retract to R parameter (StartDepth)
|
||||
if mode == "G98":
|
||||
machinestate.Z = z_before_cycle
|
||||
else: # G99
|
||||
machinestate.Z = obj.StartDepth.Value
|
||||
|
||||
# Apply feedrates to commands
|
||||
PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)
|
||||
|
||||
def opSetDefaultValues(self, obj, job):
|
||||
"""opSetDefaultValues(obj, job) ... set default value for RetractHeight"""
|
||||
"""opSetDefaultValues(obj, job) ... set default values for drilling operation"""
|
||||
obj.ExtraOffset = "None"
|
||||
obj.KeepToolDown = False # default to safest option: G98
|
||||
|
||||
if hasattr(job.SetupSheet, "RetractHeight"):
|
||||
obj.RetractHeight = job.SetupSheet.RetractHeight
|
||||
elif self.applyExpression(obj, "RetractHeight", "StartDepth+SetupSheet.SafeHeightOffset"):
|
||||
if not job:
|
||||
obj.RetractHeight = 10
|
||||
else:
|
||||
obj.RetractHeight.Value = obj.StartDepth.Value + 1.0
|
||||
|
||||
if hasattr(job.SetupSheet, "PeckDepth"):
|
||||
obj.PeckDepth = job.SetupSheet.PeckDepth
|
||||
elif self.applyExpression(obj, "PeckDepth", "OpToolDiameter*0.75"):
|
||||
@@ -335,7 +375,6 @@ def SetupProperties():
|
||||
setup.append("DwellEnabled")
|
||||
setup.append("AddTipLength")
|
||||
setup.append("ExtraOffset")
|
||||
setup.append("RetractHeight")
|
||||
setup.append("KeepToolDown")
|
||||
return setup
|
||||
|
||||
|
||||
@@ -50,9 +50,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
|
||||
def initPage(self, obj):
|
||||
self.peckDepthSpinBox = PathGuiUtil.QuantitySpinBox(self.form.peckDepth, obj, "PeckDepth")
|
||||
self.peckRetractSpinBox = PathGuiUtil.QuantitySpinBox(
|
||||
self.form.peckRetractHeight, obj, "RetractHeight"
|
||||
)
|
||||
self.dwellTimeSpinBox = PathGuiUtil.QuantitySpinBox(self.form.dwellTime, obj, "DwellTime")
|
||||
self.form.chipBreakEnabled.setEnabled(False)
|
||||
|
||||
@@ -75,9 +72,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
self.form.dwellEnabled.toggled.connect(self.form.feedRetractEnabled.setDisabled)
|
||||
self.form.dwellEnabled.toggled.connect(self.setChipBreakControl)
|
||||
|
||||
self.form.peckRetractHeight.setEnabled(True)
|
||||
self.form.retractLabel.setEnabled(True)
|
||||
|
||||
if self.form.peckEnabled.isChecked():
|
||||
self.form.dwellEnabled.setEnabled(False)
|
||||
self.form.feedRetractEnabled.setEnabled(False)
|
||||
@@ -109,14 +103,12 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
|
||||
def updateQuantitySpinBoxes(self, index=None):
|
||||
self.peckDepthSpinBox.updateWidget()
|
||||
self.peckRetractSpinBox.updateWidget()
|
||||
self.dwellTimeSpinBox.updateWidget()
|
||||
|
||||
def getFields(self, obj):
|
||||
"""setFields(obj) ... update obj's properties with values from the UI"""
|
||||
Path.Log.track()
|
||||
self.peckDepthSpinBox.updateProperty()
|
||||
self.peckRetractSpinBox.updateProperty()
|
||||
self.dwellTimeSpinBox.updateProperty()
|
||||
|
||||
if obj.KeepToolDown != self.form.KeepToolDownEnabled.isChecked():
|
||||
@@ -125,10 +117,10 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
obj.DwellEnabled = self.form.dwellEnabled.isChecked()
|
||||
if obj.PeckEnabled != self.form.peckEnabled.isChecked():
|
||||
obj.PeckEnabled = self.form.peckEnabled.isChecked()
|
||||
if obj.feedRetractEnabled != self.form.feedRetractEnabled.isChecked():
|
||||
obj.feedRetractEnabled = self.form.feedRetractEnabled.isChecked()
|
||||
if obj.chipBreakEnabled != self.form.chipBreakEnabled.isChecked():
|
||||
obj.chipBreakEnabled = self.form.chipBreakEnabled.isChecked()
|
||||
if obj.FeedRetractEnabled != self.form.feedRetractEnabled.isChecked():
|
||||
obj.FeedRetractEnabled = self.form.feedRetractEnabled.isChecked()
|
||||
if obj.ChipBreakEnabled != self.form.chipBreakEnabled.isChecked():
|
||||
obj.ChipBreakEnabled = self.form.chipBreakEnabled.isChecked()
|
||||
if obj.ExtraOffset != str(self.form.ExtraOffset.currentData()):
|
||||
obj.ExtraOffset = str(self.form.ExtraOffset.currentData())
|
||||
|
||||
@@ -147,7 +139,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
"Drill",
|
||||
QtCore.QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Apply G99 retraction: only retract to RetractHeight between holes in this operation",
|
||||
"Apply G99 retraction: only retract to StartDepth between holes in this operation",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -167,12 +159,12 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
self.form.peckEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
self.form.chipBreakEnabled.setEnabled(False)
|
||||
|
||||
if obj.chipBreakEnabled:
|
||||
if obj.ChipBreakEnabled:
|
||||
self.form.chipBreakEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.chipBreakEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
if obj.feedRetractEnabled:
|
||||
if obj.FeedRetractEnabled:
|
||||
self.form.feedRetractEnabled.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.form.feedRetractEnabled.setCheckState(QtCore.Qt.Unchecked)
|
||||
@@ -186,7 +178,6 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
"""getSignalsForUpdate(obj) ... return list of signals which cause the receiver to update the model"""
|
||||
signals = []
|
||||
|
||||
signals.append(self.form.peckRetractHeight.editingFinished)
|
||||
signals.append(self.form.peckDepth.editingFinished)
|
||||
signals.append(self.form.dwellTime.editingFinished)
|
||||
if hasattr(self.form.dwellEnabled, "checkStateChanged"): # Qt version >= 6.7.0
|
||||
@@ -209,7 +200,7 @@ class TaskPanelOpPage(PathCircularHoleBaseGui.TaskPanelOpPage):
|
||||
return signals
|
||||
|
||||
def updateData(self, obj, prop):
|
||||
if prop in ["PeckDepth", "RetractHeight"] and not prop in ["Base", "Disabled"]:
|
||||
if prop in ["PeckDepth"] and not prop in ["Base", "Disabled"]:
|
||||
self.updateQuantitySpinBoxes()
|
||||
|
||||
|
||||
|
||||
@@ -30,8 +30,12 @@ import Path
|
||||
import Path.Base.Drillable as Drillable
|
||||
import math
|
||||
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
|
||||
if False:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
class PathBaseGate(object):
|
||||
|
||||
255
src/Mod/CAM/Path/Post/PostList.py
Normal file
255
src/Mod/CAM/Path/Post/PostList.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
import re
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
import Path
|
||||
import Path.Base.Util as PathUtil
|
||||
import Path.Tool.Controller as PathToolController
|
||||
|
||||
|
||||
class _FixtureSetupObject:
|
||||
Path = None
|
||||
Name = "Fixture"
|
||||
InList = []
|
||||
Label = "Fixture"
|
||||
|
||||
|
||||
class _CommandObject:
|
||||
def __init__(self, command):
|
||||
self.Path = Path.Path([command])
|
||||
self.Name = "Command"
|
||||
self.InList = []
|
||||
self.Label = "Command"
|
||||
|
||||
|
||||
def needsTcOp(oldTc: Any, newTc: Any) -> bool:
|
||||
return (
|
||||
oldTc is None
|
||||
or oldTc.ToolNumber != newTc.ToolNumber
|
||||
or oldTc.SpindleSpeed != newTc.SpindleSpeed
|
||||
or oldTc.SpindleDir != newTc.SpindleDir
|
||||
)
|
||||
|
||||
|
||||
def create_fixture_setup(processor: Any, order: int, fixture: str) -> _FixtureSetupObject:
|
||||
fobj = _FixtureSetupObject()
|
||||
c1 = Path.Command(fixture)
|
||||
fobj.Path = Path.Path([c1])
|
||||
|
||||
if order != 0:
|
||||
clearance_z = (
|
||||
processor._job.Stock.Shape.BoundBox.ZMax
|
||||
+ processor._job.SetupSheet.ClearanceHeightOffset.Value
|
||||
)
|
||||
c2 = Path.Command(f"G0 Z{clearance_z}")
|
||||
fobj.Path.addCommands(c2)
|
||||
|
||||
fobj.InList.append(processor._job)
|
||||
return fobj
|
||||
|
||||
|
||||
def build_postlist_by_fixture(processor: Any, early_tool_prep: bool = False) -> list:
|
||||
Path.Log.debug("Ordering by Fixture")
|
||||
postlist = []
|
||||
wcslist = processor._job.Fixtures
|
||||
currTc = None
|
||||
|
||||
for index, f in enumerate(wcslist):
|
||||
sublist = [create_fixture_setup(processor, index, f)]
|
||||
|
||||
for obj in processor._operations:
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None and PathUtil.activeForOp(obj):
|
||||
if needsTcOp(currTc, tc):
|
||||
sublist.append(tc)
|
||||
Path.Log.debug(f"Appending TC: {tc.Name}")
|
||||
currTc = tc
|
||||
sublist.append(obj)
|
||||
|
||||
postlist.append((f, sublist))
|
||||
|
||||
return postlist
|
||||
|
||||
|
||||
def build_postlist_by_tool(processor: Any, early_tool_prep: bool = False) -> list:
|
||||
Path.Log.debug("Ordering by Tool")
|
||||
postlist = []
|
||||
wcslist = processor._job.Fixtures
|
||||
toolstring = "None"
|
||||
currTc = None
|
||||
|
||||
fixturelist = []
|
||||
for index, f in enumerate(wcslist):
|
||||
fixturelist.append(create_fixture_setup(processor, index, f))
|
||||
|
||||
curlist = []
|
||||
sublist = []
|
||||
|
||||
def commitToPostlist():
|
||||
if len(curlist) > 0:
|
||||
for fixture in fixturelist:
|
||||
sublist.append(fixture)
|
||||
sublist.extend(curlist)
|
||||
postlist.append((toolstring, sublist))
|
||||
|
||||
Path.Log.track(processor._job.PostProcessorOutputFile)
|
||||
for _, obj in enumerate(processor._operations):
|
||||
Path.Log.track(obj.Label)
|
||||
|
||||
if not PathUtil.activeForOp(obj):
|
||||
Path.Log.track()
|
||||
continue
|
||||
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
|
||||
if tc is None or not needsTcOp(currTc, tc):
|
||||
curlist.append(obj)
|
||||
else:
|
||||
commitToPostlist()
|
||||
|
||||
sublist = [tc]
|
||||
curlist = [obj]
|
||||
currTc = tc
|
||||
|
||||
if "%T" in processor._job.PostProcessorOutputFile:
|
||||
toolstring = f"{tc.ToolNumber}"
|
||||
else:
|
||||
toolstring = re.sub(r"[^\w\d-]", "_", tc.Label)
|
||||
|
||||
commitToPostlist()
|
||||
|
||||
return postlist
|
||||
|
||||
|
||||
def build_postlist_by_operation(processor: Any, early_tool_prep: bool = False) -> list:
|
||||
Path.Log.debug("Ordering by Operation")
|
||||
postlist = []
|
||||
wcslist = processor._job.Fixtures
|
||||
currTc = None
|
||||
|
||||
for obj in processor._operations:
|
||||
if not PathUtil.activeForOp(obj):
|
||||
continue
|
||||
|
||||
sublist = []
|
||||
Path.Log.debug(f"obj: {obj.Name}")
|
||||
|
||||
for index, f in enumerate(wcslist):
|
||||
sublist.append(create_fixture_setup(processor, index, f))
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None:
|
||||
if processor._job.SplitOutput or needsTcOp(currTc, tc):
|
||||
sublist.append(tc)
|
||||
currTc = tc
|
||||
sublist.append(obj)
|
||||
|
||||
postlist.append((obj.Label, sublist))
|
||||
|
||||
return postlist
|
||||
|
||||
|
||||
def buildPostList(processor: Any, early_tool_prep: bool = False) -> List[Tuple[str, List]]:
|
||||
orderby = processor._job.OrderOutputBy
|
||||
Path.Log.debug(f"Ordering by {orderby}")
|
||||
|
||||
if orderby == "Fixture":
|
||||
postlist = build_postlist_by_fixture(processor, early_tool_prep)
|
||||
elif orderby == "Tool":
|
||||
postlist = build_postlist_by_tool(processor, early_tool_prep)
|
||||
elif orderby == "Operation":
|
||||
postlist = build_postlist_by_operation(processor, early_tool_prep)
|
||||
else:
|
||||
raise ValueError(f"Unknown order: {orderby}")
|
||||
|
||||
Path.Log.debug(f"Postlist: {postlist}")
|
||||
|
||||
if processor._job.SplitOutput:
|
||||
final_postlist = postlist
|
||||
else:
|
||||
final_postlist = [("allitems", [item for sublist in postlist for item in sublist[1]])]
|
||||
|
||||
if early_tool_prep:
|
||||
return apply_early_tool_prep(final_postlist)
|
||||
return final_postlist
|
||||
|
||||
|
||||
def apply_early_tool_prep(postlist: List[Tuple[str, List]]) -> List[Tuple[str, List]]:
|
||||
"""
|
||||
Apply early tool preparation optimization to the postlist.
|
||||
|
||||
This function modifies tool change commands to enable early tool preparation:
|
||||
- Always outputs tool changes as "Tn M6" (tool number followed by change command)
|
||||
- Additionally emits standalone "Tn" prep commands immediately after the previous M6
|
||||
to allow the machine to prepare the next tool while the current tool is working
|
||||
|
||||
Example output:
|
||||
T4 M6 <- change to tool 4
|
||||
T5 <- prep tool 5 early (while T4 is working)
|
||||
<gcode> <- operations with T4
|
||||
T5 M6 <- change to tool 5 (already prepped)
|
||||
T7 <- prep tool 7 early (while T5 is working)
|
||||
<gcode> <- operations with T5
|
||||
T7 M6 <- change to tool 7 (already prepped)
|
||||
"""
|
||||
# First, collect all tool controllers across all groups to find next tool
|
||||
all_tool_controllers = []
|
||||
for group_idx, (name, sublist) in enumerate(postlist):
|
||||
for item_idx, item in enumerate(sublist):
|
||||
if hasattr(item, "Proxy") and isinstance(item.Proxy, PathToolController.ToolController):
|
||||
all_tool_controllers.append((group_idx, item_idx, item))
|
||||
|
||||
new_postlist = []
|
||||
for group_idx, (name, sublist) in enumerate(postlist):
|
||||
new_sublist = []
|
||||
i = 0
|
||||
while i < len(sublist):
|
||||
item = sublist[i]
|
||||
# Check if item is a tool controller
|
||||
if hasattr(item, "Proxy") and isinstance(item.Proxy, PathToolController.ToolController):
|
||||
# Tool controller has Path.Commands like: [Command (comment), Command M6 [T:n]]
|
||||
# Find the M6 command
|
||||
m6_cmd = None
|
||||
for cmd in item.Path.Commands:
|
||||
if cmd.Name == "M6":
|
||||
m6_cmd = cmd
|
||||
break
|
||||
|
||||
if m6_cmd and len(m6_cmd.Parameters) > 0:
|
||||
# M6 command has parameters like {'T': 5}, access via key
|
||||
tool_number = m6_cmd.Parameters.get("T", item.ToolNumber)
|
||||
|
||||
# Find this TC in the global list and check if there's a next one
|
||||
tc_position = next(
|
||||
(
|
||||
idx
|
||||
for idx, (g_idx, i_idx, tc) in enumerate(all_tool_controllers)
|
||||
if g_idx == group_idx and i_idx == i
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
next_tc = (
|
||||
all_tool_controllers[tc_position + 1][2]
|
||||
if tc_position is not None and tc_position + 1 < len(all_tool_controllers)
|
||||
else None
|
||||
)
|
||||
|
||||
# Keep the original M6 command with tool parameter (M6 T5 format)
|
||||
# This is the valid FreeCAD format, postprocessor will reformat if needed
|
||||
new_sublist.append(item)
|
||||
|
||||
# If there's a next tool controller, add Tn prep for it immediately after M6
|
||||
if next_tc is not None:
|
||||
next_tool_number = next_tc.ToolNumber
|
||||
prep_next = Path.Command(f"T{next_tool_number}")
|
||||
prep_next_object = _CommandObject(prep_next)
|
||||
new_sublist.append(prep_next_object)
|
||||
else:
|
||||
# No M6 command found or no tool parameter, keep as-is
|
||||
new_sublist.append(item)
|
||||
else:
|
||||
new_sublist.append(item)
|
||||
i += 1
|
||||
new_postlist.append((name, new_sublist))
|
||||
return new_postlist
|
||||
@@ -39,6 +39,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import Path.Base.Util as PathUtil
|
||||
import Path.Post.UtilsArguments as PostUtilsArguments
|
||||
import Path.Post.UtilsExport as PostUtilsExport
|
||||
import Path.Post.PostList as PostList
|
||||
import Path.Post.Utils as PostUtils
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
@@ -55,13 +57,6 @@ else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
class _TempObject:
|
||||
Path = None
|
||||
Name = "Fixture"
|
||||
InList = []
|
||||
Label = "Fixture"
|
||||
|
||||
|
||||
#
|
||||
# Define some types that are used throughout this file.
|
||||
#
|
||||
@@ -93,6 +88,9 @@ class PostProcessorFactory:
|
||||
|
||||
module_name = f"{postname}_post"
|
||||
class_name = postname.title()
|
||||
Path.Log.debug(f"PostProcessorFactory.get_post_processor() - postname: {postname}")
|
||||
Path.Log.debug(f"PostProcessorFactory.get_post_processor() - module_name: {module_name}")
|
||||
Path.Log.debug(f"PostProcessorFactory.get_post_processor() - class_name: {class_name}")
|
||||
|
||||
# Iterate all the paths to find the module
|
||||
for path in paths:
|
||||
@@ -120,12 +118,7 @@ class PostProcessorFactory:
|
||||
|
||||
|
||||
def needsTcOp(oldTc, newTc):
|
||||
return (
|
||||
oldTc is None
|
||||
or oldTc.ToolNumber != newTc.ToolNumber
|
||||
or oldTc.SpindleSpeed != newTc.SpindleSpeed
|
||||
or oldTc.SpindleDir != newTc.SpindleDir
|
||||
)
|
||||
return PostList.needsTcOp(oldTc, newTc)
|
||||
|
||||
|
||||
class PostProcessor:
|
||||
@@ -167,164 +160,21 @@ class PostProcessor:
|
||||
"""Get the units used by the post processor."""
|
||||
return self._units
|
||||
|
||||
def _buildPostList(self):
|
||||
def _buildPostList(self, early_tool_prep=False):
|
||||
"""Determine the specific objects and order to postprocess.
|
||||
|
||||
Returns a list of objects which can be passed to exportObjectsWith()
|
||||
for final posting. The ordering strategy is determined by the job's
|
||||
OrderOutputBy setting.
|
||||
|
||||
Args:
|
||||
early_tool_prep: If True, split tool changes into separate prep (Tn)
|
||||
and change (M6) commands for better machine efficiency
|
||||
|
||||
Returns:
|
||||
List of (name, operations) tuples
|
||||
"""
|
||||
determines the specific objects and order to postprocess
|
||||
Returns a list of objects which can be passed to
|
||||
exportObjectsWith() for final posting."""
|
||||
|
||||
def __fixtureSetup(order, fixture, job):
|
||||
"""Convert a Fixture setting to _TempObject instance with a G0 move to a
|
||||
safe height every time the fixture coordinate system change. Skip
|
||||
the move for first fixture, to avoid moving before tool and tool
|
||||
height compensation is enabled."""
|
||||
|
||||
fobj = _TempObject()
|
||||
c1 = Path.Command(fixture)
|
||||
fobj.Path = Path.Path([c1])
|
||||
# Avoid any tool move after G49 in preamble and before tool change
|
||||
# and G43 in case tool height compensation is in use, to avoid
|
||||
# dangerous move without tool compensation.
|
||||
if order != 0:
|
||||
c2 = Path.Command(
|
||||
"G0 Z"
|
||||
+ str(
|
||||
job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value
|
||||
)
|
||||
)
|
||||
fobj.Path.addCommands(c2)
|
||||
fobj.InList.append(job)
|
||||
return fobj
|
||||
|
||||
wcslist = self._job.Fixtures
|
||||
orderby = self._job.OrderOutputBy
|
||||
Path.Log.debug(f"Ordering by {orderby}")
|
||||
|
||||
postlist = []
|
||||
|
||||
if orderby == "Fixture":
|
||||
Path.Log.debug("Ordering by Fixture")
|
||||
# Order by fixture means all operations and tool changes will be
|
||||
# completed in one fixture before moving to the next.
|
||||
|
||||
currTc = None
|
||||
for index, f in enumerate(wcslist):
|
||||
# create an object to serve as the fixture path
|
||||
sublist = [__fixtureSetup(index, f, self._job)]
|
||||
|
||||
# Now generate the gcode
|
||||
for obj in self._operations:
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None and PathUtil.activeForOp(obj):
|
||||
if needsTcOp(currTc, tc):
|
||||
sublist.append(tc)
|
||||
Path.Log.debug(f"Appending TC: {tc.Name}")
|
||||
currTc = tc
|
||||
sublist.append(obj)
|
||||
postlist.append((f, sublist))
|
||||
|
||||
elif orderby == "Tool":
|
||||
Path.Log.debug("Ordering by Tool")
|
||||
# Order by tool means tool changes are minimized.
|
||||
# all operations with the current tool are processed in the current
|
||||
# fixture before moving to the next fixture.
|
||||
|
||||
toolstring = "None"
|
||||
currTc = None
|
||||
|
||||
# Build the fixture list
|
||||
fixturelist = []
|
||||
for index, f in enumerate(wcslist):
|
||||
# create an object to serve as the fixture path
|
||||
fixturelist.append(__fixtureSetup(index, f, self._job))
|
||||
|
||||
# Now generate the gcode
|
||||
curlist = [] # list of ops for tool, will repeat for each fixture
|
||||
sublist = [] # list of ops for output splitting
|
||||
|
||||
def commitToPostlist():
|
||||
if len(curlist) > 0:
|
||||
for fixture in fixturelist:
|
||||
sublist.append(fixture)
|
||||
sublist.extend(curlist)
|
||||
postlist.append((toolstring, sublist))
|
||||
|
||||
Path.Log.track(self._job.PostProcessorOutputFile)
|
||||
for idx, obj in enumerate(self._operations):
|
||||
Path.Log.track(obj.Label)
|
||||
|
||||
# check if the operation is active
|
||||
if not PathUtil.activeForOp(obj):
|
||||
Path.Log.track()
|
||||
continue
|
||||
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
|
||||
# The operation has no ToolController or uses the same
|
||||
# ToolController as the previous operations
|
||||
|
||||
if tc is None or not needsTcOp(currTc, tc):
|
||||
# Queue current operation
|
||||
curlist.append(obj)
|
||||
|
||||
# The operation is the first operation or uses a different
|
||||
# ToolController as the previous operations
|
||||
|
||||
else:
|
||||
# Commit previous operations
|
||||
commitToPostlist()
|
||||
|
||||
# Queue current ToolController and operation
|
||||
sublist = [tc]
|
||||
curlist = [obj]
|
||||
currTc = tc
|
||||
|
||||
# Determine the proper string for the operation's
|
||||
# ToolController
|
||||
if "%T" in self._job.PostProcessorOutputFile:
|
||||
toolstring = f"{tc.ToolNumber}"
|
||||
else:
|
||||
toolstring = re.sub(r"[^\w\d-]", "_", tc.Label)
|
||||
|
||||
# Commit remaining operations
|
||||
commitToPostlist()
|
||||
|
||||
elif orderby == "Operation":
|
||||
Path.Log.debug("Ordering by Operation")
|
||||
# Order by operation means ops are done in each fixture in
|
||||
# sequence.
|
||||
currTc = None
|
||||
|
||||
# Now generate the gcode
|
||||
for obj in self._operations:
|
||||
|
||||
# check if the operation is active
|
||||
if not PathUtil.activeForOp(obj):
|
||||
continue
|
||||
|
||||
sublist = []
|
||||
Path.Log.debug(f"obj: {obj.Name}")
|
||||
|
||||
for index, f in enumerate(wcslist):
|
||||
sublist.append(__fixtureSetup(index, f, self._job))
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None:
|
||||
if self._job.SplitOutput or needsTcOp(currTc, tc):
|
||||
sublist.append(tc)
|
||||
currTc = tc
|
||||
sublist.append(obj)
|
||||
postlist.append((obj.Label, sublist))
|
||||
|
||||
Path.Log.debug(f"Postlist: {postlist}")
|
||||
|
||||
if self._job.SplitOutput:
|
||||
Path.Log.track()
|
||||
return postlist
|
||||
|
||||
Path.Log.track()
|
||||
finalpostlist = [("allitems", [item for slist in postlist for item in slist[1]])]
|
||||
Path.Log.debug(f"Postlist: {postlist}")
|
||||
return finalpostlist
|
||||
return PostList.buildPostList(self, early_tool_prep)
|
||||
|
||||
def export(self) -> Union[None, GCodeSections]:
|
||||
"""Process the parser arguments, then postprocess the 'postables'."""
|
||||
@@ -446,6 +296,13 @@ class PostProcessor:
|
||||
|
||||
postables = self._buildPostList()
|
||||
|
||||
# Process canned cycles for drilling operations
|
||||
for _, section in enumerate(postables):
|
||||
_, sublist = section
|
||||
for obj in sublist:
|
||||
if hasattr(obj, "Path"):
|
||||
obj.Path = PostUtils.cannedCycleTerminator(obj.Path)
|
||||
|
||||
Path.Log.debug(f"postables count: {len(postables)}")
|
||||
|
||||
g_code_sections = []
|
||||
|
||||
@@ -31,6 +31,7 @@ These are common functions and classes for creating custom post processors.
|
||||
|
||||
from Path.Base.MachineState import MachineState
|
||||
from Path.Main.Gui.Editor import CodeEditor
|
||||
from Path.Geom import CmdMoveDrill
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
@@ -412,3 +413,83 @@ def splitArcs(path, deflection=None):
|
||||
machine.addCommand(command)
|
||||
|
||||
return Path.Path(results)
|
||||
|
||||
|
||||
def cannedCycleTerminator(path):
|
||||
"""iterate through a Path object and insert G80 commands to terminate canned cycles at the correct time"""
|
||||
|
||||
# Canned cycles terminate if any parameter change other than XY coordinates.
|
||||
# - if Z depth changes
|
||||
# - if feed rate changes
|
||||
# - if retract plane changes
|
||||
# - if retract mode (G98/G99) changes
|
||||
|
||||
result = []
|
||||
cycle_active = False
|
||||
last_cycle_params = {}
|
||||
last_retract_mode = None
|
||||
explicit_retract_mode_set = False
|
||||
|
||||
for command in path.Commands:
|
||||
if (
|
||||
command.Name == "G80"
|
||||
): # This shouldn't happen because cycle generators shouldn't be inserting it. Be safe anyway.
|
||||
# G80 is already a cycle terminator, don't terminate before it
|
||||
# Just mark cycle as inactive and pass it through
|
||||
cycle_active = False
|
||||
last_retract_mode = None
|
||||
explicit_retract_mode_set = False
|
||||
result.append(command)
|
||||
elif command.Name in ["G98", "G99"]:
|
||||
# Explicit retract mode in the path - track it
|
||||
if cycle_active and last_retract_mode and command.Name != last_retract_mode:
|
||||
# Mode changed while cycle active - terminate
|
||||
result.append(Path.Command("G80"))
|
||||
cycle_active = False
|
||||
last_retract_mode = command.Name
|
||||
explicit_retract_mode_set = True
|
||||
result.append(command)
|
||||
elif command.Name in CmdMoveDrill:
|
||||
# Check if this cycle has different parameters than the last one
|
||||
current_params = {k: v for k, v in command.Parameters.items() if k not in ["X", "Y"]}
|
||||
|
||||
# Get retract mode from annotations
|
||||
current_retract_mode = command.Annotations.get("RetractMode", "G98")
|
||||
|
||||
# Check if we need to terminate the previous cycle
|
||||
if cycle_active and (
|
||||
current_params != last_cycle_params or current_retract_mode != last_retract_mode
|
||||
):
|
||||
# Parameters or retract mode changed, terminate previous cycle
|
||||
result.append(Path.Command("G80"))
|
||||
cycle_active = False
|
||||
explicit_retract_mode_set = False
|
||||
|
||||
# Insert retract mode command if starting a new cycle or mode changed
|
||||
# But only if it wasn't already explicitly set in the path
|
||||
if (
|
||||
not cycle_active or current_retract_mode != last_retract_mode
|
||||
) and not explicit_retract_mode_set:
|
||||
result.append(Path.Command(current_retract_mode))
|
||||
|
||||
# Add the cycle command
|
||||
result.append(command)
|
||||
cycle_active = True
|
||||
last_cycle_params = current_params
|
||||
last_retract_mode = current_retract_mode
|
||||
explicit_retract_mode_set = False # Reset for next cycle
|
||||
else:
|
||||
# Non-cycle command (not G80 or drill cycle)
|
||||
if cycle_active:
|
||||
# Terminate active cycle
|
||||
result.append(Path.Command("G80"))
|
||||
cycle_active = False
|
||||
last_retract_mode = None
|
||||
explicit_retract_mode_set = False
|
||||
result.append(command)
|
||||
|
||||
# If cycle is still active at the end, terminate it
|
||||
if cycle_active:
|
||||
result.append(Path.Command("G80"))
|
||||
|
||||
return Path.Path(result)
|
||||
|
||||
Reference in New Issue
Block a user