diff --git a/src/octoprint/printer/profile.py b/src/octoprint/printer/profile.py index e2c16a7628..304e0c98ad 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 @@ def _sanitize(self, name): 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 + + if "volume" in profile and not "custom_box" in profile["volume"]: + profile["volume"]["custom_box"] = False + modified = True - return False + return modified def _ensure_valid_profile(self, profile): # ensure all keys are present @@ -458,6 +487,35 @@ def convert_value(profile, path, converter): 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 @@ def convert_value(profile, path, converter): 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 b34ed19ce9..bc9f6f2c62 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 094763e4f9..b00d75f9c0 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 074d6db376..61ab6906be 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 b39966d2c7..763e3aeb2c 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!') }}