Skip to content

Commit

Permalink
🥅 GCV: Work around for Chrome bug
Browse files Browse the repository at this point in the history
Chrome's canvas arc method seems to produce wrong results in case of
extremely large radii combined with extremely small enclosed angles. So
in case of Chrome we'll now default to a replacement function
approximating the arc with bezier curves. This can however be disabled
via the advanced options.

Additionally, another debug visualization option was added that shows
the arcs with their center points and segment borders.

Closes OctoPrint#4117
  • Loading branch information
foosel committed May 6, 2021
1 parent 0580b0f commit e1f8a82
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 18 deletions.
18 changes: 16 additions & 2 deletions src/octoprint/plugins/gcodeviewer/static/js/gcodeviewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ $(function () {
self.renderer_showRetracts = ko.observable(true);
self.renderer_showPrinthead = ko.observable(true);
self.renderer_showSegmentStarts = ko.observable(false);
self.renderer_showDebugArcs = ko.observable(false);
self.renderer_chromeArcFix = ko.observable(OctoPrint.coreui.browser.chrome);
self.renderer_showBoundingBox = ko.observable(false);
self.renderer_showLayerBoundingBox = ko.observable(false);
self.renderer_showFullSize = ko.observable(false);
Expand Down Expand Up @@ -97,6 +99,8 @@ $(function () {
showRetracts: self.renderer_showRetracts(),
showHead: self.renderer_showPrinthead(),
showSegmentStarts: self.renderer_showSegmentStarts(),
showDebugArcs: self.renderer_showDebugArcs(),
chromeArcFix: self.renderer_chromeArcFix(),
showBoundingBox: self.renderer_showBoundingBox(),
showLayerBoundingBox: self.renderer_showLayerBoundingBox(),
showFullSize: self.renderer_showFullSize(),
Expand Down Expand Up @@ -146,6 +150,8 @@ $(function () {
self.renderer_showRetracts.subscribe(self.rendererOptionUpdated);
self.renderer_showPrinthead.subscribe(self.rendererOptionUpdated);
self.renderer_showSegmentStarts.subscribe(self.rendererOptionUpdated);
self.renderer_showDebugArcs.subscribe(self.rendererOptionUpdated);
self.renderer_chromeArcFix.subscribe(self.rendererOptionUpdated);
self.renderer_showBoundingBox.subscribe(self.rendererOptionUpdated);
self.renderer_showLayerBoundingBox.subscribe(self.rendererOptionUpdated);
self.renderer_showFullSize.subscribe(self.rendererOptionUpdated);
Expand Down Expand Up @@ -401,6 +407,8 @@ $(function () {
self.renderer_showRetracts(true);
self.renderer_showPrinthead(true);
self.renderer_showSegmentStarts(false);
self.renderer_showDebugArcs(false);
self.renderer_chromeArcFix(OctoPrint.coreui.browser.chrome);
self.renderer_showBoundingBox(false);
self.renderer_showLayerBoundingBox(false);
self.renderer_showFullSize(false);
Expand Down Expand Up @@ -454,8 +462,8 @@ $(function () {
max: 1,
step: 1,
value: [0, 1],
enabled: false,
tooltip: "hide"
enabled: false
//tooltip: "hide"
})
.on("slide", self.changeCommandRange);
};
Expand Down Expand Up @@ -890,6 +898,8 @@ $(function () {
showRetracts: self.renderer_showRetracts(),
showPrinthead: self.renderer_showPrinthead(),
showSegmentStarts: self.renderer_showSegmentStarts(),
showDebugArcs: self.renderer_showDebugArcs(),
chromeArcFix: self.renderer_chromeArcFix(),
showPrevious: self.renderer_showPrevious(),
showCurrent: self.renderer_showCurrent(),
showNext: self.renderer_showNext(),
Expand All @@ -916,6 +926,10 @@ $(function () {
self.renderer_showPrinthead(current["showPrinthead"]);
if (current["showSegmentStarts"] !== undefined)
self.renderer_showSegmentStarts(current["showSegmentStarts"]);
if (current["showDebugArcs"] !== undefined)
self.renderer_showDebugArcs(current["showDebugArcs"]);
if (current["chromeArcFix"] !== undefined)
self.renderer_chromeArcFix(current["chromeArcFix"]);
if (current["showPrevious"] !== undefined)
self.renderer_showPrevious(current["showPrevious"]);
if (current["showCurrent"] !== undefined)
Expand Down
122 changes: 106 additions & 16 deletions src/octoprint/plugins/gcodeviewer/static/js/viewer/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ GCODE.renderer = (function () {
showHead: false,
showSegmentStarts: false,
sizeSegmentStart: 2 * pixelRatio,
showDebugArcs: false,
chromeArcFix: false,

moveModel: true,
zoomInOnModel: false,
Expand Down Expand Up @@ -339,12 +341,80 @@ GCODE.renderer = (function () {
};
}

// replace arc for chrome, code from https://stackoverflow.com/a/11689752
var bezierArc = function (x, y, radius, startAngle, endAngle, anticlockwise) {
// Signed length of curve
var signedLength;
var tau = 2 * Math.PI;

if (!anticlockwise && endAngle - startAngle >= tau) {
signedLength = tau;
} else if (anticlockwise && startAngle - endAngle >= tau) {
signedLength = -tau;
} else {
var delta = endAngle - startAngle;
signedLength = delta - tau * Math.floor(delta / tau);

// If very close to a full number of revolutions, make it full
if (Math.abs(delta) > 1e-12 && signedLength < 1e-12) signedLength = tau;

// Adjust if anti-clockwise
if (anticlockwise && signedLength > 0) signedLength = signedLength - tau;
}

// Minimum number of curves; 1 per quadrant.
var minCurves = Math.ceil(Math.abs(signedLength) / (Math.PI / 2));

// Number of curves; square-root of radius (or minimum)
var numCurves = Math.ceil(Math.max(minCurves, Math.sqrt(radius)));

// "Radius" of control points to ensure that the middle point
// of the curve is exactly on the circle radius.
var cpRadius = radius * (2 - Math.cos(signedLength / (numCurves * 2)));

// Angle step per curve
var step = signedLength / numCurves;

// Draw the circle
this.lineTo(x + radius * Math.cos(startAngle), y + radius * Math.sin(startAngle));
for (
var i = 0, a = startAngle + step, a2 = startAngle + step / 2;
i < numCurves;
++i, a += step, a2 += step
)
this.quadraticCurveTo(
x + cpRadius * Math.cos(a2),
y + cpRadius * Math.sin(a2),
x + radius * Math.cos(a),
y + radius * Math.sin(a)
);
};

var applyContextPatches = function () {
if (!ctx.origArc) ctx.origArc = ctx.arc;
ctx.circle = function (x, y, r) {
ctx.origArc(x, y, r, 0, 2 * Math.PI, true);
};

if (navigator.userAgent.toLowerCase().indexOf("chrome") > -1) {
if (renderOptions["chromeArcFix"]) {
ctx.arc = bezierArc;
log.info("Chrome Arc Fix enabled");
} else {
ctx.arc = ctx.origArc;
log.info("Chrome Arc Fix disabled");
}
}
};

var startCanvas = function () {
var jqueryCanvas = $(renderOptions["container"]);
//jqueryCanvas.css("background-color", renderOptions["bgColorOffGrid"]);
canvas = jqueryCanvas[0];

ctx = canvas.getContext("2d");
applyContextPatches();

canvas.style.height = canvas.height + "px";
canvas.style.width = canvas.width + "px";
canvas.height = canvas.height * pixelRatio;
Expand Down Expand Up @@ -540,7 +610,7 @@ GCODE.renderer = (function () {

// draw origin
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2, true);
ctx.circle(0, 0, 2);
ctx.stroke();

ctx.strokeStyle = renderOptions["colorGrid"];
Expand Down Expand Up @@ -584,7 +654,7 @@ GCODE.renderer = (function () {

// outline
var r = renderOptions["bed"]["r"];
ctx.arc(0, 0, r, 0, Math.PI * 2, true);
ctx.circle(0, 0, r);

// origin
ctx.moveTo(-1 * r, 0);
Expand All @@ -598,7 +668,7 @@ GCODE.renderer = (function () {

// draw origin
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2, true);
ctx.circle(0, 0, 2);
ctx.stroke();

ctx.strokeStyle = renderOptions["colorGrid"];
Expand Down Expand Up @@ -734,6 +804,17 @@ GCODE.renderer = (function () {
ctx.stroke();
};

var drawDebugArc = function (arc, ccw) {
ctx.moveTo(arc.x, arc.y);
ctx.lineTo(arc.startX, arc.startY);
ctx.moveTo(arc.x, arc.y);
ctx.lineTo(arc.endX, arc.endY);
ctx.moveTo(arc.startX, arc.startY);
ctx.lineTo(arc.endX, arc.endY);
ctx.moveTo(arc.startX, arc.startY);
ctx.arc(arc.x, arc.y, arc.r, arc.startAngle, arc.endAngle, ccw);
};

var drawLayer = function (layerNum, fromProgress, toProgress, isNotCurrentLayer) {
log.trace(
"Drawing layer " +
Expand Down Expand Up @@ -880,14 +961,14 @@ GCODE.renderer = (function () {

var prevPathType = "fill";
function strokePathIfNeeded(newPathType, strokeStyle) {
if (newPathType != prevPathType || newPathType == "fill") {
if (prevPathType != "fill") {
if (newPathType !== prevPathType || newPathType === "fill") {
if (prevPathType !== "fill") {
ctx.stroke();
}
prevPathType = newPathType;

ctx.beginPath();
if (newPathType != "fill") {
if (newPathType !== "fill") {
ctx.strokeStyle = strokeStyle;
ctx.moveTo(prevX, prevY);
}
Expand Down Expand Up @@ -948,14 +1029,15 @@ GCODE.renderer = (function () {
ctx.lineWidth = renderOptions["extrusionWidth"] * lineWidthFactor;
if (cmd.direction !== undefined && cmd.direction !== 0) {
var arc = getArcParams(cmd);
ctx.arc(
arc.x,
arc.y,
arc.r,
arc.startAngle,
arc.endAngle,
cmd.direction < 0
); // Y-axis is inverted so direction is also inverted
var ccw = cmd.direction < 0; // Y-axis is inverted so direction is also inverted

if (renderOptions["showDebugArcs"] && !isNotCurrentLayer) {
strokePathIfNeeded("debugarc", "#ff0000");
drawDebugArc(arc, ccw);
strokePathIfNeeded("extrude", getColorLineForTool(tool));
}

ctx.arc(arc.x, arc.y, arc.r, arc.startAngle, arc.endAngle, ccw);
} else {
ctx.lineTo(x, y);
}
Expand Down Expand Up @@ -995,7 +1077,7 @@ GCODE.renderer = (function () {
.alpha(alpha)
.html();
ctx.beginPath();
ctx.arc(prevX, prevY, sizeHeadSpot, 0, Math.PI * 2, true);
ctx.circle(prevX, prevY, sizeHeadSpot);
ctx.fill();
}
};
Expand Down Expand Up @@ -1169,6 +1251,7 @@ GCODE.renderer = (function () {
},
setOption: function (options) {
var mustRefresh = false;
var mustReapplyPatches = false;
var dirty = false;
for (var opt in options) {
if (!renderOptions.hasOwnProperty(opt) || !options.hasOwnProperty(opt))
Expand All @@ -1185,15 +1268,22 @@ GCODE.renderer = (function () {
"zoomInOnModel",
"bed",
"invertAxes",
"onViewportChange"
"onViewportChange",
"chromeArcFix"
]) > -1
) {
mustRefresh = true;
}
if ($.inArray(opt, ["chromeArcFix"]) > -1) {
mustReapplyPatches = true;
}
}

if (!dirty) return;
if (initialized) {
if (mustReapplyPatches) {
applyContextPatches();
}
if (mustRefresh) {
this.refresh();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showSegmentStarts">{{ _('Show segment starts') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showDebugArcs">{{ _('Show debug arcs') }}
</label>
<label class="checkbox" data-bind="visible: OctoPrint.coreui.browser.chrome">
<input type="checkbox" data-bind="checked: renderer_chromeArcFix">{{ _('Apply arc fix') }} <span class="label">{{ _('Chrome') }}</span>
<span class="help-block">{{ _("See <a href='%(url)s' target='_blank'>issue #4117</a>.", url="https://github.com/OctoPrint/OctoPrint/issues/4117") }}</span>
</label>
</p>
<p>
<label class="checkbox">
Expand Down

0 comments on commit e1f8a82

Please sign in to comment.