CAM: Simplify annotation handling in GCode and improve annotation parsing

src/Mod/CAM/App/Command.cpp:
- Removed requirement for annotations= prefix; now all text after ; is treated as annotation data.
- Updated Command::toGCode to output annotations as key-value pairs in comments.
- Improved setFromGCode to extract annotations from any comment after ;.
- Enhanced annotation parsing to handle quoted strings and floating-point numbers.
- Simplified XML serialization and restoration logic for annotations.

src/Mod/CAM/App/Path.cpp:
- Added addCommandNoRecalc, allowing bulk loading of commands without repeated recalculation.
- Refactored RestoreDocFile to read GCode files line-by-line, parse each command, and call recalculate() only once after all commands are loaded.
- Added explanatory comment above the old implementation.

src/Mod/CAM/App/Path.h:
- Declared addCommandNoRecalc in the Toolpath class.

src/Mod/CAM/CAMTests/TestPathCommandAnnotations.py:
- Adjusted unit test for scientific notation annotation to check only 6 decimal places.
- Adjusted unit test 10 to properly handle assertions with the toGCode method

src/Mod/CAM/PathSimulator/AppGL/GCodeParser.cpp:
- GCodeParser::ParseLine: Truncate at first semicolon (annotations / comment)
This commit is contained in:
Billy Huddleston
2025-10-05 12:13:30 -04:00
parent a970235484
commit bcefebca5f
6 changed files with 160 additions and 87 deletions

View File

