diff --git a/src/Mod/CAM/App/Command.cpp b/src/Mod/CAM/App/Command.cpp index f7496834e6..59e9afa31d 100644 --- a/src/Mod/CAM/App/Command.cpp +++ b/src/Mod/CAM/App/Command.cpp @@ -25,7 +25,6 @@ #include #include - #include #include #include @@ -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(pair.second)) { + str << "'" << std::get(pair.second) << "'"; + } + else if (std::holds_alternative(pair.second)) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(6) << std::get(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() << "" << 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.second)) { - writer.Stream() << " type=\"string\" value=\"" - << std::get(annotation.second) << "\" />" << std::endl; + writer.Stream() << " annotations=\""; + bool first = true; + for (const auto& pair : Annotations) { + if (!first) { + writer.Stream() << " "; } - else if (std::holds_alternative(annotation.second)) { - writer.Stream() << " type=\"double\" value=\"" - << std::get(annotation.second) << "\" />" << std::endl; + first = false; + writer.Stream() << pair.first << ":"; + if (std::holds_alternative(pair.second)) { + writer.Stream() << "'" << std::get(pair.second) << "'"; + } + else if (std::holds_alternative(pair.second)) { + writer.Stream() << std::fixed << std::setprecision(6) + << std::get(pair.second); } } - - writer.decInd(); - writer.Stream() << writer.ind() << "" << 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("gcode"); setFromGCode(gcode); - // Clear any existing annotations Annotations.clear(); - // Check if there are annotations to restore - int annotationCount = reader.getAttribute("annotationCount", 0); + std::string attr; + try { + attr = reader.getAttribute("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("key"); - std::string type = reader.getAttribute("type"); - std::string value = reader.getAttribute("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"); } } diff --git a/src/Mod/CAM/App/Command.pyi b/src/Mod/CAM/App/Command.pyi index 16f2d3e791..a90117313c 100644 --- a/src/Mod/CAM/App/Command.pyi +++ b/src/Mod/CAM/App/Command.pyi @@ -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""" ... diff --git a/src/Mod/CAM/App/Path.cpp b/src/Mod/CAM/App/Path.cpp index be3c9cf1fb..bbac240cdb 100644 --- a/src/Mod/CAM/App/Path.cpp +++ b/src/Mod/CAM/App/Path.cpp @@ -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 } diff --git a/src/Mod/CAM/App/Path.h b/src/Mod/CAM/App/Path.h index 7647630669..1912e0f6b6 100644 --- a/src/Mod/CAM/App/Path.h +++ b/src/Mod/CAM/App/Path.h @@ -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 diff --git a/src/Mod/CAM/CAMTests/TestPathCommandAnnotations.py b/src/Mod/CAM/CAMTests/TestPathCommandAnnotations.py index e834de9b21..8416535e66 100644 --- a/src/Mod/CAM/CAMTests/TestPathCommandAnnotations.py +++ b/src/Mod/CAM/CAMTests/TestPathCommandAnnotations.py @@ -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) diff --git a/src/Mod/CAM/PathSimulator/AppGL/GCodeParser.cpp b/src/Mod/CAM/PathSimulator/AppGL/GCodeParser.cpp index b4458efe73..bd7fb03def 100644 --- a/src/Mod/CAM/PathSimulator/AppGL/GCodeParser.cpp +++ b/src/Mod/CAM/PathSimulator/AppGL/GCodeParser.cpp @@ -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;