diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..de1afee --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell scripts: use LF so they run on Linux/macOS/WSL +*.sh text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5999510 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +######################### +# general patterns +######################### + +docs/html +docs/tagfile.xml + +*/bin/* +!*/bin/data/ + +# for bin folder in root +/bin/* +!/bin/data/ + +[Bb]uild/ +[Oo]bj/ +*.o +[Dd]ebug*/ +[Rr]elease*/ +*.mode* +*.app/ +*.pyc +.svn/ + +######################### +# IDE +######################### + +# XCode +*.pbxuser +*.perspective +*.perspectivev3 +*.mode1v3 +*.mode2v3 +#XCode 4 +xcuserdata +*.xcworkspace + +# Code::Blocks +*.depend +*.layout +*.cbTemp + +# Visual Studio +*.sdf +*.opensdf +*.suo +*.pdb +*.ilk +*.aps +ipch/ +.vs +*.sln +*.vcxproj +*.vcxproj.* + +# Eclipse +.metadata +local.properties +.externalToolBuilders + +# Codelite +*.session +*.tags +*.workspace.* + +######################### +# operating system +######################### + +# Linux +*~ +# KDE +.directory +.AppleDouble + +# OSX +.DS_Store +*.swp +*~.nib +# Thumbnails +._* + +# Windows +# Windows image file caches +Thumbs.db +# Folder config file +Desktop.ini + +#Android +.csettings + +######################### +# packages +######################### + +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases +*.log +*.sql +*.sqlite + +######################### +# DLL +######################### +*.dll +example_ui/icon.rc +example_ui/addons.make diff --git a/example-bezier_arcs/Makefile b/example-bezier_arcs/Makefile new file mode 100644 index 0000000..fdfbd4b --- /dev/null +++ b/example-bezier_arcs/Makefile @@ -0,0 +1,13 @@ +# Attempt to load a config.make file. +# If none is found, project defaults in config.project.make will be used. +ifneq ($(wildcard config.make),) + include config.make +endif + +# make sure the OF_ROOT location is defined +ifndef OF_ROOT + OF_ROOT=../../.. +endif + +# call the project makefile! +include $(OF_ROOT)/libs/openFrameworksCompiled/project/makefileCommon/compile.project.mk diff --git a/example-bezier_arcs/addons.make b/example-bezier_arcs/addons.make new file mode 100644 index 0000000..22d9976 --- /dev/null +++ b/example-bezier_arcs/addons.make @@ -0,0 +1 @@ +ofxGCode diff --git a/example-bezier_arcs/src/main.cpp b/example-bezier_arcs/src/main.cpp new file mode 100644 index 0000000..4df991f --- /dev/null +++ b/example-bezier_arcs/src/main.cpp @@ -0,0 +1,8 @@ +#include "ofMain.h" +#include "ofApp.h" + +//======================================================================== +int main( ){ + ofSetupOpenGL(800, 700, OF_WINDOW); + ofRunApp(new ofApp()); +} diff --git a/example-bezier_arcs/src/ofApp.cpp b/example-bezier_arcs/src/ofApp.cpp new file mode 100644 index 0000000..650af83 --- /dev/null +++ b/example-bezier_arcs/src/ofApp.cpp @@ -0,0 +1,157 @@ +#include "ofApp.h" + +// ============================================================================ +// example-bezier_arcs +// +// Demonstrates bezier_arc() — a biarc approximation of cubic Bezier curves +// that produces G2/G3 circular-arc commands instead of many short G1 lines. +// +// The screen preview looks identical for both methods; the difference is +// visible by opening the two saved .nc files in a G-code viewer such as +// https://ncviewer.com/ +// +// Key API: +// gcode.bezier(p1, c1, c2, p2) → many G1 line segments +// gcode.bezier_arc(p1, c1, c2, p2, tol) → few G2/G3 arc commands +// gcode.save("name.nc") → G1-only output +// gcode.save_arcs("name.nc") → G1 + G2/G3 output +// ============================================================================ + +void ofApp::setup(){ + ofBackground(250); + + // 100 pixels per inch is the default for AxiDraw-style plotters + gcode.setup(100); + + const float W = ofGetWidth(); + const float H = ofGetHeight(); + + // ------------------------------------------------------------------------- + // Section 1 — Side-by-side S-curve comparison + // + // Left (x ≈ 50–350) : drawn with bezier() → G1 straight lines + // Right (x ≈ 450–750) : drawn with bezier_arc() → G2/G3 arc commands + // + // Both look the same on screen; open the saved .nc files to see the + // difference in the G-code. + // ------------------------------------------------------------------------- + + ofVec2f p1(50, 220), c1(160, 60), c2(280, 380), p2(370, 220); + + // Left: classic tessellation (50 line segments) + gcode.bezier(p1, c1, c2, p2, 50); + + // Right: biarc approximation, tolerance = 1 pixel + ofVec2f shift(400, 0); + gcode.bezier_arc(p1 + shift, c1 + shift, c2 + shift, p2 + shift, 1.0f); + + // Control-point handles (drawn with plain lines — appear in both saves) + ofSetColor(180); + gcode.line(p1, c1); + gcode.line(p2, c2); + gcode.line(p1+shift, c1+shift); + gcode.line(p2+shift, c2+shift); + + + // ------------------------------------------------------------------------- + // Section 2 — Petal / rosette shape + // + // A realistic creative use-case: a loop of connected Bezier curves. + // Using bezier_arc() keeps the G-code compact and arc-smooth. + // ------------------------------------------------------------------------- + + ofVec2f centre(W * 0.5f, H * 0.62f); + const int petals = 7; + const float r_tip = 110.f; + const float r_cp = 65.f; + + for (int i = 0; i < petals; i++){ + float a0 = TWO_PI * (float)i / petals - HALF_PI; + float a1 = TWO_PI * (float)(i + 1) / petals - HALF_PI; + + ofVec2f tip0 = centre + ofVec2f(cos(a0), sin(a0)) * r_tip; + ofVec2f tip1 = centre + ofVec2f(cos(a1), sin(a1)) * r_tip; + + // Control points angled inward toward the centre + float mid_angle = (a0 + a1) * 0.5f; + ofVec2f cp0 = centre + ofVec2f(cos(mid_angle - 0.45f), sin(mid_angle - 0.45f)) * r_cp; + ofVec2f cp1 = centre + ofVec2f(cos(mid_angle + 0.45f), sin(mid_angle + 0.45f)) * r_cp; + + gcode.bezier_arc(tip0, cp0, cp1, tip1, 0.5f); + } + + + // ------------------------------------------------------------------------- + // Section 3 — Tolerance comparison + // + // Same curve drawn at three tolerances. Tighter tolerance → more arcs, + // closer to the true Bezier. Looser → fewer arcs, more visible deviation. + // + // Left : 0.25 px (very tight — usually 4-8 arcs per curve) + // Centre: 2.0 px (typical plotter quality) + // Right : 8.0 px (loose — may be visually noticeably different) + // ------------------------------------------------------------------------- + + ofVec2f q1(50, H - 100), qc1(140, H - 200), qc2(250, H), q2(340, H - 100); + + struct { float tol; ofVec2f offset; } cases[] = { + { 0.25f, ofVec2f( 0, 0) }, + { 2.0f, ofVec2f(230, 0) }, + { 8.0f, ofVec2f(460, 0) }, + }; + for (auto& c : cases){ + gcode.bezier_arc(q1 + c.offset, qc1 + c.offset, + qc2 + c.offset, q2 + c.offset, c.tol); + } + + + // ------------------------------------------------------------------------- + // Save both formats so you can compare them in a G-code viewer + // ------------------------------------------------------------------------- + + // Traditional output (all G1 straight-line segments) + gcode.save("bezier_lines_g1.nc"); + + // Arc-aware output (G2/G3 where bezier_arc() or arc() was used, G1 elsewhere) + gcode.save_arcs("bezier_arcs_g2g3.nc"); + + ofLogNotice("example-bezier_arcs") + << "Saved bezier_lines_g1.nc and bezier_arcs_g2g3.nc\n" + << "Open both in https://ncviewer.com/ to compare arc vs line output."; +} + +//-------------------------------------------------------------- +void ofApp::draw(){ + + // Both bezier() and bezier_arc() add a linearised preview to gcode.lines, + // so draw() looks identical for both — the difference is only in the file. + gcode.draw(); + + // Labels + ofSetColor(80); + ofDrawBitmapString("bezier() G1 lines", 60, 30); + ofDrawBitmapString("bezier_arc() G2/G3 arcs", 460, 30); + + ofDrawBitmapString("tolerance: 0.25 px", 60, ofGetHeight() - 120); + ofDrawBitmapString("tolerance: 2.0 px", 290, ofGetHeight() - 120); + ofDrawBitmapString("tolerance: 8.0 px", 520, ofGetHeight() - 120); + + ofSetColor(140); + ofDrawBitmapString("Open output/bezier_lines_g1.nc and output/bezier_arcs_g2g3.nc\n" + "in ncviewer.com to compare G-code output.", + 20, ofGetHeight() - 18); +} + +//-------------------------------------------------------------- +void ofApp::update(){} +void ofApp::keyPressed(int key){} +void ofApp::keyReleased(int key){} +void ofApp::mouseMoved(int x, int y){} +void ofApp::mouseDragged(int x, int y, int button){} +void ofApp::mousePressed(int x, int y, int button){} +void ofApp::mouseReleased(int x, int y, int button){} +void ofApp::mouseEntered(int x, int y){} +void ofApp::mouseExited(int x, int y){} +void ofApp::windowResized(int w, int h){} +void ofApp::gotMessage(ofMessage msg){} +void ofApp::dragEvent(ofDragInfo dragInfo){} diff --git a/example-bezier_arcs/src/ofApp.h b/example-bezier_arcs/src/ofApp.h new file mode 100644 index 0000000..a2ccebf --- /dev/null +++ b/example-bezier_arcs/src/ofApp.h @@ -0,0 +1,25 @@ +#pragma once + +#include "ofMain.h" +#include "ofxGCode.hpp" + +class ofApp : public ofBaseApp { +public: + void setup(); + void update(); + void draw(); + + void keyPressed(int key); + void keyReleased(int key); + void mouseMoved(int x, int y); + void mouseDragged(int x, int y, int button); + void mousePressed(int x, int y, int button); + void mouseReleased(int x, int y, int button); + void mouseEntered(int x, int y); + void mouseExited(int x, int y); + void windowResized(int w, int h); + void dragEvent(ofDragInfo dragInfo); + void gotMessage(ofMessage msg); + + ofxGCode gcode; +}; diff --git a/src/GCodeParser.cpp b/src/GCodeParser.cpp new file mode 100644 index 0000000..68c54fa --- /dev/null +++ b/src/GCodeParser.cpp @@ -0,0 +1,299 @@ +// +// GCodeParser.cpp +// Parses G-code text into GCodeToolpath. +// + +#include "GCodeParser.h" +#include +#include +#include + +GCodeParser::GCodeParser() { + reset(); +} + +void GCodeParser::reset() { + m_currentPos = glm::vec3(0); + m_feedRate = 0; + m_motionMode = 0; // G0 (rapid) + m_distanceMode = 90; // Absolute + m_unitMode = 21; // mm +} + +GCodeToolpath GCodeParser::parse(const std::string& gcodeText) { + reset(); + GCodeToolpath toolpath; + + std::istringstream stream(gcodeText); + std::string line; + int lineNumber = 0; + + while (std::getline(stream, line)) { + parseLine(line, lineNumber, toolpath); + lineNumber++; + } + + toolpath.computeBoundsAndStats(); + return toolpath; +} + +void GCodeParser::parseLine(const std::string& line, int lineNumber, GCodeToolpath& toolpath) { + std::string clean = stripComments(line); + + // Skip empty lines + if (clean.empty()) return; + + // Skip lines that are only whitespace + bool allSpace = true; + for (char c : clean) { + if (!std::isspace((unsigned char)c)) { allSpace = false; break; } + } + if (allSpace) return; + + WordSet words = tokenize(clean); + if (words.words.empty()) return; + + processWords(words, lineNumber, toolpath); +} + +std::string GCodeParser::stripComments(const std::string& line) { + std::string result; + result.reserve(line.size()); + bool inParenComment = false; + + for (size_t i = 0; i < line.size(); i++) { + char c = line[i]; + if (c == ';') { + // Rest of line is comment + break; + } + if (c == '(') { + inParenComment = true; + continue; + } + if (c == ')') { + inParenComment = false; + continue; + } + if (!inParenComment) { + result += c; + } + } + return result; +} + +GCodeParser::WordSet GCodeParser::tokenize(const std::string& cleanLine) { + WordSet ws; + size_t i = 0; + size_t len = cleanLine.size(); + + while (i < len) { + // Skip whitespace + while (i < len && std::isspace((unsigned char)cleanLine[i])) i++; + if (i >= len) break; + + char letter = std::toupper((unsigned char)cleanLine[i]); + if (!std::isalpha((unsigned char)letter)) { + i++; + continue; + } + i++; + + // Skip whitespace between letter and number + while (i < len && std::isspace((unsigned char)cleanLine[i])) i++; + + // Parse the number + std::string numStr; + while (i < len && (std::isdigit((unsigned char)cleanLine[i]) || + cleanLine[i] == '.' || cleanLine[i] == '-' || cleanLine[i] == '+')) { + numStr += cleanLine[i]; + i++; + } + + float value = 0; + if (!numStr.empty()) { + try { value = std::stof(numStr); } + catch (...) { value = 0; } + } + + // For G and M codes, allow multiple per line (G0 X10 G1 Y20 is unusual but possible) + // We store the last occurrence of each letter + ws.words[letter] = value; + } + + return ws; +} + +void GCodeParser::processWords(const WordSet& words, int lineNumber, GCodeToolpath& toolpath) { + // Handle G-codes (mode changes) + if (words.has('G')) { + int gCode = (int)words.get('G'); + switch (gCode) { + case 0: m_motionMode = 0; break; // Rapid + case 1: m_motionMode = 1; break; // Linear feed + case 2: m_motionMode = 2; break; // CW arc + case 3: m_motionMode = 3; break; // CCW arc + case 17: break; // XY plane (only supported plane) + case 18: break; // XZ plane (parsed but not implemented) + case 19: break; // YZ plane (parsed but not implemented) + case 20: m_unitMode = 20; break; // Inches + case 21: m_unitMode = 21; break; // Millimeters + case 28: { + // Home: move to origin + GCodeMove move; + move.type = MoveType::Rapid; + move.start = m_currentPos; + move.end = glm::vec3(0); + move.feedRate = 0; + move.sourceLineNumber = lineNumber; + if (glm::length(move.end - move.start) > 0.0001f) { + toolpath.moves.push_back(move); + } + m_currentPos = glm::vec3(0); + return; + } + case 90: m_distanceMode = 90; break; // Absolute + case 91: m_distanceMode = 91; break; // Incremental + case 92: { + // Set position (coordinate system offset) + if (words.has('X')) m_currentPos.x = words.get('X'); + if (words.has('Y')) m_currentPos.y = words.get('Y'); + if (words.has('Z')) m_currentPos.z = words.get('Z'); + return; + } + default: + // Unknown G-code, skip + break; + } + } + + // Handle feed rate + if (words.has('F')) { + m_feedRate = words.get('F'); + } + + // Handle M-codes (we don't generate moves for these, just log them) + // M0/M1=pause, M2/M30=end, M3-M5=spindle, M6=tool change, M7-M9=coolant + if (words.has('M') && !words.has('X') && !words.has('Y') && !words.has('Z')) { + return; + } + + // Check if there's any motion command (X, Y, Z, I, J, K coordinates) + bool hasMotion = words.has('X') || words.has('Y') || words.has('Z'); + bool hasArcParams = words.has('I') || words.has('J') || words.has('K') || words.has('R'); + + if (!hasMotion && !hasArcParams) return; + + // Handle arcs + if (m_motionMode == 2 || m_motionMode == 3) { + processArc(m_motionMode == 2, words, lineNumber, toolpath); + return; + } + + // Linear moves (G0/G1) + glm::vec3 target = m_currentPos; + + if (m_distanceMode == 90) { + // Absolute + if (words.has('X')) target.x = words.get('X'); + if (words.has('Y')) target.y = words.get('Y'); + if (words.has('Z')) target.z = words.get('Z'); + } else { + // Incremental + if (words.has('X')) target.x += words.get('X'); + if (words.has('Y')) target.y += words.get('Y'); + if (words.has('Z')) target.z += words.get('Z'); + } + + // Unit conversion + if (m_unitMode == 20) { + // Convert inches to mm for internal representation + if (words.has('X')) target.x *= 25.4f; + if (words.has('Y')) target.y *= 25.4f; + if (words.has('Z')) target.z *= 25.4f; + } + + // Only add move if there's actual movement + if (glm::length(target - m_currentPos) < 0.00001f) return; + + GCodeMove move; + move.type = (m_motionMode == 0) ? MoveType::Rapid : MoveType::Linear; + move.start = m_currentPos; + move.end = target; + move.feedRate = m_feedRate; + move.sourceLineNumber = lineNumber; + + toolpath.moves.push_back(move); + m_currentPos = target; +} + +void GCodeParser::processArc(bool clockwise, const WordSet& words, int lineNumber, GCodeToolpath& toolpath) { + glm::vec3 target = m_currentPos; + + if (m_distanceMode == 90) { + if (words.has('X')) target.x = words.get('X'); + if (words.has('Y')) target.y = words.get('Y'); + if (words.has('Z')) target.z = words.get('Z'); + } else { + if (words.has('X')) target.x += words.get('X'); + if (words.has('Y')) target.y += words.get('Y'); + if (words.has('Z')) target.z += words.get('Z'); + } + + // Unit conversion for target + if (m_unitMode == 20) { + if (words.has('X')) target.x *= 25.4f; + if (words.has('Y')) target.y *= 25.4f; + if (words.has('Z')) target.z *= 25.4f; + } + + glm::vec3 arcCenterAbs; + + if (words.has('R')) { + // Radius format: compute center from R + float R = words.get('R'); + if (m_unitMode == 20) R *= 25.4f; + + glm::vec2 p1(m_currentPos.x, m_currentPos.y); + glm::vec2 p2(target.x, target.y); + glm::vec2 mid = (p1 + p2) * 0.5f; + glm::vec2 diff = p2 - p1; + float dist = glm::length(diff); + + if (dist < 0.0001f || std::abs(R) < dist * 0.5f) { + toolpath.errors.push_back("Line " + std::to_string(lineNumber + 1) + ": Invalid arc radius"); + m_currentPos = target; + return; + } + + float h = std::sqrt(R * R - (dist * 0.5f) * (dist * 0.5f)); + glm::vec2 perp(-diff.y / dist, diff.x / dist); + + // Sign of R determines which center to use + if ((clockwise && R > 0) || (!clockwise && R < 0)) { + arcCenterAbs = glm::vec3(mid - perp * h, m_currentPos.z); + } else { + arcCenterAbs = glm::vec3(mid + perp * h, m_currentPos.z); + } + } else { + // I, J, K format (offsets from start point -- always incremental in standard G-code) + float I = words.get('I', 0); + float J = words.get('J', 0); + float K = words.get('K', 0); + + if (m_unitMode == 20) { I *= 25.4f; J *= 25.4f; K *= 25.4f; } + + arcCenterAbs = m_currentPos + glm::vec3(I, J, K); + } + + GCodeMove move; + move.type = clockwise ? MoveType::ArcCW : MoveType::ArcCCW; + move.start = m_currentPos; + move.end = target; + move.arcCenter = arcCenterAbs; + move.feedRate = m_feedRate; + move.sourceLineNumber = lineNumber; + + toolpath.moves.push_back(move); + m_currentPos = target; +} diff --git a/src/GCodeParser.h b/src/GCodeParser.h new file mode 100644 index 0000000..bda13d7 --- /dev/null +++ b/src/GCodeParser.h @@ -0,0 +1,57 @@ +#pragma once +// +// GCodeParser.h +// Parses G-code text into a GCodeToolpath. +// Supports G0, G1, G2, G3, G17-G19, G20/G21, G28, G90/G91, M-codes, comments. +// + +#include "GCodeToolpath.h" +#include +#include +#include + +class GCodeParser { +public: + GCodeParser(); + + // Parse a full G-code string (may contain multiple lines) + GCodeToolpath parse(const std::string& gcodeText); + + // Parse a single line, updating internal state and appending to toolpath + void parseLine(const std::string& line, int lineNumber, GCodeToolpath& toolpath); + + // Reset parser state to defaults + void reset(); + +private: + // Machine state + glm::vec3 m_currentPos; + float m_feedRate; + + // Modal state + int m_motionMode; // 0=G0, 1=G1, 2=G2, 3=G3 + int m_distanceMode; // 90=absolute, 91=incremental + int m_unitMode; // 20=inches, 21=mm + + // Parsed word values from a single line + struct WordSet { + std::map words; + bool has(char c) const { return words.count(c) > 0; } + float get(char c, float def = 0) const { + auto it = words.find(c); + return it != words.end() ? it->second : def; + } + }; + + // Strip comments and return cleaned line + std::string stripComments(const std::string& line); + + // Tokenize a cleaned line into letter-value pairs + WordSet tokenize(const std::string& cleanLine); + + // Process G-code words and generate moves + void processWords(const WordSet& words, int lineNumber, GCodeToolpath& toolpath); + + // Generate arc move (G2/G3) + void processArc(bool clockwise, const WordSet& words, int lineNumber, GCodeToolpath& toolpath); +}; diff --git a/src/GCodeToolpath.cpp b/src/GCodeToolpath.cpp new file mode 100644 index 0000000..61859db --- /dev/null +++ b/src/GCodeToolpath.cpp @@ -0,0 +1,188 @@ +// +// GCodeToolpath.cpp +// Implementation of GCodeMove and GCodeToolpath methods. +// + +#include "GCodeToolpath.h" +#include + +//-------------------------------------------------------------- +// Helper: compute arc sweep angle given start/end vectors from center +//-------------------------------------------------------------- +static float computeArcSweep(const glm::vec2& v1, const glm::vec2& v2, MoveType type) { + float angle1 = atan2(v1.y, v1.x); + float angle2 = atan2(v2.y, v2.x); + float sweep = angle2 - angle1; + if (type == MoveType::ArcCW) { + if (sweep > 0) sweep -= TWO_PI; + } else { + if (sweep < 0) sweep += TWO_PI; + } + return sweep; +} + +//-------------------------------------------------------------- +// GCodeMove +//-------------------------------------------------------------- + +float GCodeMove::getLength() const { + if (type == MoveType::ArcCW || type == MoveType::ArcCCW) { + float radius = glm::length(glm::vec2(start) - glm::vec2(arcCenter)); + glm::vec2 v1 = glm::vec2(start) - glm::vec2(arcCenter); + glm::vec2 v2 = glm::vec2(end) - glm::vec2(arcCenter); + float sweep = computeArcSweep(v1, v2, type); + float arcLen2D = std::abs(sweep) * radius; + float dz = end.z - start.z; + return std::sqrt(arcLen2D * arcLen2D + dz * dz); + } + return glm::length(end - start); +} + +std::vector GCodeMove::linearize(int segments) const { + std::vector pts; + if (type == MoveType::ArcCW || type == MoveType::ArcCCW) { + glm::vec2 center2D = glm::vec2(arcCenter); + glm::vec2 v1 = glm::vec2(start) - center2D; + glm::vec2 v2 = glm::vec2(end) - center2D; + float angle1 = atan2(v1.y, v1.x); + float sweep = computeArcSweep(v1, v2, type); + float radius = glm::length(v1); + pts.reserve(segments + 1); + for (int i = 0; i <= segments; i++) { + float t = (float)i / (float)segments; + float angle = angle1 + sweep * t; + float z = start.z + (end.z - start.z) * t; + pts.push_back(glm::vec3( + center2D.x + cos(angle) * radius, + center2D.y + sin(angle) * radius, + z + )); + } + } else { + pts.push_back(start); + pts.push_back(end); + } + return pts; +} + +//-------------------------------------------------------------- +// GCodeToolpath +//-------------------------------------------------------------- + +static constexpr int kArcBoundsSegments = 16; +static constexpr float kDefaultRapidRate = 3000.0f; // mm/min + +void GCodeToolpath::computeBoundsAndStats() { + if (moves.empty()) { + minBounds = maxBounds = glm::vec3(0); + totalDistance = 0; + estimatedTime = 0; + return; + } + minBounds = glm::vec3(FLT_MAX); + maxBounds = glm::vec3(-FLT_MAX); + totalDistance = 0; + estimatedTime = 0; + + for (const auto& m : moves) { + minBounds = glm::min(minBounds, glm::min(m.start, m.end)); + maxBounds = glm::max(maxBounds, glm::max(m.start, m.end)); + + if (m.type == MoveType::ArcCW || m.type == MoveType::ArcCCW) { + auto pts = m.linearize(kArcBoundsSegments); + for (const auto& p : pts) { + minBounds = glm::min(minBounds, p); + maxBounds = glm::max(maxBounds, p); + } + } + + float len = m.getLength(); + totalDistance += len; + + float rate = m.feedRate; + if (m.type == MoveType::Rapid || rate <= 0) { + rate = kDefaultRapidRate; + } + estimatedTime += (len / rate) * 60.0f; // seconds + } +} + +std::vector GCodeToolpath::getUniqueZLayers() const { + std::set zSet; + for (const auto& m : moves) { + float z1 = std::round(m.start.z * 1000.0f) / 1000.0f; + float z2 = std::round(m.end.z * 1000.0f) / 1000.0f; + zSet.insert(z1); + zSet.insert(z2); + } + return std::vector(zSet.begin(), zSet.end()); +} + +std::vector GCodeToolpath::getMovesAtZ(float z, float tolerance) const { + std::vector result; + for (const auto& m : moves) { + if (std::abs(m.start.z - z) <= tolerance || std::abs(m.end.z - z) <= tolerance) { + result.push_back(&m); + } + } + return result; +} + +glm::vec3 GCodeToolpath::getPositionAt(int moveIndex, float fraction) const { + if (moves.empty() || moveIndex < 0) return glm::vec3(0); + if (moveIndex >= (int)moves.size()) return moves.back().end; + const auto& m = moves[moveIndex]; + fraction = std::clamp(fraction, 0.0f, 1.0f); + if (m.type == MoveType::ArcCW || m.type == MoveType::ArcCCW) { + auto pts = m.linearize(32); + float totalLen = 0; + std::vector cumLen; + cumLen.push_back(0); + for (size_t i = 1; i < pts.size(); i++) { + totalLen += glm::length(pts[i] - pts[i - 1]); + cumLen.push_back(totalLen); + } + float targetLen = fraction * totalLen; + for (size_t i = 1; i < cumLen.size(); i++) { + if (cumLen[i] >= targetLen) { + float segFrac = (cumLen[i] - cumLen[i - 1]) > 0 ? + (targetLen - cumLen[i - 1]) / (cumLen[i] - cumLen[i - 1]) : 0; + return glm::mix(pts[i - 1], pts[i], segFrac); + } + } + return pts.back(); + } + return glm::mix(m.start, m.end, fraction); +} + +int GCodeToolpath::findMoveIndexForLine(int lineNumber) const { + for (int i = 0; i < (int)moves.size(); i++) { + if (moves[i].sourceLineNumber == lineNumber) return i; + } + // If exact match not found, find the closest move at or before this line + int best = -1; + for (int i = 0; i < (int)moves.size(); i++) { + if (moves[i].sourceLineNumber >= 0 && moves[i].sourceLineNumber <= lineNumber) { + best = i; + } + } + return best; +} + +float GCodeToolpath::moveIndexToPosition(int moveIndex) const { + if (moves.empty()) return 0.0f; + return std::clamp((float)(moveIndex + 1) / (float)moves.size(), 0.0f, 1.0f); +} + +int GCodeToolpath::positionToMoveIndex(float position) const { + if (moves.empty()) return -1; + position = std::clamp(position, 0.0f, 1.0f); + int idx = (int)(position * (float)moves.size()) - 1; + return std::clamp(idx, 0, (int)moves.size() - 1); +} + +int GCodeToolpath::getLineAtPosition(float position) const { + int idx = positionToMoveIndex(position); + if (idx < 0 || idx >= (int)moves.size()) return -1; + return moves[idx].sourceLineNumber; +} diff --git a/src/GCodeToolpath.h b/src/GCodeToolpath.h new file mode 100644 index 0000000..6e46ca6 --- /dev/null +++ b/src/GCodeToolpath.h @@ -0,0 +1,70 @@ +#pragma once +// +// GCodeToolpath.h +// Shared data structures for parsed G-code toolpaths. +// Used by GCodeParser (reading) and ofxGCode (generation). +// + +#include "ofMain.h" +#include +#include +#include +#include +#include + +enum class MoveType { + Rapid, // G0 - rapid positioning + Linear, // G1 - linear interpolation (feed move) + ArcCW, // G2 - clockwise arc + ArcCCW // G3 - counter-clockwise arc +}; + +struct GCodeMove { + MoveType type = MoveType::Rapid; + glm::vec3 start = glm::vec3(0); // XYZ start position + glm::vec3 end = glm::vec3(0); // XYZ end position + glm::vec3 arcCenter = glm::vec3(0); // Arc center (absolute position, for arcs only) + float feedRate = 0; + int sourceLineNumber = -1; // Line in source text (0-based, for editor sync) + + /// Approximate length of this move (arc-aware). + float getLength() const; + + /// Linearize this move into a polyline (useful for rendering arcs). + std::vector linearize(int segments = 32) const; +}; + +struct GCodeToolpath { + std::vector moves; + glm::vec3 minBounds = glm::vec3(0); + glm::vec3 maxBounds = glm::vec3(0); + float totalDistance = 0; + float estimatedTime = 0; // In seconds, estimated from feed rates + std::vector errors; // Parse errors/warnings + + /// Compute bounding box, total distance, and estimated time. + void computeBoundsAndStats(); + + /// Return sorted unique Z heights found in the toolpath. + std::vector getUniqueZLayers() const; + + /// Return pointers to moves at a given Z height (within tolerance). + std::vector getMovesAtZ(float z, float tolerance = 0.01f) const; + + /// Get interpolated position at a given move index and fraction through that move. + glm::vec3 getPositionAt(int moveIndex, float fraction) const; + + /// Find the first move index originating from a given source line number (0-based). + /// Returns -1 if no move maps to that line. + int findMoveIndexForLine(int lineNumber) const; + + /// Convert a move index to a normalized position (0.0-1.0) within the toolpath. + float moveIndexToPosition(int moveIndex) const; + + /// Convert a normalized position (0.0-1.0) to a move index. + int positionToMoveIndex(float position) const; + + /// Get the source line number at a given normalized position. + /// Returns -1 if no move is found. + int getLineAtPosition(float position) const; +}; diff --git a/src/ofxGCode.cpp b/src/ofxGCode.cpp index 911a3d6..a1e5559 100644 --- a/src/ofxGCode.cpp +++ b/src/ofxGCode.cpp @@ -32,11 +32,11 @@ void ofxGCode::set_size(int w, int h){ void ofxGCode::clear(){ lines.clear(); + segments.clear(); } void ofxGCode::draw(int max_lines_to_show){ - int draw_count = 0; if (max_lines_to_show <= 0) max_lines_to_show = lines.size(); int end_index = MIN(max_lines_to_show, lines.size()); @@ -86,12 +86,11 @@ void ofxGCode::draw(int max_lines_to_show){ } -//genertaes gcode and writes it to a file +//generates gcode and writes it to a file void ofxGCode::save(string name){ float inches_per_pixel = 1.0 / pixels_per_inch; vector commands; - commands.clear(); //pen up and positioned at the origin commands.push_back("M3 S0"); @@ -125,18 +124,17 @@ void ofxGCode::save(string name){ commands.push_back("M3 S0"); commands.push_back("G0 X0 Y0"); - cout<<"transit distance: "< ofxGCode::get_arc_pnts(ofVec2f center, float size, int steps, fl return pnts; } +vector ofxGCode::get_arc_points_ijk(ofVec2f start, ofVec2f end, ofVec2f center, bool clockwise, int steps){ + vector pnts; + + ofVec2f v1 = start - center; + ofVec2f v2 = end - center; + float angle1 = atan2(v1.y, v1.x); + float angle2 = atan2(v2.y, v2.x); + float radius = v1.length(); + + float sweep = angle2 - angle1; + if (clockwise) { + if (sweep > 0) sweep -= TWO_PI; + } else { + if (sweep < 0) sweep += TWO_PI; + } + + for (int i = 0; i <= steps; i++){ + float t = (float)i / (float)steps; + float angle = angle1 + sweep * t; + ofVec2f pos; + pos.x = center.x + cos(angle) * radius; + pos.y = center.y + sin(angle) * radius; + pnts.push_back(pos); + } + return pnts; +} + //Emulating the begin/end shape functionality void ofxGCode::begin_shape(){ shape_pnts.clear(); @@ -314,7 +339,7 @@ void ofxGCode::end_shape(bool close){ } } -//drawing polygone from points +//drawing polygon from points void ofxGCode::polygon(vector pnts, bool close_shape){ begin_shape(); for (int i=0; i new_lines){ } } -//Thick lines are just multiple lines, eenly spaced +//Thick lines are just multiple lines, evenly spaced void ofxGCode::thick_line(float x1, float y1, float x2, float y2, float spacing, int layers){ thick_line(ofVec2f(x1,y1), ofVec2f(x2,y2), spacing, layers); } @@ -792,7 +824,7 @@ void ofxGCode::trim_outside(ofRectangle bounds){ lines = trim_lines_outside(lines, bounds); } -//takes a list of lines and rmeoves any lines that intersect a satic line +//takes a list of lines and removes any lines that intersect a static line vector ofxGCode::trim_intersecting_lines(vector lines_to_trim, vector static_lines){ vector val; for (int i=0; i> ofxGCode::load_outlines(string file_path){ ofFile file(file_path); if(!file.exists()){ - cout<<"The outline file " << file_path << " is missing"<> ofxGCode::load_outlines(string file_path){ } //add the last shape if there's anything there - cout< 1){ outlines.push_back(cur_outline); } @@ -991,7 +1023,7 @@ vector ofxGCode::load_lines(string file_path){ ofFile file(file_path); if(!file.exists()){ - cout<<"The file " << file_path << " is missing"< p, ofVec2f pnt){ return checkInPolygon(p, pnt.x, pnt.y); } +//-------------------------------------------------------------- +// 3-axis G-code output +//-------------------------------------------------------------- + +string ofxGCode::toGCodeString(float safeZ){ + vector commands; + + // Preamble + commands.push_back("G21 ; mm mode"); + commands.push_back("G90 ; absolute positioning"); + commands.push_back("G0 Z" + ofToString(safeZ, 3)); + commands.push_back("G0 X0 Y0"); + + ofVec2f lastPos2D(0, 0); + bool penIsUp = true; + + for (size_t i = 0; i < lines.size(); i++){ + GLine line = lines[i]; + float z = (i < z_values.size()) ? z_values[i] : 0.0f; + + // If we're not at the start of this line, travel there + if (line.a != lastPos2D || penIsUp) { + if (!penIsUp) { + // Retract + commands.push_back("G0 Z" + ofToString(safeZ, 3)); + penIsUp = true; + } + // Rapid to start XY + commands.push_back("G0 X" + ofToString(line.a.x, 3) + " Y" + ofToString(line.a.y, 3)); + // Plunge to Z + commands.push_back("G1 Z" + ofToString(z, 3) + " F300"); + penIsUp = false; + } + + // Feed move to end point + commands.push_back("G1 X" + ofToString(line.b.x, 3) + " Y" + ofToString(line.b.y, 3) + " Z" + ofToString(z, 3)); + + lastPos2D = line.b; + } + + // Closing + commands.push_back("G0 Z" + ofToString(safeZ, 3)); + commands.push_back("G0 X0 Y0"); + commands.push_back("M2 ; end program"); + + string result; + for (const auto& cmd : commands) { + result += cmd + "\n"; + } + return result; +} + +void ofxGCode::save3D(string name, float safeZ){ + string gcodeStr = toGCodeString(safeZ); + + ofFile myTextFile; + myTextFile.open(name, ofFile::WriteOnly); + myTextFile << gcodeStr; + + ofLogNotice("ofxGCode") << "3D G-code saved to " << name; +} + + +// =========================================================================== +// Biarc Bezier approximation +// +// Implements the biarc method described by D. Lacko: +// http://dlacko.org/blog/2016/10/19/approximating-bezier-curves-by-biarcs/ +// +// This was inspired by Austin Whittier's Observable notebook: +// https://observablehq.com/@awhitty/approximating-bezier-curves-for-cnc +// Whittier's notebook explores circular arc approximation of Bezier curves +// for CNC G-code, and explicitly points to the biarc method (above) as +// "a better way" that guarantees G1 tangent continuity at arc joints — +// which is what this implementation uses. +// =========================================================================== + +// --------------------------------------------------------------------------- +// Internal helpers (file-scope static so they don't pollute the public API) +// --------------------------------------------------------------------------- + +static ofVec2f s_bezier_eval(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, float t) +{ + float mt = 1.0f - t; + return p1*(mt*mt*mt) + c1*(3.f*mt*mt*t) + c2*(3.f*mt*t*t) + p2*(t*t*t); +} + +// Unnormalized first derivative B'(t) of the cubic Bezier. +static ofVec2f s_bezier_deriv(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, float t) +{ + float mt = 1.0f - t; + return (c1-p1)*(3.f*mt*mt) + (c2-c1)*(6.f*mt*t) + (p2-c2)*(3.f*t*t); +} + +// Normalised tangent at t. Falls back to (1,0) for degenerate curves. +static ofVec2f s_bezier_tangent(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, float t) +{ + ofVec2f d = s_bezier_deriv(p1, c1, c2, p2, t); + float len = d.length(); + return (len < 1e-8f) ? ofVec2f(1.f, 0.f) : d*(1.f/len); +} + +// Intersection of parametric lines P1+s·D1 and P2+u·D2. +// Returns false when the lines are parallel. +static bool s_line_intersect(ofVec2f p1, ofVec2f d1, + ofVec2f p2, ofVec2f d2, + ofVec2f& result) +{ + float denom = d1.x*d2.y - d1.y*d2.x; + if (fabsf(denom) < 1e-10f) return false; + float s = ((p2.x-p1.x)*d2.y - (p2.y-p1.y)*d2.x) / denom; + result = p1 + d1*s; + return true; +} + +// Centre of the unique circle that is tangent to 'tangent' at 'p1' +// and also passes through 'p2'. +// Returns false when the points are coincident or the chord is parallel +// to the tangent (straight-line degenerate case). +static bool s_arc_center(ofVec2f p1, ofVec2f tangent, ofVec2f p2, ofVec2f& center) +{ + ofVec2f perp_t(-tangent.y, tangent.x); // perpendicular to tangent at p1 + ofVec2f mid = (p1 + p2) * 0.5f; + ofVec2f chord = p2 - p1; + if (chord.length() < 1e-10f) return false; + ofVec2f perp_chord(-chord.y, chord.x); // perpendicular bisector direction + return s_line_intersect(p1, perp_t, mid, perp_chord, center); +} + +// True when the arc travelling from 'point' (with that tangent) around +// 'center' is clockwise. +static bool s_arc_is_clockwise(ofVec2f point, ofVec2f center, ofVec2f tangent) +{ + ofVec2f r = point - center; + // cross(r, tangent) < 0 → tangent is to the right of r → CW + return (r.x*tangent.y - r.y*tangent.x) < 0.f; +} + +// --------------------------------------------------------------------------- +// Single biarc fit +// --------------------------------------------------------------------------- +struct BiArcFit { GArc arc1, arc2; bool valid = false; }; + +static BiArcFit s_fit_biarc(ofVec2f p1, ofVec2f t1, ofVec2f p2, ofVec2f t2) +{ + BiArcFit result; + + // --- join point G = incenter of triangle (P1, P2, V) ----------------- + ofVec2f V; + bool have_V = s_line_intersect(p1, t1, p2, t2, V); + + // Guard against near-parallel tangents (V flies to infinity) + bool use_mid = !have_V + || (V - p1).length() > 1e5f + || std::isnan(V.x) || std::isnan(V.y); + + ofVec2f G; + if (use_mid) { + G = (p1 + p2) * 0.5f; + } else { + float d_p2v = (p2 - V).length(); + float d_p1v = (p1 - V).length(); + float d_p1p2 = (p1 - p2).length(); + float perim = d_p2v + d_p1v + d_p1p2; + if (perim < 1e-10f) return result; // degenerate triangle + G = (p1*d_p2v + p2*d_p1v + V*d_p1p2) / perim; + } + + // --- arc 1: P1 → G, tangent T1 at P1 ---------------------------------- + ofVec2f C1; + bool ok1 = s_arc_center(p1, t1, G, C1); + float R1 = ok1 ? (p1 - C1).length() : 0.f; + bool cw1 = ok1 ? s_arc_is_clockwise(p1, C1, t1) : false; + + result.arc1.start = p1; + result.arc1.end = G; + result.arc1.center = C1; + result.arc1.radius = ok1 ? R1 : 0.f; + result.arc1.clockwise = cw1; + + // --- arc 2: G → P2, tangent T2 at P2 ---------------------------------- + ofVec2f C2; + bool ok2 = s_arc_center(p2, t2, G, C2); + float R2 = ok2 ? (p2 - C2).length() : 0.f; + bool cw2 = ok2 ? s_arc_is_clockwise(p2, C2, t2) : false; + + result.arc2.start = G; + result.arc2.end = p2; + result.arc2.center = C2; + result.arc2.radius = ok2 ? R2 : 0.f; + result.arc2.clockwise = cw2; + + result.valid = true; + return result; +} + +// --------------------------------------------------------------------------- +// Hausdorff distance from B(t) on [t0, t1] to a full circle (C, R). +// +// The one-sided Hausdorff distance is: +// +// max_{t ∈ [t0,t1]} |dist(B(t), C) - R| +// +// The interior extrema of dist(B(t), C) occur where its derivative is zero: +// +// d/dt dist(B(t), C) = (B(t) - C) · B'(t) / dist(B(t), C) = 0 +// +// ⟹ (B(t) - C) · B'(t) = 0 (degree-5 polynomial) +// +// We find all roots in [t0, t1] by scanning for sign changes at N_COARSE +// equally-spaced samples, then bisecting each bracket to 16 iterations. +// The maximum radial error over endpoints + all critical points is returned. +// --------------------------------------------------------------------------- +static float s_hausdorff_bezier_circle( + ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, + float t0, float t1, + ofVec2f C, float R) +{ + // Radial error at a given parameter + auto err_at = [&](float t) -> float { + return fabsf((s_bezier_eval(p1, c1, c2, p2, t) - C).length() - R); + }; + + // Value of (B(t)-C)·B'(t) — zero at critical points of dist(B(t), C) + auto radial_dot = [&](float t) -> float { + ofVec2f r = s_bezier_eval(p1, c1, c2, p2, t) - C; + ofVec2f dp = s_bezier_deriv(p1, c1, c2, p2, t); + return r.x*dp.x + r.y*dp.y; + }; + + const int N_COARSE = 32; + float max_err = MAX(err_at(t0), err_at(t1)); // always check endpoints + float prev_t = t0; + float prev_dot = radial_dot(t0); + + for (int i = 1; i <= N_COARSE; i++) { + float t = t0 + (t1 - t0) * (float)i / (float)N_COARSE; + float dot = radial_dot(t); + max_err = MAX(max_err, err_at(t)); + + if (prev_dot * dot < 0.f) { + // Sign change → critical point in (prev_t, t): bisect to refine + float lo = prev_t, hi = t; + for (int k = 0; k < 16; k++) { + float mid = (lo + hi) * 0.5f; + if (radial_dot(mid) * prev_dot < 0.f) hi = mid; + else lo = mid; + } + max_err = MAX(max_err, err_at((lo + hi) * 0.5f)); + } + + prev_dot = dot; + prev_t = t; + } + return max_err; +} + +// --------------------------------------------------------------------------- +// Hausdorff error of a biarc fit against the true Bezier. +// +// Arc 1 covers the first half of the curve parameter range [0, 0.5], +// arc 2 covers [0.5, 1]. The split at 0.5 is an approximation of the +// true parameter value at the biarc join point G; it is tight enough in +// practice because recursive subdivision keeps each segment small. +// +// Degenerate (straight-line) arcs fall back to point-to-segment distance. +// --------------------------------------------------------------------------- +static float s_biarc_error(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, + const BiArcFit& fit) +{ + // Helper: max distance from Bezier samples on [ta, tb] to a line segment + auto line_error = [&](float ta, float tb, ofVec2f A, ofVec2f B) -> float { + float max_d = 0.f; + ofVec2f seg = B - A; + float seg_len = seg.length(); + for (int i = 0; i <= 8; i++) { + float t = ta + (tb - ta) * (float)i / 8.f; + ofVec2f P = s_bezier_eval(p1, c1, c2, p2, t); + float d; + if (seg_len < 1e-8f) { + d = (P - A).length(); + } else { + ofVec2f dv = seg * (1.f / seg_len); + float proj = ofClamp((P - A).dot(dv), 0.f, seg_len); + d = (P - (A + dv * proj)).length(); + } + if (d > max_d) max_d = d; + } + return max_d; + }; + + float err1 = fit.arc1.isLine() + ? line_error(0.f, 0.5f, fit.arc1.start, fit.arc1.end) + : s_hausdorff_bezier_circle(p1, c1, c2, p2, 0.f, 0.5f, + fit.arc1.center, fit.arc1.radius); + + float err2 = fit.arc2.isLine() + ? line_error(0.5f, 1.f, fit.arc2.start, fit.arc2.end) + : s_hausdorff_bezier_circle(p1, c1, c2, p2, 0.5f, 1.f, + fit.arc2.center, fit.arc2.radius); + + return MAX(err1, err2); +} + +// --------------------------------------------------------------------------- +// Recursive subdivision +// --------------------------------------------------------------------------- +static void s_bezier_to_biarcs(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, + float tolerance, int depth, + vector& out) +{ + // Skip zero-length segments + if ((p1 - p2).length() < 0.001f) return; + + if (depth <= 0) { + // Max recursion reached: fall back to a straight line segment + GArc a; + a.start = p1; a.end = p2; a.radius = 0.f; + out.push_back(a); + return; + } + + ofVec2f t1 = s_bezier_tangent(p1, c1, c2, p2, 0.f); + ofVec2f t2 = s_bezier_tangent(p1, c1, c2, p2, 1.f); + + BiArcFit fit = s_fit_biarc(p1, t1, p2, t2); + + if (!fit.valid) { + GArc a; + a.start = p1; a.end = p2; a.radius = 0.f; + out.push_back(a); + return; + } + + if (s_biarc_error(p1, c1, c2, p2, fit) <= tolerance) { + out.push_back(fit.arc1); + out.push_back(fit.arc2); + } else { + // De Casteljau subdivision at t = 0.5 + ofVec2f m01 = (p1 + c1) * 0.5f; + ofVec2f m12 = (c1 + c2) * 0.5f; + ofVec2f m23 = (c2 + p2) * 0.5f; + ofVec2f m012 = (m01 + m12) * 0.5f; + ofVec2f m123 = (m12 + m23) * 0.5f; + ofVec2f m0123 = (m012 + m123) * 0.5f; + + s_bezier_to_biarcs(p1, m01, m012, m0123, tolerance, depth-1, out); + s_bezier_to_biarcs(m0123, m123, m23, p2, tolerance, depth-1, out); + } +} + +// --------------------------------------------------------------------------- +// Public static: decompose Bezier → biarcs +// --------------------------------------------------------------------------- +vector ofxGCode::bezier_to_biarcs(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, + float tolerance, int max_depth) +{ + vector out; + s_bezier_to_biarcs(p1, c1, c2, p2, tolerance, max_depth, out); + return out; +} + +// --------------------------------------------------------------------------- +// bezier_arc() — draw a Bezier using biarcs +// --------------------------------------------------------------------------- +void ofxGCode::bezier_arc(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, float tolerance) +{ + // Transform control points to screen space (honours ofTranslate/Scale/Rotate) + ofVec2f tp1 = getModelPoint(p1); + ofVec2f tc1 = getModelPoint(c1); + ofVec2f tc2 = getModelPoint(c2); + ofVec2f tp2 = getModelPoint(p2); + + // --- Preview: add linearised curve to lines so draw() works as usual --- + { + vector pnts = get_bezier_pnts(tp1, tc1, tc2, tp2, 20); + for (int i = 0; i < (int)pnts.size()-1; i++) { + ofVec2f a = pnts[i], b = pnts[i+1]; + if (clip.clip(a, b)) { + GLine l; + l.set(a, b); + lines.push_back(l); + } + } + } + + // --- Arc segments for the G2/G3 save pipeline ------------------------- + vector arcs = bezier_to_biarcs(tp1, tc1, tc2, tp2, tolerance); + for (const GArc& a : arcs) { + GSegment seg; + if (a.isLine()) { + seg.type = GSegment::Type::Line; + } else { + seg.type = a.clockwise ? GSegment::Type::ArcCW : GSegment::Type::ArcCCW; + } + seg.start = a.start; + seg.end = a.end; + seg.center = a.center; + segments.push_back(seg); + } +} + +// --------------------------------------------------------------------------- +// arc() — add a single circular arc directly +// --------------------------------------------------------------------------- +void ofxGCode::arc(ofVec2f start, ofVec2f end, ofVec2f center, bool clockwise) +{ + ofVec2f ts = getModelPoint(start); + ofVec2f te = getModelPoint(end); + ofVec2f tc = getModelPoint(center); + + // Linearised preview → lines + { + vector pnts = get_arc_points_ijk(ts, te, tc, clockwise, 32); + for (int i = 0; i < (int)pnts.size()-1; i++) { + ofVec2f a = pnts[i], b = pnts[i+1]; + if (clip.clip(a, b)) { + GLine l; + l.set(a, b); + lines.push_back(l); + } + } + } + // Arc segment for G2/G3 output + GSegment seg; + seg.type = clockwise ? GSegment::Type::ArcCW : GSegment::Type::ArcCCW; + seg.start = ts; + seg.end = te; + seg.center = tc; + segments.push_back(seg); +} +// --------------------------------------------------------------------------- +// save_arcs() — pen-plotter save with G2/G3 arc commands +// --------------------------------------------------------------------------- +void ofxGCode::save_arcs(string name) +{ + const float ipp = 1.0f / pixels_per_inch; + vector commands; + commands.push_back("M3 S0"); + commands.push_back("G0 X0 Y0"); + + ofVec2f last_pos(0.f, 0.f); + bool pen_is_down = false; + + for (const GSegment& seg : segments) { + ofVec2f s(seg.start.x * ipp, seg.start.y * ipp); + ofVec2f e(seg.end.x * ipp, seg.end.y * ipp); + ofVec2f c(seg.center.x * ipp, seg.center.y * ipp); + + // Rapid to start of this segment if needed + if (s != last_pos) { + if (pen_is_down) { + commands.push_back("M3 S0"); + pen_is_down = false; + } + commands.push_back("G0 X" + ofToString(s.x, 4) + " Y" + ofToString(s.y, 4)); + } + + // Pen down if not already + if (!pen_is_down) { + commands.push_back("M3 S" + ofToString(pen_down_value)); + pen_is_down = true; + } + + // The move itself + if (seg.type == GSegment::Type::Line) { + commands.push_back("G1 X" + ofToString(e.x, 4) + " Y" + ofToString(e.y, 4)); + } else { + // I, J are the arc-centre offset from the current position + float I = c.x - s.x; + float J = c.y - s.y; + string cmd = (seg.type == GSegment::Type::ArcCW) ? "G2" : "G3"; + commands.push_back(cmd + + " X" + ofToString(e.x, 4) + + " Y" + ofToString(e.y, 4) + + " I" + ofToString(I, 4) + + " J" + ofToString(J, 4)); + } + + last_pos = e; + } + + commands.push_back("M3 S0"); + commands.push_back("G0 X0 Y0"); + + ofLogNotice("ofxGCode") << "save_arcs: " << commands.size() << " commands"; + + ofFile file; + file.open(name, ofFile::WriteOnly); + for (const string& cmd : commands) file << cmd << "\n"; + + ofLogNotice("ofxGCode") << "save_arcs: saved " << name; +} diff --git a/src/ofxGCode.hpp b/src/ofxGCode.hpp index 85c76ed..dcf6815 100644 --- a/src/ofxGCode.hpp +++ b/src/ofxGCode.hpp @@ -15,6 +15,34 @@ #include "GLine.hpp" #include "GCodeLineGroup.h" +// --------------------------------------------------------------------------- +// Biarc types +// --------------------------------------------------------------------------- + +/// A single circular arc produced by the biarc Bezier approximation. +/// If isLine() returns true the arc is degenerate and should be a G1 move. +struct GArc { + ofVec2f start; + ofVec2f end; + ofVec2f center; + float radius = 0.f; + bool clockwise = false; + + bool isLine() const { return radius < 0.001f; } +}; + +/// Unified move entry used by the arc-aware save pipeline. +/// Populated by line() and bezier_arc() / arc() calls; consumed by save_arcs(). +struct GSegment { + enum class Type { Line, ArcCW, ArcCCW }; + Type type = Type::Line; + ofVec2f start; + ofVec2f end; + ofVec2f center; ///< Arc centre in screen-space pixels (arcs only) +}; + +// --------------------------------------------------------------------------- + //ofxGCode is the core class of this library //an ofxGCode object represents a single g-code file (typically one pass on the plotter) //use multiple ofxGCode objects to create layered drawings (for multiple colors etc) @@ -38,6 +66,10 @@ class ofxGCode{ vector shape_pnts; //used for begin_shape / end_shape vector lines; //the collection of lines that make up this drawing + + /// Arc-aware move sequence. Populated by every line() call and by + /// bezier_arc() / arc() calls. Used by save_arcs() to emit G2/G3 commands. + vector segments; Clipping clip; //clipping mask to make sure we don't have lines out of bounds @@ -78,9 +110,20 @@ class ofxGCode{ //--- Saving - ///saves the file to the bin/data folder + ///saves the file to the bin/data folder (2D pen plotter format with M3 pen control) void save(string name); + ///saves 3-axis G-code to a file. Uses G0/G1 with X Y Z F (no pen servo commands). + ///safeZ is the height for rapid travel moves between cuts. + void save3D(string name, float safeZ = 5.0f); + + ///returns the G-code as a string (3-axis format) instead of writing to file + string toGCodeString(float safeZ = 5.0f); + + // Z height for each GLine (optional, for 3D toolpaths) + // When non-empty, lines[i] uses z_values[i] for its Z coordinate + vector z_values; + //--- Rectangles @@ -114,6 +157,10 @@ class ofxGCode{ static vector get_arc_pnts(ofVec2f center, float size, int steps, float start_angle, float end_angle, float height_scale = 1); + ///static function to get points along an arc defined by start, end, center (I,J,K style) + ///clockwise determines the arc direction. steps is the resolution. + static vector get_arc_points_ijk(ofVec2f start, ofVec2f end, ofVec2f center, bool clockwise, int steps = 32); + //--- Polygons @@ -161,6 +208,31 @@ class ofxGCode{ void bezier(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, int steps = 50); ///static function that returns a vector of the points that make up a bezier curve with the given values. steps defines the number of points that will be used in the line static vector get_bezier_pnts(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, int steps); + + /// Draws a cubic Bezier curve approximated by biarcs (pairs of circular arcs). + /// tolerance is the maximum allowed deviation in pixels between the true curve + /// and the arc approximation. Adds G2/G3-ready entries to the segments list; + /// also adds a linearised preview to the lines list for draw(). + /// Use save_arcs() to emit a file with G2/G3 arc commands. + void bezier_arc(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, float tolerance = 1.0f); + + /// Adds a single circular arc directly. + /// Like bezier_arc(), the arc is stored in segments for save_arcs() and + /// linearised into lines for draw(). + void arc(ofVec2f start, ofVec2f end, ofVec2f center, bool clockwise); + + /// Decomposes a cubic Bezier into a sequence of GArc segments whose deviation + /// from the true curve is at most tolerance (in user units / pixels). + /// max_depth limits recursion; 8 handles all practical curves. + static vector bezier_to_biarcs(ofVec2f p1, ofVec2f c1, ofVec2f c2, ofVec2f p2, + float tolerance = 1.0f, int max_depth = 8); + + //--- Arc-aware saving + + /// Like save(), but emits G2/G3 arc commands for any arcs added via + /// bezier_arc() or arc(). Straight lines (including those from line(), + /// polygon(), rect(), etc.) are still emitted as G1 moves. + void save_arcs(string name); //--- Dot