Skip to content

Commit

Permalink
Allow to define a custom bounding box for printer head movements
Browse files Browse the repository at this point in the history
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
  • Loading branch information
foosel committed Nov 9, 2016
1 parent 1c57769 commit 47a3e03
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 22 deletions.
82 changes: 79 additions & 3 deletions src/octoprint/printer/profile.py
Expand Up @@ -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``)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"]:
Expand All @@ -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"])
9 changes: 5 additions & 4 deletions src/octoprint/server/api/printer_profiles.py
Expand Up @@ -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()


Expand Down Expand Up @@ -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:
Expand Down
38 changes: 25 additions & 13 deletions src/octoprint/static/js/app/viewmodels/files.js
Expand Up @@ -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
Expand Down
99 changes: 98 additions & 1 deletion src/octoprint/static/js/app/viewmodels/printerprofiles.js
Expand Up @@ -10,7 +10,8 @@ $(function() {
width: 200,
depth: 200,
height: 200,
origin: "lowerleft"
origin: "lowerleft",
custom_box: false
},
heatedBed: true,
axes: {
Expand Down Expand Up @@ -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();

Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -268,6 +293,8 @@ $(function() {
}
};

self.fillBoundingBoxData(profile);

var offsetX, offsetY;
if (self.extruders() > 1) {
for (var i = 0; i < self.extruders() - 1; i++) {
Expand All @@ -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, "_");
};
Expand Down
Expand Up @@ -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>

0 comments on commit 47a3e03

Please sign in to comment.