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
This commit is contained in:
Gina Häußge 2016-11-09 15:45:25 +01:00
parent 1c57769af3
commit 47a3e0340e
5 changed files with 263 additions and 22 deletions

View file

@ -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"])

View file

@ -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:

View file

@ -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

View file

@ -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, "_");
};

View file

@ -71,7 +71,62 @@
</div>
</div>
<div class="control-group">
<div class="controls">{% 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 %}</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Custom bounding box') }}</label>
<div class="controls">
<input type="checkbox" data-bind="checked: customBoundingBox">
</div>
</div>
<div data-bind="visible: customBoundingBox">
<div class="control-group">
<label class="control-label">{{ _('X Coordinates') }}</label>
<div class="controls">
<div class="input-prepend">
<span class="add-on">Min</span>
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: boundingBoxMinX, attr: {placeholder: boundingBoxMinXPlaceholder, max: boundingBoxMinXPlaceholder}">
</div>
<div class="input-prepend">
<span class="add-on">Max</span>
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: boundingBoxMaxX, attr: {placeholder: boundingBoxMaxXPlaceholder, min: boundingBoxMaxXPlaceholder}">
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Y Coordinates') }}</label>
<div class="controls">
<div class="input-prepend">
<span class="add-on">Min</span>
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: boundingBoxMinY, attr: {placeholder: boundingBoxMinYPlaceholder, max: boundingBoxMinYPlaceholder}">
</div>
<div class="input-prepend">
<span class="add-on">Max</span>
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: boundingBoxMaxY, attr: {placeholder: boundingBoxMaxYPlaceholder, min: boundingBoxMaxYPlaceholder}">
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Z Coordinates') }}</label>
<div class="controls">
<div class="input-prepend">
<span class="add-on">Min</span>
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: boundingBoxMinZ, attr: {placeholder: boundingBoxMinZPlaceholder, max: boundingBoxMinZPlaceholder}">
</div>
<div class="input-prepend">
<span class="add-on">Max</span>
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: boundingBoxMaxZ, attr: {placeholder: boundingBoxMaxZPlaceholder, min: boundingBoxMaxZPlaceholder}">
</div>
</div>
</div>
</div>
<p>
<small>{{ _('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!') }}</small>
<small>{{ _('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!') }}</small>
</p>
</form>