mirror of
https://github.com/Doodle3D/Doodle3D-Slicer.git
synced 2024-12-23 19:43:48 +01:00
added support for non closing parts
This commit is contained in:
parent
ce50b84010
commit
f8be250815
@ -25,16 +25,16 @@ canvas {border: 1px solid black;}
|
|||||||
<canvas id="canvas" width="400" height="400"></canvas>
|
<canvas id="canvas" width="400" height="400"></canvas>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var USER_SETTINGS, PRINTER_SETTINGS, doodleBox, gcode;
|
var USER_SETTINGS, PRINTER_SETTINGS, doodleBox, gcode, scene;
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
"use strict";
|
"use strict";
|
||||||
var scene = createScene();
|
scene = createScene();
|
||||||
|
|
||||||
var localIp = location.hash.substring(1);
|
var localIp = location.hash.substring(1);
|
||||||
//doodleBox = new D3D.Box(localIp);
|
doodleBox = new D3D.Box(localIp).init();
|
||||||
|
|
||||||
var printer = new D3D.Printer().updateConfig(USER_SETTINGS).updateConfig(PRINTER_SETTINGS["ultimaker"]);
|
var printer = new D3D.Printer().updateConfig(USER_SETTINGS).updateConfig(PRINTER_SETTINGS["ultimaker2go"]);
|
||||||
|
|
||||||
var loader = new THREE.STLLoader();
|
var loader = new THREE.STLLoader();
|
||||||
loader.load('models/pokemon/pikachu.stl', function (geometry) {
|
loader.load('models/pokemon/pikachu.stl', function (geometry) {
|
||||||
@ -66,11 +66,29 @@ function init () {
|
|||||||
return geometry;
|
return geometry;
|
||||||
})();
|
})();
|
||||||
*/
|
*/
|
||||||
var material = new THREE.MeshPhongMaterial({color: 0x00ff00, wireframe: false});
|
|
||||||
|
/*var path = [{x: 60, y: 40}, {x: 60, y: 50}, {x: 60, y: 60}, {x: 80, y: 60}, {x: 40, y: 40}, {x: 50, y: 40}, {x: 10, y: 60}];
|
||||||
|
var geometry = new THREE.Geometry();
|
||||||
|
|
||||||
|
for (var i = 0; i < path.length; i ++) {
|
||||||
|
var point = path[i];
|
||||||
|
|
||||||
|
geometry.vertices.push(new THREE.Vector3(point.x, 0, point.y));
|
||||||
|
geometry.vertices.push(new THREE.Vector3(point.x, 50, point.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < path.length - 1; i ++) {
|
||||||
|
var base = i * 2;
|
||||||
|
|
||||||
|
geometry.faces.push(new THREE.Face3(base, base + 1, base + 2));
|
||||||
|
geometry.faces.push(new THREE.Face3(base + 3, base + 2, base + 1));
|
||||||
|
}*/
|
||||||
|
|
||||||
|
var material = new THREE.MeshPhongMaterial({color: 0x00ff00, wireframe: false, side: THREE.DoubleSide});
|
||||||
var mesh = new THREE.Mesh(geometry, material);
|
var mesh = new THREE.Mesh(geometry, material);
|
||||||
|
|
||||||
mesh.rotation.x = -Math.PI/2;
|
mesh.rotation.x = -Math.PI/2;
|
||||||
mesh.scale.x = mesh.scale.y = mesh.scale.z = 1;
|
//mesh.scale.x = mesh.scale.y = mesh.scale.z = 1;
|
||||||
mesh.position.x = 60;
|
mesh.position.x = 60;
|
||||||
mesh.position.z = 60;
|
mesh.position.z = 60;
|
||||||
|
|
||||||
|
68
src/slice.js
68
src/slice.js
@ -26,7 +26,12 @@ D3D.Slice.prototype.optimizePaths = function (start) {
|
|||||||
|
|
||||||
for (var i = 0; i < this.parts.length; i ++) {
|
for (var i = 0; i < this.parts.length; i ++) {
|
||||||
var part = this.parts[i];
|
var part = this.parts[i];
|
||||||
var bounds = part.outerLine.bounds();
|
if (part.addFill) {
|
||||||
|
var bounds = part.outerLine.bounds();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var bounds = part.intersect.bounds();
|
||||||
|
}
|
||||||
|
|
||||||
var top = bounds.top - start.y;
|
var top = bounds.top - start.y;
|
||||||
var bottom = start.y - bounds.bottom;
|
var bottom = start.y - bounds.bottom;
|
||||||
@ -44,22 +49,28 @@ D3D.Slice.prototype.optimizePaths = function (start) {
|
|||||||
var part = this.parts.splice(closestPart, 1)[0];
|
var part = this.parts.splice(closestPart, 1)[0];
|
||||||
parts.push(part);
|
parts.push(part);
|
||||||
|
|
||||||
if (part.outerLine.length > 0) {
|
if (part.addFill) {
|
||||||
part.outerLine = part.outerLine.optimizePath(start);
|
if (part.outerLine.length > 0) {
|
||||||
start = part.outerLine.lastPoint();
|
part.outerLine = part.outerLine.optimizePath(start);
|
||||||
}
|
start = part.outerLine.lastPoint();
|
||||||
|
}
|
||||||
|
|
||||||
for (var j = 0; j < part.innerLines.length; j ++) {
|
for (var j = 0; j < part.innerLines.length; j ++) {
|
||||||
var innerLine = part.innerLines[j];
|
var innerLine = part.innerLines[j];
|
||||||
if (innerLine.length > 0) {
|
if (innerLine.length > 0) {
|
||||||
part.innerLines[j] = innerLine.optimizePath(start);
|
part.innerLines[j] = innerLine.optimizePath(start);
|
||||||
start = part.innerLines[j].lastPoint();
|
start = part.innerLines[j].lastPoint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.fill.length > 0) {
|
||||||
|
part.fill = part.fill.optimizePath(start);
|
||||||
|
start = part.fill.lastPoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
if (part.fill.length > 0) {
|
part.intersect.optimizePath(start);
|
||||||
part.fill = part.fill.optimizePath(start);
|
start = part.intersect.lastPoint();
|
||||||
start = part.fill.lastPoint();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -79,18 +90,31 @@ D3D.Slice.prototype.getOutline = function () {
|
|||||||
var outLines = new D3D.Paths([], true);
|
var outLines = new D3D.Paths([], true);
|
||||||
|
|
||||||
for (var i = 0; i < this.parts.length; i ++) {
|
for (var i = 0; i < this.parts.length; i ++) {
|
||||||
outLines.join(this.parts[i].outerLine);
|
var part = this.parts[i];
|
||||||
|
|
||||||
|
if (part.addFill) {
|
||||||
|
outLines.join(this.parts[i].outerLine);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return outLines;
|
return outLines;
|
||||||
};
|
};
|
||||||
D3D.Slice.prototype.addIntersect = function (intersect) {
|
D3D.Slice.prototype.add = function (intersect) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
this.parts.push({
|
if (intersect.closed) {
|
||||||
intersect: intersect,
|
this.parts.push({
|
||||||
innerLines: [],
|
intersect: intersect,
|
||||||
outerLine: new D3D.Paths([], true),
|
innerLines: [],
|
||||||
fill: new D3D.Paths([], false)
|
outerLine: new D3D.Paths([], true),
|
||||||
});
|
fill: new D3D.Paths([], false),
|
||||||
|
addFill: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.parts.push({
|
||||||
|
intersect: intersect,
|
||||||
|
addFill: false
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
218
src/slicer.js
218
src/slicer.js
@ -182,6 +182,8 @@ D3D.Slicer.prototype._slice = function (lines, printer) {
|
|||||||
var sliceParts = [];
|
var sliceParts = [];
|
||||||
for (var i = 0; i < layerIntersections.length; i ++) {
|
for (var i = 0; i < layerIntersections.length; i ++) {
|
||||||
var index = layerIntersections[i];
|
var index = layerIntersections[i];
|
||||||
|
var firstPoint = index;
|
||||||
|
var closed = false;
|
||||||
|
|
||||||
if (done.indexOf(index) === -1) {
|
if (done.indexOf(index) === -1) {
|
||||||
var shape = [];
|
var shape = [];
|
||||||
@ -195,32 +197,44 @@ D3D.Slicer.prototype._slice = function (lines, printer) {
|
|||||||
|
|
||||||
var connects = lines[index].connects.clone();
|
var connects = lines[index].connects.clone();
|
||||||
var faceNormals = lines[index].normals.clone();
|
var faceNormals = lines[index].normals.clone();
|
||||||
|
|
||||||
|
if (shape.length > 2 && connects.indexOf(firstPoint) !== -1) {
|
||||||
|
closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
for (var j = 0; j < connects.length; j ++) {
|
for (var j = 0; j < connects.length; j ++) {
|
||||||
index = connects[j];
|
index = connects[j];
|
||||||
|
|
||||||
if (intersections[index] !== undefined && done.indexOf(index) === -1) {
|
if (done.indexOf(index) === -1) {
|
||||||
|
if (intersections[index] !== undefined) {
|
||||||
|
|
||||||
var a = new THREE.Vector2(intersection.x, intersection.y);
|
var a = new THREE.Vector2(intersection.x, intersection.y);
|
||||||
var b = new THREE.Vector2(intersections[index].x, intersections[index].y);
|
var b = new THREE.Vector2(intersections[index].x, intersections[index].y);
|
||||||
|
|
||||||
var faceNormal = faceNormals[Math.floor(j/2)];
|
var faceNormal = faceNormals[Math.floor(j/2)];
|
||||||
|
|
||||||
if (a.distanceTo(b) < 0.0001 || faceNormal.length() === 0) {
|
if (a.distanceTo(b) < 0.0001 || faceNormal.length() === 0) {
|
||||||
done.push(index);
|
done.push(index);
|
||||||
|
|
||||||
connects = connects.concat(lines[index].connects);
|
connects = connects.concat(lines[index].connects);
|
||||||
faceNormals = faceNormals.concat(lines[index].normals);
|
faceNormals = faceNormals.concat(lines[index].normals);
|
||||||
index = -1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var normal = a.sub(b).normal().normalize();
|
|
||||||
|
|
||||||
if (normal.dot(faceNormal) > 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
index = -1;
|
index = -1;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
var normal = a.sub(b).normal().normalize();
|
||||||
|
|
||||||
|
if (normal.dot(faceNormal) > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
index = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
done.push(index);
|
||||||
|
index = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -229,7 +243,49 @@ D3D.Slicer.prototype._slice = function (lines, printer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var part = new D3D.Paths([shape]).clean(0.01);
|
if (!closed) {
|
||||||
|
var index = firstPoint;
|
||||||
|
|
||||||
|
while (index !== -1) {
|
||||||
|
if (index !== firstPoint) {
|
||||||
|
done.push(index);
|
||||||
|
|
||||||
|
var intersection = intersections[index];
|
||||||
|
console.log(intersection);
|
||||||
|
//uppercase X and Y because clipper vector
|
||||||
|
shape.unshift({X: intersection.x, Y: intersection.y});
|
||||||
|
}
|
||||||
|
|
||||||
|
var connects = lines[index].connects.clone();
|
||||||
|
|
||||||
|
for (var j = 0; j < connects.length; j ++) {
|
||||||
|
index = connects[j];
|
||||||
|
|
||||||
|
if (done.indexOf(index) === -1) {
|
||||||
|
if (intersections[index] !== undefined) {
|
||||||
|
if (a.distanceTo(b) < 0.0001) {
|
||||||
|
done.push(index);
|
||||||
|
|
||||||
|
connects = connects.concat(lines[index].connects);
|
||||||
|
index = -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
done.push(index);
|
||||||
|
index = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
index = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var part = new D3D.Paths([shape], closed).clean(0.01);
|
||||||
if (part.length > 0) {
|
if (part.length > 0) {
|
||||||
sliceParts.push(part);
|
sliceParts.push(part);
|
||||||
}
|
}
|
||||||
@ -240,19 +296,24 @@ D3D.Slicer.prototype._slice = function (lines, printer) {
|
|||||||
|
|
||||||
for (var i = 0; i < sliceParts.length; i ++) {
|
for (var i = 0; i < sliceParts.length; i ++) {
|
||||||
var slicePart1 = sliceParts[i];
|
var slicePart1 = sliceParts[i];
|
||||||
var merge = false;
|
if (slicePart1.closed) {
|
||||||
|
var merge = false;
|
||||||
|
|
||||||
for (var j = 0; j < slice.parts.length; j ++) {
|
for (var j = 0; j < slice.parts.length; j ++) {
|
||||||
var slicePart2 = slice.parts[j].intersect;
|
var slicePart2 = slice.parts[j].intersect;
|
||||||
|
|
||||||
if (slicePart2.intersect(slicePart1).length > 0) {
|
if (slicePart2.closed && slicePart2.intersect(slicePart1).length > 0) {
|
||||||
slicePart2.join(slicePart1);
|
slicePart2.join(slicePart1);
|
||||||
merge = true;
|
merge = true;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!merge) {
|
||||||
|
slice.add(slicePart1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!merge) {
|
else {
|
||||||
slice.addIntersect(slicePart1);
|
slice.add(slicePart1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,19 +345,21 @@ D3D.Slicer.prototype._generateInnerLines = function (slices, printer) {
|
|||||||
for (var i = 0; i < slice.parts.length; i ++) {
|
for (var i = 0; i < slice.parts.length; i ++) {
|
||||||
var part = slice.parts[i];
|
var part = slice.parts[i];
|
||||||
|
|
||||||
var outerLine = part.intersect.clone().scaleUp(scale).offset(-nozzleRadius);
|
if (part.addFill) {
|
||||||
|
var outerLine = part.intersect.clone().scaleUp(scale).offset(-nozzleRadius);
|
||||||
|
|
||||||
if (outerLine.length > 0) {
|
if (outerLine.length > 0) {
|
||||||
part.outerLine = outerLine;
|
part.outerLine = outerLine;
|
||||||
|
|
||||||
for (var offset = nozzleDiameter; offset <= shellThickness; offset += nozzleDiameter) {
|
for (var offset = nozzleDiameter; offset <= shellThickness; offset += nozzleDiameter) {
|
||||||
var innerLine = outerLine.offset(-offset);
|
var innerLine = outerLine.offset(-offset);
|
||||||
|
|
||||||
if (innerLine.length > 0) {
|
if (innerLine.length > 0) {
|
||||||
part.innerLines.push(innerLine);
|
part.innerLines.push(innerLine);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -340,40 +403,43 @@ D3D.Slicer.prototype._generateInfills = function (slices, printer) {
|
|||||||
|
|
||||||
for (var i = 0; i < slice.parts.length; i ++) {
|
for (var i = 0; i < slice.parts.length; i ++) {
|
||||||
var part = slice.parts[i];
|
var part = slice.parts[i];
|
||||||
var outerLine = part.outerLine;
|
|
||||||
|
|
||||||
if (outerLine.length > 0) {
|
if (part.addFill) {
|
||||||
var inset = ((part.innerLines.length > 0) ? part.innerLines[part.innerLines.length - 1] : outerLine);
|
var outerLine = part.outerLine;
|
||||||
|
|
||||||
var fillArea = inset.offset(-nozzleRadius);
|
if (outerLine.length > 0) {
|
||||||
if (surroundingLayer) {
|
var inset = ((part.innerLines.length > 0) ? part.innerLines[part.innerLines.length - 1] : outerLine);
|
||||||
if (infillOverlap === 0) {
|
|
||||||
var highFillArea = fillArea.difference(surroundingLayer).intersect(fillArea);
|
var fillArea = inset.offset(-nozzleRadius);
|
||||||
|
if (surroundingLayer) {
|
||||||
|
if (infillOverlap === 0) {
|
||||||
|
var highFillArea = fillArea.difference(surroundingLayer).intersect(fillArea);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var highFillArea = fillArea.difference(surroundingLayer).offset(infillOverlap).intersect(fillArea);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var highFillArea = fillArea.difference(surroundingLayer).offset(infillOverlap).intersect(fillArea);
|
var highFillArea = fillArea;
|
||||||
}
|
}
|
||||||
}
|
var lowFillArea = fillArea.difference(highFillArea);
|
||||||
else {
|
|
||||||
var highFillArea = fillArea;
|
|
||||||
}
|
|
||||||
var lowFillArea = fillArea.difference(highFillArea);
|
|
||||||
|
|
||||||
var fill = new D3D.Paths([], false);
|
var fill = new D3D.Paths([], false);
|
||||||
|
|
||||||
if (lowFillArea.length > 0) {
|
if (lowFillArea.length > 0) {
|
||||||
var bounds = lowFillArea.bounds();
|
var bounds = lowFillArea.bounds();
|
||||||
var lowFillTemplate = this._getFillTemplate(bounds, fillGridSize, true, true);
|
var lowFillTemplate = this._getFillTemplate(bounds, fillGridSize, true, true);
|
||||||
|
|
||||||
part.fill.join(lowFillTemplate.intersect(lowFillArea));
|
part.fill.join(lowFillTemplate.intersect(lowFillArea));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (highFillArea.length > 0) {
|
if (highFillArea.length > 0) {
|
||||||
var bounds = highFillArea.bounds();
|
var bounds = highFillArea.bounds();
|
||||||
var even = (layer % 2 === 0);
|
var even = (layer % 2 === 0);
|
||||||
var highFillTemplate = this._getFillTemplate(bounds, hightemplateSize, even, !even);
|
var highFillTemplate = this._getFillTemplate(bounds, hightemplateSize, even, !even);
|
||||||
|
|
||||||
part.fill.join(highFillTemplate.intersect(highFillArea));
|
part.fill.join(highFillTemplate.intersect(highFillArea));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -471,12 +537,14 @@ D3D.Slicer.prototype._optimizePaths = function (slices, printer) {
|
|||||||
for (var i = 0; i < slice.parts.length; i ++) {
|
for (var i = 0; i < slice.parts.length; i ++) {
|
||||||
var part = slice.parts[i];
|
var part = slice.parts[i];
|
||||||
|
|
||||||
part.outerLine.scaleDown(scale);
|
if (part.addFill) {
|
||||||
for (var j = 0; j < part.innerLines.length; j ++) {
|
part.outerLine.scaleDown(scale);
|
||||||
var innerLine = part.innerLines[j];
|
for (var j = 0; j < part.innerLines.length; j ++) {
|
||||||
innerLine.scaleDown(scale);
|
var innerLine = part.innerLines[j];
|
||||||
|
innerLine.scaleDown(scale);
|
||||||
|
}
|
||||||
|
part.fill.scaleDown(scale);
|
||||||
}
|
}
|
||||||
part.fill.scaleDown(scale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slice.support !== undefined) {
|
if (slice.support !== undefined) {
|
||||||
@ -572,14 +640,20 @@ D3D.Slicer.prototype._slicesToGCode = function (slices, printer) {
|
|||||||
for (var i = 0; i < slice.parts.length; i ++) {
|
for (var i = 0; i < slice.parts.length; i ++) {
|
||||||
var part = slice.parts[i];
|
var part = slice.parts[i];
|
||||||
|
|
||||||
pathToGCode(part.outerLine, false, true, "outerLine");
|
if (part.addFill) {
|
||||||
|
pathToGCode(part.outerLine, false, true, "outerLine");
|
||||||
|
|
||||||
for (var j = 0; j < part.innerLines.length; j ++) {
|
for (var j = 0; j < part.innerLines.length; j ++) {
|
||||||
var innerLine = part.innerLines[j];
|
var innerLine = part.innerLines[j];
|
||||||
pathToGCode(innerLine, false, false, "innerLine");
|
pathToGCode(innerLine, false, false, "innerLine");
|
||||||
|
}
|
||||||
|
|
||||||
|
pathToGCode(part.fill, true, false, "fill");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var retract = !(slice.parts.length === 1 && slice.support === undefined);
|
||||||
|
pathToGCode(part.intersect, retract, retract, "outerLine");
|
||||||
}
|
}
|
||||||
|
|
||||||
pathToGCode(part.fill, true, false, "fill");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slice.support !== undefined) {
|
if (slice.support !== undefined) {
|
||||||
|
@ -47,7 +47,7 @@ function init () {
|
|||||||
var scene = createScene();
|
var scene = createScene();
|
||||||
|
|
||||||
var localIp = location.hash.substring(1);
|
var localIp = location.hash.substring(1);
|
||||||
doodleBox = new D3D.Box(localIp).init();
|
/*doodleBox = new D3D.Box(localIp).init();
|
||||||
doodleBox.onupdate = function (data) {
|
doodleBox.onupdate = function (data) {
|
||||||
document.getElementById('state').innerHTML = data.state;
|
document.getElementById('state').innerHTML = data.state;
|
||||||
document.getElementById('bed_temp').innerHTML = data.bed;
|
document.getElementById('bed_temp').innerHTML = data.bed;
|
||||||
@ -58,7 +58,7 @@ function init () {
|
|||||||
document.getElementById('buffered_lines').innerHTML = data.buffered_lines;
|
document.getElementById('buffered_lines').innerHTML = data.buffered_lines;
|
||||||
document.getElementById('total_lines').innerHTML = data.total_lines;
|
document.getElementById('total_lines').innerHTML = data.total_lines;
|
||||||
document.getElementById('print_batches').innerHTML = doodleBox._printBatches.length;
|
document.getElementById('print_batches').innerHTML = doodleBox._printBatches.length;
|
||||||
};
|
};*/
|
||||||
|
|
||||||
printer = new D3D.Printer().updateConfig(USER_SETTINGS).updateConfig(PRINTER_SETTINGS['ultimaker2go']);
|
printer = new D3D.Printer().updateConfig(USER_SETTINGS).updateConfig(PRINTER_SETTINGS['ultimaker2go']);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user