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:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"""
|
||||
...
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user