@@ -25,7 +25,6 @@
#include <iomanip>
#include <boost/algorithm/string.hpp>
#include <Base/Exception.h>
#include <Base/Reader.h>
#include <Base/Rotation.h>
@@ -139,20 +138,54 @@ std::string Command::toGCode(int precision, bool padzero) const
}
str << '.' << std::setw(width) << std::right << digits;
}
// Add annotations as a comment if they exist
if (!Annotations.empty()) {
str << " ; ";
bool first = true;
for (const auto& pair : Annotations) {
if (!first) {
str << " ";
}
first = false;
str << pair.first << ":";
if (std::holds_alternative<std::string>(pair.second)) {
str << "'" << std::get<std::string>(pair.second) << "'";
}
else if (std::holds_alternative<double>(pair.second)) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(6) << std::get<double>(pair.second);
str << oss.str();
}
}
}
return str.str();
}
void Command::setFromGCode(const std::string& str)
{
Parameters.clear();
Annotations.clear();
// Check for annotation comment and split the string
std::string gcode_part = str;
std::string annotation_part;
auto comment_pos = str.find("; ");
if (comment_pos != std::string::npos) {
gcode_part = str.substr(0, comment_pos);
annotation_part = str.substr(comment_pos + 1); // length of "; "
}
std::string mode = "none";
std::string key;
std::string value;
for (unsigned int i = 0; i < str.size(); i++) {
if ((isdigit(str[i])) || (str[i] == '-') || (str[i] == '.')) {
value += str[i];
for (unsigned int i = 0; i < gcode_part.size(); i++) {
if ((isdigit(gcode_part[i])) || (gcode_part[i] == '-') || (gcode_part[i] == '.')) {
value += gcode_part[i];
}
else if (isalpha(str[i])) {
else if (isalpha(gcode_part[i])) {
if (mode == "command") {
if (!key.empty() && !value.empty()) {
std::string cmd = key + value;
@@ -183,24 +216,30 @@ void Command::setFromGCode(const std::string& str)
}
}
else if (mode == "comment") {
value += str[i];
value += gcode_part[i];
}
key = str[i];
key = gcode_part[i];
}
else if (str[i] == '(') {
else if (gcode_part[i] == '(') {
mode = "comment";
}
else if (str[i] == ')') {
else if (gcode_part[i] == ')') {
key = "(";
value += ")";
}
else {
// add non-ascii characters only if this is a comment
if (mode == "comment") {
value += str[i];
value += gcode_part[i];
}
}
}
// Parse annotations if found
if (!annotation_part.empty()) {
setAnnotations(annotation_part);
}
if (!key.empty() && !value.empty()) {
if ((mode == "command") || (mode == "comment")) {
std::string cmd = key + value;
@@ -392,117 +431,109 @@ bool Command::hasAnnotation(const std::string& key) const
Command& Command::setAnnotations(const std::string& annotationString)
{
// Simple parser: split by space, then by colon
std::stringstream ss(annotationString);
std::istringstream iss(annotationString);
std::string token;
while (ss >> token) {
while (iss >> token) {
auto pos = token.find(':');
if (pos != std::string::npos) {
std::string key = token.substr(0, pos);
std::string value = token.substr(pos + 1);
// Try to parse as double, if successful store as double, otherwise as string
try {
size_t processed = 0;
double numValue = std::stod(value, &processed);
if (processed == value.length()) {
// Entire string was successfully parsed as a number
Annotations[key] = numValue;
// If value starts and ends with single quote, treat as string
if (value.size() >= 2 && value.front() == '\'' && value.back() == '\'') {
Annotations[key] = value.substr(1, value.size() - 2);
}
else {
// Try to parse as double, if successful store as double, otherwise as string
try {
size_t processed = 0;
double numValue = std::stod(value, &processed);
if (processed == value.length()) {
Annotations[key] = numValue;
}
else {
Annotations[key] = value;
}
}
else {
// Partial parse, treat as string
catch (const std::exception&) {
Annotations[key] = value;
}
}
catch (const std::exception&) {
// Not a valid number, store as string
Annotations[key] = value;
}
}
}
return *this;
}
// Reimplemented from base class
unsigned int Command::getMemSize() const
{
return toGCode().size();
}
void Command::Save(Writer& writer) const
void Command::Save(Base::Writer& writer) const
{
// Save command with GCode and annotations
writer.Stream() << writer.ind() << "<Command "
<< "gcode=\"" << toGCode() << "\"";
// this will only get used if saved as XML (probably never)
writer.Stream() << writer.ind() << "<Command gcode=\"" << toGCode() << "\"";
// Add annotation count for faster restoration
if (!Annotations.empty()) {
writer.Stream() << " annotationCount=\"" << Annotations.size() << "\"";
}
if (Annotations.empty()) {
writer.Stream() << " />" << std::endl;
}
else {
writer.Stream() << ">" << std::endl;
writer.incInd();
// Save each annotation with type information
for (const auto& annotation : Annotations) {
writer.Stream() << writer.ind() << "<Annotation key=\"" << annotation.first << "\"";
if (std::holds_alternative<std::string>(annotation.second)) {
writer.Stream() << " type=\"string\" value=\""
<< std::get<std::string>(annotation.second) << "\" />" << std::endl;
writer.Stream() << " annotations=\"";
bool first = true;
for (const auto& pair : Annotations) {
if (!first) {
writer.Stream() << " ";
}
else if (std::holds_alternative<double>(annotation.second)) {
writer.Stream() << " type=\"double\" value=\""
<< std::get<double>(annotation.second) << "\" />" << std::endl;
first = false;
writer.Stream() << pair.first << ":";
if (std::holds_alternative<std::string>(pair.second)) {
writer.Stream() << "'" << std::get<std::string>(pair.second) << "'";
}
else if (std::holds_alternative<double>(pair.second)) {
writer.Stream() << std::fixed << std::setprecision(6)
<< std::get<double>(pair.second);
}
}
writer.decInd();
writer.Stream() << writer.ind() << "</Command>" << std::endl;
writer.Stream() << "\"";
}
writer.Stream() << " />" << std::endl;
}
void Command::Restore(XMLReader& reader)
void Command::Restore(Base::XMLReader& reader)
{
reader.readElement("Command");
std::string gcode = reader.getAttribute<const char*>("gcode");
setFromGCode(gcode);
// Clear any existing annotations
Annotations.clear();
// Check if there are annotations to restore
int annotationCount = reader.getAttribute<int>("annotationCount", 0);
std::string attr;
try {
attr = reader.getAttribute<const char*>("annotations");
}
catch (...) {
return; // No annotations
}
if (annotationCount > 0) {
// Read annotation elements
for (int i = 0; i < annotationCount; i++) {
reader.readElement("Annotation");
std::string key = reader.getAttribute<const char*>("key");
std::string type = reader.getAttribute<const char*>("type");
std::string value = reader.getAttribute<const char*>("value");
std::istringstream iss(attr);
std::string token;
while (iss >> token) {
auto pos = token.find(':');
if (pos != std::string::npos) {
std::string key = token.substr(0, pos);
std::string value = token.substr(pos + 1);
if (type == "string") {
Annotations[key] = value;
// If value starts and ends with single quote, treat as string
if (value.size() >= 2 && value.front() == '\'' && value.back() == '\'') {
Annotations[key] = value.substr(1, value.size() - 2);
}
else if (type == "double") {
else {
try {
double dvalue = std::stod(value);
Annotations[key] = dvalue;
double d = std::stod(value);
Annotations[key] = d;
}
catch (const std::exception&) {
// If conversion fails, store as string
catch (...) {
Annotations[key] = value;
}
}
}
// Read closing tag
reader.readEndElement("Command");
}
}

View File

@@ -23,12 +23,15 @@ class Command(Persistence):
def toGCode(self) -> str:
"""toGCode(): returns a GCode representation of the command"""
...
def setFromGCode(self, gcode: str) -> None:
"""setFromGCode(): sets the path from the contents of the given GCode string"""
...
def transform(self, placement: Placement) -> "CommandPy":
"""transform(Placement): returns a copy of this command transformed by the given placement"""
...
def addAnnotations(self, annotations) -> "Command":
"""addAnnotations(annotations): adds annotations from dictionary or string and returns self for chaining"""
...

View File

@@ -515,13 +515,36 @@ void Toolpath::Restore(XMLReader& reader)
}
}
// The previous implementation read the file word-by-word, merging all content into a single string.
// This caused GCode commands and annotations to be split and parsed incorrectly.
// The new implementation reads the file line-by-line, ensuring each command and its annotations are
// handled correctly. void Toolpath::RestoreDocFile(Base::Reader& reader)
// {
// std::string gcode;
// std::string line;
// while (reader >> line) {
// gcode += line;
// gcode += " ";
// }
// setFromGCode(gcode);
// }
void Toolpath::addCommandNoRecalc(const Command& Cmd)
{
Command* tmp = new Command(Cmd);
vpcCommands.push_back(tmp);
// No recalculate here
}
void Toolpath::RestoreDocFile(Base::Reader& reader)
{
std::string gcode;
std::string line;
while (reader >> line) {
gcode += line;
gcode += " ";
while (std::getline(reader.getStream(), line)) {
if (!line.empty()) {
Command cmd;
cmd.setFromGCode(line);
addCommandNoRecalc(cmd);
}
}
setFromGCode(gcode);
recalculate(); // Only once, after all commands are loaded
}

View File

@@ -57,6 +57,7 @@ public:
// interface
void clear(); // clears the internal data
void addCommand(const Command& Cmd); // adds a command at the end
void addCommandNoRecalc(const Command& Cmd); // adds a command without recalculation
void insertCommand(const Command& Cmd, int); // inserts a command
void deleteCommand(int); // deletes a command
double getLength(); // return the Length (mm) of the Path

View File

@@ -208,9 +208,16 @@ class TestPathCommandAnnotations(PathTestBase):
self.assertEqual(c.Annotations["depth"], "10mm")
# Annotations should not appear in gcode output
self.assertNotIn("operation", gcode)
self.assertNotIn("tapping", gcode)
self.assertNotIn("thread", gcode)
gcode_parts = gcode.split(";", 1)
main_gcode = gcode_parts[0]
comment = gcode_parts[1] if len(gcode_parts) > 1 else ""
self.assertIn("operation:'tapping'", comment)
self.assertIn("thread:'M6x1.0'", comment)
self.assertIn("depth:'10mm'", comment)
self.assertNotIn("operation", main_gcode)
self.assertNotIn("thread", main_gcode)
self.assertNotIn("depth", main_gcode)
def test11(self):
"""Test save/restore with mixed string and numeric annotations (in-memory)."""
@@ -301,6 +308,6 @@ class TestPathCommandAnnotations(PathTestBase):
self.assertIsInstance(complex_restored.Annotations["operation_id"], str)
self.assertIsInstance(complex_restored.Annotations["thread_spec"], str)
# Check scientific notation
self.assertAlmostEqual(complex_restored.Annotations["scientific"], 1.23e-6, places=8)
# Check scientific notation (now only 6 decimal places)
self.assertAlmostEqual(complex_restored.Annotations["scientific"], 1.23e-6, places=6)
self.assertIsInstance(complex_restored.Annotations["scientific"], float)

View File

@@ -138,6 +138,14 @@ const char* GCodeParser::ParseFloat(const char* ptr, float* retFloat)
bool GCodeParser::ParseLine(const char* ptr)
{
// Truncate at first semicolon (annotations / comment)
const char* comment = strchr(ptr, ';');
std::string line;
if (comment) {
line = std::string(ptr, comment - ptr);
ptr = line.c_str();
}
GCToken token;
bool validMotion = false;
bool exitLoop = false;