From 47a3e0340e3a170b7c8af53388c921d75470299e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 9 Nov 2016 15:45:25 +0100 Subject: [PATCH] Allow to define a custom bounding box for printer head movements That bounding box may have larger dimensions than the print volume (but not smaller ones). That allows to define safe areas for which no "exceeds print volume" messages need to be triggered. Solves #1551 --- src/octoprint/printer/profile.py | 82 ++++++++++++++- src/octoprint/server/api/printer_profiles.py | 9 +- .../static/js/app/viewmodels/files.js | 38 ++++--- .../js/app/viewmodels/printerprofiles.js | 99 ++++++++++++++++++- .../profileEditorBuildvolume.jinja2 | 57 ++++++++++- 5 files changed, 263 insertions(+), 22 deletions(-) diff --git a/src/octoprint/printer/profile.py b/src/octoprint/printer/profile.py index e2c16a76..304e0c98 100644 --- a/src/octoprint/printer/profile.py +++ b/src/octoprint/printer/profile.py @@ -88,6 +88,28 @@ class PrinterProfileManager(object): * - ``volume.origin`` - ``string`` - Location of gcode origin in the print volume, either ``lowerleft`` or ``center`` + * - ``volume.custom_box`` + - ``dict`` or ``False`` + - Custom boundary box overriding the default bounding box based on the provided width, depth, height and origin. + If ``False``, the default boundary box will be used. + * - ``volume.custom_box.x_min`` + - ``float`` + - Minimum valid X coordinate + * - ``volume.custom_box.y_min`` + - ``float`` + - Minimum valid Y coordinate + * - ``volume.custom_box.z_min`` + - ``float`` + - Minimum valid Z coordinate + * - ``volume.custom_box.x_max`` + - ``float`` + - Maximum valid X coordinate + * - ``volume.custom_box.y_max`` + - ``float`` + - Maximum valid Y coordinate + * - ``volume.custom_box.z_max`` + - ``float`` + - Maximum valid Z coordinate * - ``heatedBed`` - ``bool`` - Whether the printer has a heated bed (``True``) or not (``False``) @@ -154,7 +176,8 @@ class PrinterProfileManager(object): depth = 200, height = 200, formFactor = BedTypes.RECTANGULAR, - origin = BedOrigin.LOWERLEFT + origin = BedOrigin.LOWERLEFT, + custom_box = False ), heatedBed = True, extruder=dict( @@ -391,11 +414,17 @@ class PrinterProfileManager(object): def _migrate_profile(self, profile): # make sure profile format is up to date + modified = False + if "volume" in profile and "formFactor" in profile["volume"] and not "origin" in profile["volume"]: profile["volume"]["origin"] = BedOrigin.CENTER if profile["volume"]["formFactor"] == BedTypes.CIRCULAR else BedOrigin.LOWERLEFT - return True + modified = True - return False + if "volume" in profile and not "custom_box" in profile["volume"]: + profile["volume"]["custom_box"] = False + modified = True + + return modified def _ensure_valid_profile(self, profile): # ensure all keys are present @@ -458,6 +487,35 @@ class PrinterProfileManager(object): if profile["volume"]["formFactor"] == BedTypes.CIRCULAR: profile["volume"]["depth"] = profile["volume"]["width"] + # if we have a custom bounding box, validate it + if profile["volume"]["custom_box"] and isinstance(profile["volume"]["custom_box"], dict): + if not len(profile["volume"]["custom_box"]): + profile["volume"]["custom_box"] = False + + else: + default_box = self._default_box_for_volume(profile["volume"]) + for prop, limiter in (("x_min", min), ("y_min", min), ("z_min", min), + ("x_max", max), ("y_max", max), ("z_max", max)): + if prop not in profile["volume"]["custom_box"] or profile["volume"]["custom_box"][prop] is None: + profile["volume"]["custom_box"][prop] = default_box[prop] + else: + value = profile["volume"]["custom_box"][prop] + try: + value = limiter(float(value), default_box[prop]) + profile["volume"]["custom_box"][prop] = value + except: + self._logger.warn("Profile has invalid value in volume.custom_box.{}: {!r}".format(prop, value)) + return False + + # make sure we actually do have a custom box and not just the same values as the + # default box + for prop in profile["volume"]["custom_box"]: + if profile["volume"]["custom_box"][prop] != default_box[prop]: + break + else: + # exactly the same as the default box, remove custom box + profile["volume"]["custom_box"] = False + # validate offsets offsets = [] for offset in profile["extruder"]["offsets"]: @@ -474,3 +532,21 @@ class PrinterProfileManager(object): return profile + @staticmethod + def _default_box_for_volume(volume): + if volume["origin"] == BedOrigin.CENTER: + half_width = volume["width"] / 2.0 + half_depth = volume["depth"] / 2.0 + return dict(x_min=-half_width, + x_max=half_width, + y_min=-half_depth, + y_max=half_depth, + z_min=0.0, + z_max=volume["height"]) + else: + return dict(x_min=0.0, + x_max=volume["width"], + y_min=0.0, + y_max=volume["depth"], + z_min=0.0, + z_max=volume["height"]) diff --git a/src/octoprint/server/api/printer_profiles.py b/src/octoprint/server/api/printer_profiles.py index b34ed19c..bc9f6f2c 100644 --- a/src/octoprint/server/api/printer_profiles.py +++ b/src/octoprint/server/api/printer_profiles.py @@ -31,6 +31,7 @@ def _etag(lm=None): hash = hashlib.sha1() hash.update(str(lm)) hash.update(repr(printerProfileManager.get_default())) + hash.update(repr(printerProfileManager.get_current())) return hash.hexdigest() @@ -128,17 +129,17 @@ def printerProfilesUpdate(identifier): profile = printerProfileManager.get_default() new_profile = json_data["profile"] - new_profile = dict_merge(profile, new_profile) + merged_profile = dict_merge(profile, new_profile) make_default = False - if "default" in new_profile: + if "default" in merged_profile: make_default = True del new_profile["default"] - new_profile["id"] = identifier + merged_profile["id"] = identifier try: - saved_profile = printerProfileManager.save(new_profile, allow_overwrite=True, make_default=make_default) + saved_profile = printerProfileManager.save(merged_profile, allow_overwrite=True, make_default=make_default) except InvalidProfileError: return make_response("Profile is invalid", 400) except CouldNotOverwriteError: diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 094763e4..b00d75f9 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -647,19 +647,31 @@ $(function() { } // set print volume boundaries - var boundaries = { - minX : 0, - maxX : volumeInfo.width(), - minY : 0, - maxY : volumeInfo.depth(), - minZ : 0, - maxZ : volumeInfo.height() - }; - if (volumeInfo.origin() == "center") { - boundaries["maxX"] = volumeInfo.width() / 2; - boundaries["minX"] = -1 * boundaries["maxX"]; - boundaries["maxY"] = volumeInfo.depth() / 2; - boundaries["minY"] = -1 * boundaries["maxY"]; + var boundaries; + if (_.isPlainObject(volumeInfo.custom_box)) { + boundaries = { + minX : volumeInfo.custom_box.x_min(), + minY : volumeInfo.custom_box.y_min(), + minZ : volumeInfo.custom_box.z_min(), + maxX : volumeInfo.custom_box.x_max(), + maxY : volumeInfo.custom_box.y_max(), + maxZ : volumeInfo.custom_box.z_max() + } + } else { + boundaries = { + minX : 0, + maxX : volumeInfo.width(), + minY : 0, + maxY : volumeInfo.depth(), + minZ : 0, + maxZ : volumeInfo.height() + }; + if (volumeInfo.origin() == "center") { + boundaries["maxX"] = volumeInfo.width() / 2; + boundaries["minX"] = -1 * boundaries["maxX"]; + boundaries["maxY"] = volumeInfo.depth() / 2; + boundaries["minY"] = -1 * boundaries["maxY"]; + } } // model not within bounds, we need to prepare a warning diff --git a/src/octoprint/static/js/app/viewmodels/printerprofiles.js b/src/octoprint/static/js/app/viewmodels/printerprofiles.js index 074d6db3..61ab6906 100644 --- a/src/octoprint/static/js/app/viewmodels/printerprofiles.js +++ b/src/octoprint/static/js/app/viewmodels/printerprofiles.js @@ -10,7 +10,8 @@ $(function() { width: 200, depth: 200, height: 200, - origin: "lowerleft" + origin: "lowerleft", + custom_box: false }, heatedBed: true, axes: { @@ -53,6 +54,9 @@ $(function() { self.volumeOrigin("center"); } }); + self.volumeOrigin.subscribe(function() { + self.toBoundingBoxPlaceholders(self.defaultBoundingBox(self.volumeWidth(), self.volumeDepth(), self.volumeHeight(), self.volumeOrigin())); + }); self.heatedBed = ko.observable(); @@ -70,6 +74,20 @@ $(function() { self.axisZInverted = ko.observable(false); self.axisEInverted = ko.observable(false); + self.customBoundingBox = ko.observable(false); + self.boundingBoxMinX = ko.observable(); + self.boundingBoxMinY = ko.observable(); + self.boundingBoxMinZ = ko.observable(); + self.boundingBoxMaxX = ko.observable(); + self.boundingBoxMaxY = ko.observable(); + self.boundingBoxMaxZ = ko.observable(); + self.boundingBoxMinXPlaceholder = ko.observable(); + self.boundingBoxMinYPlaceholder = ko.observable(); + self.boundingBoxMinZPlaceholder = ko.observable(); + self.boundingBoxMaxXPlaceholder = ko.observable(); + self.boundingBoxMaxYPlaceholder = ko.observable(); + self.boundingBoxMaxZPlaceholder = ko.observable(); + self.koExtruderOffsets = ko.pureComputed(function() { var extruderOffsets = self.extruderOffsets(); var numExtruders = self.extruders(); @@ -181,6 +199,13 @@ $(function() { self.volumeFormFactor(data.volume.formFactor); self.volumeOrigin(data.volume.origin); + if (data.volume.custom_box) { + self.toBoundingBoxData(data.volume.custom_box, true); + } else { + var box = self.defaultBoundingBox(data.volume.width, data.volume.depth, data.volume.height, data.volume.origin); + self.toBoundingBoxData(box, false); + } + self.heatedBed(data.heatedBed); self.nozzleDiameter(data.extruder.nozzleDiameter); @@ -268,6 +293,8 @@ $(function() { } }; + self.fillBoundingBoxData(profile); + var offsetX, offsetY; if (self.extruders() > 1) { for (var i = 0; i < self.extruders() - 1; i++) { @@ -288,6 +315,76 @@ $(function() { return profile; }; + self.defaultBoundingBox = function(width, depth, height, origin) { + if (origin == "center") { + var halfWidth = width / 2.0; + var halfDepth = depth / 2.0; + + return { + x_min: -halfWidth, + y_min: -halfDepth, + z_min: 0.0, + x_max: halfWidth, + y_max: halfDepth, + z_max: height + } + } else { + return { + x_min: 0.0, + y_min: 0.0, + z_min: 0.0, + x_max: width, + y_max: depth, + z_max: height + } + } + }; + + self.toBoundingBoxData = function(box, custom) { + self.customBoundingBox(custom); + if (custom) { + self.boundingBoxMinX(box.x_min); + self.boundingBoxMinY(box.y_min); + self.boundingBoxMinZ(box.z_min); + self.boundingBoxMaxX(box.x_max); + self.boundingBoxMaxY(box.y_max); + self.boundingBoxMaxZ(box.z_max); + } else { + self.boundingBoxMinX(undefined); + self.boundingBoxMinY(undefined); + self.boundingBoxMinZ(undefined); + self.boundingBoxMaxX(undefined); + self.boundingBoxMaxY(undefined); + self.boundingBoxMaxZ(undefined); + } + self.toBoundingBoxPlaceholders(box); + }; + + self.toBoundingBoxPlaceholders = function(box) { + self.boundingBoxMinXPlaceholder(box.x_min); + self.boundingBoxMinYPlaceholder(box.y_min); + self.boundingBoxMinZPlaceholder(box.z_min); + self.boundingBoxMaxXPlaceholder(box.x_max); + self.boundingBoxMaxYPlaceholder(box.y_max); + self.boundingBoxMaxZPlaceholder(box.z_max); + }; + + self.fillBoundingBoxData = function(profile) { + if (self.customBoundingBox()) { + var defaultBox = self.defaultBoundingBox(self.volumeWidth(), self.volumeDepth(), self.volumeHeight(), self.volumeOrigin()); + profile.volume.custom_box = { + x_min: (self.boundingBoxMinX() !== undefined) ? Math.min(self.boundingBoxMinX(), defaultBox.x_min) : defaultBox.x_min, + y_min: (self.boundingBoxMinY() !== undefined) ? Math.min(self.boundingBoxMinY(), defaultBox.y_min) : defaultBox.y_min, + z_min: (self.boundingBoxMinZ() !== undefined) ? Math.min(self.boundingBoxMinZ(), defaultBox.z_min) : defaultBox.z_min, + x_max: (self.boundingBoxMaxX() !== undefined) ? Math.max(self.boundingBoxMaxX(), defaultBox.x_max) : defaultBox.x_max, + y_max: (self.boundingBoxMaxY() !== undefined) ? Math.max(self.boundingBoxMaxY(), defaultBox.y_max) : defaultBox.y_max, + z_max: (self.boundingBoxMaxZ() !== undefined) ? Math.max(self.boundingBoxMaxZ(), defaultBox.z_max) : defaultBox.z_max + }; + } else { + profile.volume.custom_box = false; + } + }; + self._sanitize = function(name) { return name.replace(/[^a-zA-Z0-9\-_\.\(\) ]/g, "").replace(/ /g, "_"); }; diff --git a/src/octoprint/templates/_snippets/settings/printerprofiles/profileEditorBuildvolume.jinja2 b/src/octoprint/templates/_snippets/settings/printerprofiles/profileEditorBuildvolume.jinja2 index b39966d2..763e3aeb 100644 --- a/src/octoprint/templates/_snippets/settings/printerprofiles/profileEditorBuildvolume.jinja2 +++ b/src/octoprint/templates/_snippets/settings/printerprofiles/profileEditorBuildvolume.jinja2 @@ -71,7 +71,62 @@ +
+
{% trans %} + If your printer's print head may move slightly outside the print volume (e.g. for nozzle cleaning routines) + you can define a custom safe bounding box for its movements below. + {% endtrans %}
+
+ +
+ +
+ +
+
+
+
+ +
+
+ Min + +
+
+ Max + +
+
+
+
+ +
+
+ Min + +
+
+ Max + +
+
+
+
+ +
+
+ Min + +
+
+ Max + +
+
+
+
+

- {{ _('This information is used for the GCODE Viewer and/or when slicing from OctoPrint. It does NOT influence already sliced files that you upload to OctoPrint!') }} + {{ _('This information is used for the temperature tab, the bounding box check, the GCODE Viewer and/or when slicing from OctoPrint. It does NOT influence already sliced files that you upload to OctoPrint!') }}