mirror of
https://github.com/Doodle3D/doodle3d-client.git
synced 2025-01-06 02:23:49 +01:00
progress on printmanager
This commit is contained in:
parent
0c2f774376
commit
7f4965400c
@ -11,8 +11,15 @@ var API = function() {
|
||||
var _wifiboxURL = 'http://192.168.5.1/d3dapi/';
|
||||
var _wifiboxCGIBinURL = 'http://192.168.5.1/cgi-bin/d3dapi/';
|
||||
var _timeoutTime = 10000;
|
||||
var _isBusy = false;
|
||||
|
||||
function setURL(url,cgiUrl) {
|
||||
_wifiboxURL = url;
|
||||
_wifiboxCGIBinURL = cgiUrl || url;
|
||||
}
|
||||
|
||||
function post(cmd,data,success,fail) {
|
||||
_isBusy = true;
|
||||
$.ajax({
|
||||
url: _wifiboxURL + cmd,
|
||||
type: "POST",
|
||||
@ -20,6 +27,7 @@ var API = function() {
|
||||
dataType: 'json',
|
||||
timeout: _timeoutTime,
|
||||
success: function(response){
|
||||
_isBusy = false;
|
||||
if(response.status == "error" || response.status == "fail") {
|
||||
console.log('API.post fail',cmd)
|
||||
if (fail) fail(response);
|
||||
@ -29,18 +37,21 @@ var API = function() {
|
||||
}
|
||||
}
|
||||
}).fail(function(jqXHR, textStatus) {
|
||||
_isBusy = false;
|
||||
console.log('API.post fail',cmd,jqXHR,textStatus);
|
||||
if (fail) fail(jqXHR,textStatus);
|
||||
});
|
||||
}
|
||||
|
||||
function get(cmd,success,fail) {
|
||||
_isBusy = true;
|
||||
$.ajax({
|
||||
url: _wifiboxURL + cmd,
|
||||
type: "GET",
|
||||
dataType: 'json',
|
||||
timeout: _timeoutTime,
|
||||
success: function(response){
|
||||
success: function(response) {
|
||||
_isBusy = false;
|
||||
if (response.status == "error" || response.status == "fail") {
|
||||
console.log('API.get fail',cmd,response);
|
||||
if (fail) fail(response);
|
||||
@ -50,14 +61,21 @@ var API = function() {
|
||||
}
|
||||
}
|
||||
}).fail(function() {
|
||||
_isBusy = false;
|
||||
console.log('API.get fail',cmd);
|
||||
if (fail) fail();
|
||||
});
|
||||
}
|
||||
|
||||
function getBusy() {
|
||||
return _isBusy;
|
||||
}
|
||||
|
||||
return {
|
||||
get: get,
|
||||
post: post
|
||||
post: post,
|
||||
getBusy: getBusy,
|
||||
setURL: setURL,
|
||||
}
|
||||
|
||||
}();
|
||||
|
@ -7,7 +7,12 @@
|
||||
*/
|
||||
|
||||
function ConfigAPI() {
|
||||
|
||||
var className = 'ConfigAPI';
|
||||
|
||||
function init() {
|
||||
console.log(className,'init is deprecated');
|
||||
}
|
||||
|
||||
function loadAll(success,fail) {
|
||||
API.get('config/all',success,fail);
|
||||
};
|
||||
@ -73,6 +78,7 @@ function ConfigAPI() {
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
loadAll: loadAll,
|
||||
load: load,
|
||||
save: save,
|
||||
|
@ -7,10 +7,15 @@
|
||||
*/
|
||||
|
||||
function PrinterAPI() {
|
||||
var className = 'PrinterAPI';
|
||||
|
||||
this.remainingLines = [];
|
||||
this.totalLinesAtStart = 0;
|
||||
|
||||
this.init = function() {
|
||||
console.log(className,'init is deprecated');
|
||||
}
|
||||
|
||||
this.state = function(success,fail) {
|
||||
API.get('printer/state',success,fail);
|
||||
};
|
||||
@ -57,8 +62,8 @@ function PrinterAPI() {
|
||||
var data = {gcode: chunk.join("\n"), first: first, start: start};
|
||||
|
||||
_printPartPost(lines,data,function() {
|
||||
console.log('_printPartPost cb');
|
||||
cb(); //??? needed
|
||||
// console.log('_printPartPost cb');
|
||||
// cb(); //??? needed
|
||||
});
|
||||
|
||||
} else {
|
||||
|
@ -182,8 +182,8 @@
|
||||
<script src="js/libs/jquery-coolfieldset.min.js"></script>
|
||||
<script src="js/libs/FileSaver.min.js"></script>
|
||||
<script src="js/libs/jquery-fastclick.min.js"></script>
|
||||
<script src="js/doodle3d-api.min.js"></script>
|
||||
<script src="js/doodle3d-client.min.js"></script>
|
||||
<script src="js/doodle3d-api.js"></script>
|
||||
<script src="js/doodle3d-client.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,6 +1,7 @@
|
||||
body {
|
||||
font-family: Helvetica, Abel, Arial;
|
||||
font-family: Abel, 'sans-serif-thin', Sans-serif, Helvetica, Arial;
|
||||
font-size: 1em;
|
||||
margin: 0 0 0 0;
|
||||
|
||||
-webkit-user-select: none; /* webkit (safari, chrome) browsers */
|
||||
-moz-user-select: none; /* mozilla browsers */
|
||||
@ -12,13 +13,47 @@ button {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
float: left;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#btnPrint {
|
||||
font-size: 30px;
|
||||
/*width: 100px;*/
|
||||
/*height: 100px;*/
|
||||
}
|
||||
|
||||
#toolbar h1 {
|
||||
margin-top: 5px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#rightToolbar {
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#toolbar button {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#svgContainer {
|
||||
width: 540px;
|
||||
height: 540px;
|
||||
/*margin-top: 10px;*/
|
||||
background-color: #fff;
|
||||
width: 640px;
|
||||
height: 640px;
|
||||
border: 1px solid black;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
top: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
#svgContainer svg {
|
||||
@ -27,8 +62,8 @@ button {
|
||||
}
|
||||
|
||||
#printPreview {
|
||||
width: 540px;
|
||||
height: 540px;
|
||||
width: 640px;
|
||||
height: 640px;
|
||||
border: 1px solid black;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
@ -70,6 +105,7 @@ button {
|
||||
img#logo {
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#buttonsPanel {
|
||||
@ -79,7 +115,8 @@ img#logo {
|
||||
span.statusvar {
|
||||
background-color: #7cf;
|
||||
margin: 2px 2px 2px 2px;
|
||||
padding: 5px 5px 2px 5px;
|
||||
/*padding: 5px 5px 2px 5px;*/
|
||||
display: block;
|
||||
}
|
||||
|
||||
#preview {
|
||||
|
@ -3,45 +3,47 @@
|
||||
<head>
|
||||
<title>Doodle3D</title>
|
||||
<link href="css/style.css" rel="stylesheet" media="screen">
|
||||
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.3/css/base/jquery.ui.all.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/css/lightness/jquery-ui-1.10.2.custom.min.css" rel="stylesheet">
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.2/jquery.ui.touch-punch.min.js"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<img id="logo" src="../img/logo/doodle3d.png" height="25" alt="Doodle3D">PrintManager
|
||||
<button id="btnRefresh">Refresh</button>
|
||||
<!-- <button id="btnPlus">+</button>
|
||||
<button id="btnMin">-</button> -->
|
||||
<button id="btnPrepare">Prepare</button>
|
||||
<button id="btnPrint">Print</button>
|
||||
<button id="btnStop">Stop</button>
|
||||
<input type="file" name="file"/>
|
||||
<span class="statusvar" id="lblState"></span>
|
||||
<span class="statusvar" id="lblNozzle"></span>
|
||||
<span class="statusvar" id="lblBed"></span>
|
||||
<span class="statusvar" id="txtInfo"></span>
|
||||
<span class="statusvar" id="lblBufferProgress"></span>
|
||||
<span class="statusvar" id="lblPrintProgress"></span>
|
||||
<hr>
|
||||
<div id="toolbar">
|
||||
<img id="logo" src="../img/logo/doodle3d.png" alt="Doodle3D">
|
||||
<h1>Management App</h1>
|
||||
<hr>
|
||||
<button id="btnRefresh">Refresh</button>
|
||||
<button id="btnDownload">Download</button>
|
||||
<button id="btnPrint">Print</button>
|
||||
<button id="btnStop">Stop</button>
|
||||
<button id="btnExtrude">Extrude 10mm</button>
|
||||
<span class="statusvar" id="lblState"></span>
|
||||
<span class="statusvar" id="lblNozzle"></span>
|
||||
<span class="statusvar" id="lblBed"></span>
|
||||
<span class="statusvar" id="txtInfo"></span>
|
||||
<span class="statusvar" id="lblBufferProgress"></span>
|
||||
<span class="statusvar" id="lblPrintProgress"></span>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div id="svgContainer"></div>
|
||||
<div id="printPreview"></div>
|
||||
|
||||
<div id="rightToolbar">
|
||||
|
||||
</div>
|
||||
<!-- <canvas id="printPreview" id="canvas" width="400" height="400"></canvas> -->
|
||||
|
||||
<script src="../js/libs/jquery-1-9-1.min.js"></script>
|
||||
<script src="../js/doodle3d-api.js"></script>
|
||||
<script src="js/touchSwipe.min.js"></script>
|
||||
<script src="js/Point.js"></script>
|
||||
<script src="js/Rectangle.js"></script>
|
||||
<script src="js/Polyline.js"></script>
|
||||
<script src="js/Path.js"></script>
|
||||
<script src="js/Svg.js"></script>
|
||||
<script src="js/Doodle.js"></script>
|
||||
<script src="js/ViewerItem.js"></script>
|
||||
<script src="js/Viewer.js"></script>
|
||||
<script src="js/Doodle2gcode.js"></script>
|
||||
<script src="js/PrintPreview.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
</body>
|
||||
|
@ -1,32 +1,40 @@
|
||||
var Doodle = function(svgData,settings) {
|
||||
|
||||
var path = new Path();
|
||||
var height = 50; //in mm
|
||||
var height = 10; //in mm
|
||||
var offset = new Point(0,0);
|
||||
var scale = 1;
|
||||
var rotation = 0;
|
||||
var twist = 0;
|
||||
var twist = .1;
|
||||
|
||||
if (svgData!==undefined) {
|
||||
if (settings!=undefined) {
|
||||
if (settings.height!=undefined) height = settings.height;
|
||||
if (settings.twist!=undefined) twist = settings.twist;
|
||||
if (settings.scale!=undefined) scale = settings.scale;
|
||||
if (settings.offset!=undefined) offset = settings.offset;
|
||||
}
|
||||
|
||||
if (svgData!=undefined) {
|
||||
setFromSvgPathDescription(svgData);
|
||||
removeShortPaths(); //move this to main.js?
|
||||
|
||||
//TODO: determine current offset from boundingbox and store in 'offset' variable
|
||||
// offset.x += path.getBoundingBox().getX();
|
||||
// offset.y += path.getBoundingBox().getY();
|
||||
|
||||
path.alignCorner(); //set left-top corner of path boundingbox to 0,0
|
||||
|
||||
// scale = 1;
|
||||
|
||||
// console.log(path.getBoundingBox().getX(),offset);
|
||||
}
|
||||
|
||||
if (settings!==undefined) {
|
||||
if (settings.height!==undefined) height = settings.height;
|
||||
if (settings.twist!==undefined) twist = settings.twist;
|
||||
}
|
||||
|
||||
function setFromSvgPathDescription(svgData) {
|
||||
// if (!svgData) svgData = "";
|
||||
// else if (typeof(svgData)!='string') svgData = "";
|
||||
// // else if (svgData.indexOf("CDATA")==-1) svgData = "";
|
||||
// else svgData = svgData.split('d="')[1].split('"')[0];
|
||||
|
||||
console.log('svgData',svgData);
|
||||
// console.log('svgData',svgData);
|
||||
|
||||
svgData+=' '; //add a trailing space
|
||||
|
||||
|
@ -1,17 +1,23 @@
|
||||
var Doodle2gcode = function() {
|
||||
var className = "Doodle2gcode";
|
||||
|
||||
var speed = 50;
|
||||
var speed = 100 * 60; //mm/min
|
||||
var travelSpeed = 150 * 60 //mm/min
|
||||
var retractionSpeed = 45 * 60 //mm/min
|
||||
var retractionAmount = 4.5;
|
||||
var layerHeight = .2;
|
||||
var filamentDiameter = 2.89;
|
||||
var nozzleDiameter = .4;
|
||||
var dimensions = {x:200,y:200,z:200};
|
||||
var px2mm = .3;
|
||||
var flow = 1;
|
||||
var filamentArea = Math.PI * (filamentDiameter/2)*(filamentDiameter/2);
|
||||
var extrusionPerMM = layerHeight / filamentArea * flow;
|
||||
|
||||
var nozzleFilamentRatio = nozzleDiameter / filamentDiameter;
|
||||
var layerNozzleRatio = layerHeight / nozzleDiameter;
|
||||
var extrudeFactor = nozzleFilamentRatio * layerNozzleRatio;
|
||||
var flowRatio = 1;
|
||||
// var nozzleFilamentRatio = nozzleDiameter / filamentDiameter;
|
||||
// var layerNozzleRatio = layerHeight / nozzleDiameter;
|
||||
// var extrudeFactor = nozzleFilamentRatio * layerNozzleRatio;
|
||||
// var flowRatio = 1;
|
||||
|
||||
var extruder = 0;
|
||||
|
||||
@ -19,16 +25,13 @@ var Doodle2gcode = function() {
|
||||
var gcode = "";
|
||||
extruder = 0;
|
||||
for (var z=0,layer=0; z<dimensions.z; z+=layerHeight,layer++) {
|
||||
gcode += ';LAYER:' + layer + '\n';
|
||||
if (layer==0) gcode += 'M107\nM220 S50\n'; //fan off, print half speed
|
||||
else if (layer==1) gcode += 'M106 S255\nM220 S100\n' //fan on, print full speed
|
||||
for (var i=0; i<doodles.length; i++) {
|
||||
|
||||
var path = getDoodlePathAtHeight(doodles[i],z);
|
||||
|
||||
// var path = new Path();
|
||||
// path.moveTo(0,0);
|
||||
// path.lineTo(100,0);
|
||||
// path.lineTo(100,100);
|
||||
// path.lineTo(0,100);
|
||||
// path.lineTo(0,0);
|
||||
|
||||
// console.log(path.getBoundingBox().toString());
|
||||
|
||||
gcode += path2gcode(path,z);
|
||||
}
|
||||
@ -49,12 +52,12 @@ var Doodle2gcode = function() {
|
||||
// var org = path.getOffset();
|
||||
var box = path.getBoundingBox();
|
||||
|
||||
// console.log(box.toString());
|
||||
|
||||
|
||||
//center object on origin to apply transformations
|
||||
path.translate(-box.getX(),-box.getY());
|
||||
path.translate(-box.getWidth()/2,-box.getHeight()/2);
|
||||
|
||||
// path.alignCenter();
|
||||
path.scale(scale);
|
||||
path.scale(scaler);
|
||||
|
||||
@ -63,8 +66,11 @@ var Doodle2gcode = function() {
|
||||
|
||||
// path.rotate(rotation);
|
||||
path.rotate(twist,box.getCenter());
|
||||
// path.alignCorner();
|
||||
path.translate(offset.x,offset.y);
|
||||
|
||||
var scaledCenterX = box.getCenter().x * (1-(scale * scaler));
|
||||
var scaledCenterY = box.getCenter().y * (1-(scale * scaler));
|
||||
|
||||
path.translate(offset.x - scaledCenterX, offset.y - scaledCenterY);
|
||||
|
||||
return path;
|
||||
}
|
||||
@ -76,28 +82,41 @@ var Doodle2gcode = function() {
|
||||
path.scale(px2mm);
|
||||
path.translate(0,-dimensions.y);
|
||||
|
||||
// G1 X95.054 Y95.154 E4.83242
|
||||
// G1 F2400 E0.33242 ;retract voor travel
|
||||
// G0 F9000 X98.641 Y93.617
|
||||
// ;TYPE:WALL-INNER
|
||||
// G1 F2400 E4.83242 ;unretract na een travel en voor
|
||||
// G1 F1200 X98.621 Y93.596 E4.83297
|
||||
|
||||
|
||||
|
||||
for (var i=0; i<polylines.length; i++) {
|
||||
var points = polylines[i].getPoints();
|
||||
for (var j=0; j<points.length; j++) {
|
||||
var x = points[j].x;
|
||||
var y = -points[j].y;
|
||||
|
||||
gcode += (j==0 ? 'G0' : 'G1');
|
||||
gcode += ' ';
|
||||
gcode += 'X' + x.toFixed(2);
|
||||
gcode += ' ';
|
||||
gcode += 'Y' + y.toFixed(2);
|
||||
gcode += ' ';
|
||||
gcode += 'Z' + z.toFixed(2);
|
||||
gcode += ' ';
|
||||
//retract + travel + unretract
|
||||
if (j==0) {
|
||||
gcode += 'G0 F' + retractionSpeed + ' E' + (extruder-retractionAmount).toFixed(4) + '\n';
|
||||
gcode += 'G0 F' + travelSpeed + ' X' + x.toFixed(2) + ' Y' + y.toFixed(2) + ' Z' + z.toFixed(2) + '\n';
|
||||
gcode += 'G1 F' + retractionSpeed + ' E' + extruder.toFixed(4) + '\n';
|
||||
} else {
|
||||
gcode += 'G1 '; //gcode command
|
||||
if (j==1) gcode += 'F' + speed + ' '; //print speed
|
||||
gcode += 'X' + x.toFixed(2) + ' Y' + y.toFixed(2) + ' ';
|
||||
|
||||
if (j>0) {
|
||||
var dist = points[j-1].distance(points[j]) * px2mm;
|
||||
extruder += dist * extrudeFactor * flowRatio;
|
||||
gcode += 'E' + extruder.toFixed(4);
|
||||
//extrude
|
||||
if (j>0) {
|
||||
var dist = points[j-1].distance(points[j]) * px2mm;
|
||||
extruder += dist * extrusionPerMM;
|
||||
gcode += 'E' + extruder.toFixed(4);
|
||||
}
|
||||
|
||||
gcode += '\n';
|
||||
}
|
||||
|
||||
gcode += '\n';
|
||||
}
|
||||
}
|
||||
return gcode;
|
||||
|
374
www/printmanager/js/PrintPreview.js
Executable file
374
www/printmanager/js/PrintPreview.js
Executable file
@ -0,0 +1,374 @@
|
||||
var PrintPreview = function(jqCanvas) {
|
||||
var canvas = jqCanvas[0];
|
||||
|
||||
// === basic wheel reinvention stuff ===
|
||||
|
||||
// function $(id) { return document.getElementById(id) }
|
||||
|
||||
// comparison function using a key, to pass to .sort()
|
||||
function keycomp(key) {
|
||||
return function(a, b) {
|
||||
var ka = key(a)
|
||||
var kb = key(b)
|
||||
if (ka < kb) return -1
|
||||
if (ka > kb) return 1
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// return a list transformed by a function
|
||||
function map(f, list) {
|
||||
var rv = []
|
||||
for (var ii = 0; ii < list.length; ii++) rv.push(f(list[ii]))
|
||||
return rv
|
||||
}
|
||||
|
||||
// === 3d transforms ===
|
||||
|
||||
// We represent transforms as a 3x4 list of lists (ahem, array of arrays):
|
||||
// [[x_from_x, x_from_y, x_from_z, x_off],
|
||||
// [y_from_x, y_from_y, y_from_z, y_off],
|
||||
// [z_from_x, z_from_y, z_from_z, z_off]]
|
||||
// And we only actually multiply points through them in xform.
|
||||
function translate(x, y, z) {
|
||||
return [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z]]
|
||||
}
|
||||
function identity() { return translate(0, 0, 0) }
|
||||
// rotation around the Z-axis
|
||||
function rotate(theta) {
|
||||
var s = Math.sin(theta)
|
||||
var c = Math.cos(theta)
|
||||
return [[c, -s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0]]
|
||||
}
|
||||
// exchange two of the X, Y, Z axes --- useful for making rotate() go around
|
||||
// another axis :)
|
||||
function transpose_axes(a, b) {
|
||||
var rv = identity()
|
||||
var tmp = rv[a]
|
||||
rv[a] = rv[b]
|
||||
rv[b] = tmp
|
||||
return rv
|
||||
}
|
||||
// you'd think we'd have a scale() function too, but I haven't needed it yet.
|
||||
// concatenate two transforms --- the magic that makes it all possible
|
||||
function concat(x1, x2) {
|
||||
var rv = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
|
||||
for (var ii = 0; ii < 3; ii++) {
|
||||
rv[ii][3] = x2[ii][3]
|
||||
for (var jj = 0; jj < 3; jj++) {
|
||||
rv[ii][3] += x1[jj][3] * x2[ii][jj]
|
||||
for (var kk = 0; kk < 3; kk++) {
|
||||
rv[ii][jj] += x1[kk][jj] * x2[ii][kk]
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv
|
||||
}
|
||||
// concatenate N transforms. I'd insert a special case for 0 transforms,
|
||||
// but amusingly this function isn't all that performance-critical.
|
||||
function concat_n(xforms) {
|
||||
var rv = identity()
|
||||
for (var ii = 0; ii < xforms.length; ii++) rv = concat(rv, xforms[ii])
|
||||
return rv
|
||||
}
|
||||
// transform a point.
|
||||
function xform(xform, p) {
|
||||
var result_vec = []
|
||||
for (var ii = 0; ii < 3; ii++) {
|
||||
var rv = xform[ii][3]
|
||||
for (var jj = 0; jj < 3; jj++) rv += xform[ii][jj] * p[jj]
|
||||
result_vec.push(rv)
|
||||
}
|
||||
return result_vec
|
||||
}
|
||||
// transform multiple points.
|
||||
function xform_points(xf, points) {
|
||||
var xp = []
|
||||
for (var ii = 0; ii < points.length; ii++) {
|
||||
xp.push(xform(xf, points[ii]))
|
||||
}
|
||||
return xp
|
||||
}
|
||||
// perspective-transform a point --- into 2d.
|
||||
function persp(p) { return [p[0] / p[2], p[1] / p[2]] }
|
||||
// perspective-transform multiple points
|
||||
function persp_points(points) {
|
||||
return map(persp, points)
|
||||
}
|
||||
|
||||
// return the normal of a triangle defined by three points.
|
||||
function normal(p1, p2, p3) {
|
||||
var v1 = [p1[0]-p2[0], p1[1]-p2[1], p1[2]-p2[2]]
|
||||
var v2 = [p2[0]-p3[0], p2[1]-p3[1], p2[2]-p3[2]]
|
||||
var n = [v1[1]*v2[2]-v1[2]*v2[1],
|
||||
v1[2]*v2[0]-v1[0]*v2[2],
|
||||
v1[0]*v2[1]-v1[1]*v2[0]]
|
||||
var mag = Math.sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2])
|
||||
return [n[0]/mag, n[1]/mag, n[2]/mag]
|
||||
}
|
||||
|
||||
// === 3d shapes ===
|
||||
// We represent these as an array of three arrays
|
||||
// [points, lines, polies] where each line is two indices into the points array
|
||||
// and each poly is three indices into the points array
|
||||
|
||||
function dup(array) {
|
||||
var newarray = new Array(array.length)
|
||||
for (var ii = 0; ii < array.length; ii++) newarray[ii] = array[ii]
|
||||
return newarray
|
||||
}
|
||||
|
||||
// transform a shape, returning a new shape
|
||||
function xform_shape(xf, shape) {
|
||||
// de-alias new lines and polies
|
||||
return [xform_points(xf, shape[0]), dup(shape[1]), dup(shape[2])]
|
||||
}
|
||||
|
||||
// add a new shape onto an old shape, mutating the old one
|
||||
function augment(shape1, shape2) {
|
||||
var s1p = shape1[0]
|
||||
var off = s1p.length
|
||||
for (var ii = 0; ii < shape2[0].length; ii++) s1p.push(shape2[0][ii])
|
||||
var s2ll = shape2[1].length // in case of aliasing
|
||||
for (var ii = 0; ii < s2ll; ii++)
|
||||
shape1[1].push([shape2[1][ii][0] + off, shape2[1][ii][1] + off])
|
||||
var s2pl = shape2[2].length
|
||||
for (var ii = 0; ii < s2pl; ii++) {
|
||||
var tri = shape2[2][ii]
|
||||
shape1[2].push([tri[0]+off, tri[1]+off, tri[2]+off])
|
||||
}
|
||||
}
|
||||
|
||||
// given a shape, make a more complicated shape by copying it through transform
|
||||
// xf n times, and connecting the corresponding points. This is more powerful
|
||||
// than the usual kind of extrusion, and can be used to create fairly
|
||||
// interesting shapes --- a snail shell from a circle, for instance.
|
||||
function extrude_shape(xf, shape, n) {
|
||||
if (n == null) n = 1
|
||||
var new_part = shape
|
||||
var old_line_base = 0 // where the lines to attach the triangles start
|
||||
for (var ii = 0; ii < n; ii++) {
|
||||
var new_part = xform_shape(xf, new_part)
|
||||
var shape_length = shape[0].length
|
||||
var new_line_base = shape[1].length // for triangles later
|
||||
augment(shape, new_part)
|
||||
var new_part_length = new_part[0].length
|
||||
// connect corresponding points
|
||||
for (var jj = 0; jj < new_part_length; jj++) {
|
||||
shape[1].push([shape_length + jj - new_part_length, shape_length + jj])
|
||||
}
|
||||
// make triangles
|
||||
var nlines = new_part[1].length
|
||||
// var old_line_base = new_line_base - nlines
|
||||
for (var jj = 0; jj < nlines; jj++) {
|
||||
var old_line = shape[1][old_line_base + jj]
|
||||
var new_line = shape[1][new_line_base + jj]
|
||||
shape[2].push([old_line[0], old_line[1], new_line[0]])
|
||||
shape[2].push([new_line[1], new_line[0], old_line[1]])
|
||||
}
|
||||
old_line_base = new_line_base
|
||||
}
|
||||
}
|
||||
// a shape consisting of a single point
|
||||
function point_shape(x, y, z) { return [[[x, y, z]], [], []] }
|
||||
// approximate a circle in the x-y plane around the origin; radius r and n points
|
||||
function circle(r, n) {
|
||||
var shape = point_shape(r, 0, 0)
|
||||
extrude_shape(rotate(Math.atan(1)*8/n), shape, n)
|
||||
return shape
|
||||
}
|
||||
// approximate a torus with major radius r2 and minor radius r1,
|
||||
// with correspondingly n2 and n1 points around each axis
|
||||
function make_torus(r1, r2, n1, n2) {
|
||||
var c = xform_shape(translate(r2, 0, 0), circle(r1, n1))
|
||||
extrude_shape(concat_n([transpose_axes(1, 2),
|
||||
rotate(Math.atan(1)*8/n2),
|
||||
transpose_axes(1, 2)]),
|
||||
c, n2)
|
||||
return c
|
||||
}
|
||||
|
||||
// === drawing code ===
|
||||
|
||||
// draw a 3d shape on a canvas
|
||||
// 95% of the run time is in this function and its kids
|
||||
function draw_shape(canvas, xf, shape, alpha) {
|
||||
var ctx = canvas.getContext('2d')
|
||||
var w = canvas.width
|
||||
var h = canvas.height
|
||||
|
||||
// set up coordinate system so canvas is (-1, -1) to (1, 1)
|
||||
ctx.save()
|
||||
ctx.translate(w/2, h/2)
|
||||
ctx.scale(w/2, h/2)
|
||||
|
||||
// 1/3 of the time is in these two lines (when not doing polies)
|
||||
var points3d = xform_points(xf, shape[0])
|
||||
var points = persp_points(points3d)
|
||||
var lines = shape[1]
|
||||
// 2/3 of the time is in this loop (when we're not doing polies)
|
||||
if (alpha == null) {
|
||||
ctx.strokeStyle = 'grey'
|
||||
ctx.lineWidth = 1/(w/2)
|
||||
ctx.beginPath()
|
||||
var p1, p2
|
||||
for (var ii = 0; ii < lines.length; ii++) {
|
||||
p1 = points[lines[ii][0]]
|
||||
p2 = points[lines[ii][1]]
|
||||
ctx.moveTo(p1[0], p1[1])
|
||||
ctx.lineTo(p2[0], p2[1])
|
||||
}
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// when we're doing polies, 90% of our time is spent doing polies
|
||||
if (alpha != null) {
|
||||
// Sort polygons by depth so we draw the farthest-away stuff first
|
||||
// ("painter's algorithm")
|
||||
var minusdepth = function(p) {
|
||||
return [-(points3d[p[0]][2] + points3d[p[1]][2] + points3d[p[2]][2]), p]
|
||||
}
|
||||
var polies = map(minusdepth, shape[2])
|
||||
polies.sort(keycomp(function(p) { return p[0] }))
|
||||
|
||||
// draw all the polygons
|
||||
var tri, p1, p2, p3, n, bright
|
||||
for (var ii = 0; ii < polies.length; ii++) {
|
||||
tri = polies[ii][1]
|
||||
if (alpha == '1') {
|
||||
// light surface
|
||||
n = normal(points3d[tri[0]], points3d[tri[1]], points3d[tri[2]])
|
||||
// I'm not sure how to make backface removal work with perspective:
|
||||
// if (n[2] > 0 && alpha == '1') continue // backface removal
|
||||
|
||||
// lighting from (1, -1, -1) direction
|
||||
bright = parseInt(((n[0]-n[1]-n[2]) / Math.sqrt(3) * 255))
|
||||
if (bright < 20) bright = 20
|
||||
} else {
|
||||
// lighting doesn't make sense if the object is transparent,
|
||||
// so we color by depth to have some variation in color...
|
||||
var maxd = polies[polies.length-1][0]
|
||||
var mind = polies[0][0]
|
||||
var d = polies[ii][0]
|
||||
bright = parseInt((d-mind)/(maxd-mind) * 255)
|
||||
}
|
||||
ctx.fillStyle = 'rgba(' + bright + ',' + bright + ',' + bright + ',' + alpha + ')';
|
||||
ctx.beginPath()
|
||||
p1 = points[tri[0]]
|
||||
p2 = points[tri[1]]
|
||||
p3 = points[tri[2]]
|
||||
ctx.moveTo(p1[0], p1[1])
|
||||
ctx.lineTo(p2[0], p2[1])
|
||||
ctx.lineTo(p3[0], p3[1])
|
||||
// ctx.closePath() seems to be unnecessary
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
// clear a canvas
|
||||
function cls(canvas) {
|
||||
var ctx = canvas.getContext('2d')
|
||||
ctx.fillStyle = 'black'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
// // === drawing of particular shapes. also DOM. ===
|
||||
// angle = 0
|
||||
// function unit_cube() {
|
||||
// var shape = point_shape(0, 0, 0)
|
||||
// extrude_shape(translate(0,0,1), shape)
|
||||
// extrude_shape(translate(0,1,0), shape)
|
||||
// extrude_shape(translate(1,0,0), shape)
|
||||
// return shape
|
||||
// }
|
||||
|
||||
// // this was where I tested stuff as I wrote this
|
||||
// function make_some_junk() {
|
||||
// // make a unit cube centered on the origin
|
||||
// var shape = xform_shape(translate(-0.5, -0.5, -0.5), unit_cube())
|
||||
|
||||
// // add some circles
|
||||
// augment(shape, circle(0.707, 16))
|
||||
// augment(shape, xform_shape(transpose_axes(0, 2), circle(0.707, 16)))
|
||||
// augment(shape, xform_shape(transpose_axes(1, 2), circle(0.707, 16)))
|
||||
// augment(shape, circle(1, 15))
|
||||
|
||||
// // add a disc
|
||||
// var big_disc = circle(2, 20)
|
||||
// extrude_shape(translate(0, 0, 0.5), big_disc, 2)
|
||||
// augment(shape, big_disc)
|
||||
// return shape
|
||||
// }
|
||||
// var some_junk = make_some_junk()
|
||||
|
||||
// function draw_some_junk(canvas) {
|
||||
// var xf = concat_n([transpose_axes(1, 2),
|
||||
// rotate(angle),
|
||||
// transpose_axes(1, 2),
|
||||
// rotate(angle*1.618),
|
||||
// translate(0, 0, 2.5)])
|
||||
|
||||
// draw_shape(canvas, xf, some_junk)
|
||||
// }
|
||||
|
||||
var torus = make_torus(1, 3, 12, 12)
|
||||
|
||||
console.log('torus',torus);
|
||||
|
||||
function draw() {
|
||||
// var start = new Date()
|
||||
// var alpha = null
|
||||
// if ($('fill').checked) {
|
||||
// alpha = ($('translucent').checked ? '0.5' : 1)
|
||||
// }
|
||||
// if ($('trails').checked) {
|
||||
// $('canvas').getContext('2d').globalAlpha = 0.33
|
||||
// } else {
|
||||
// $('canvas').getContext('2d').globalAlpha = 1
|
||||
// }
|
||||
|
||||
var angle = 0;
|
||||
var alpha = 1;
|
||||
|
||||
draw_shape(canvas,
|
||||
concat_n([
|
||||
rotate(0),
|
||||
transpose_axes(1, 2),
|
||||
rotate(0), // to reduce periodicity
|
||||
transpose_axes(1, 2),
|
||||
translate(0, 0, 6),
|
||||
// transpose_axes(1, 2),
|
||||
// rotate(1),
|
||||
]),
|
||||
torus, alpha);
|
||||
|
||||
// var end = new Date()
|
||||
// var ms = $('ms')
|
||||
// if (ms) {
|
||||
// var msvalue = ms.value + ' ' + (end.getTime() - start.getTime())
|
||||
// if (msvalue.length > 25) msvalue = msvalue.substr(msvalue.length - 25)
|
||||
// ms.value = msvalue
|
||||
// }
|
||||
}
|
||||
|
||||
// function update() {
|
||||
// if (!$('go').checked) return
|
||||
// angle += 3.14159 / 30
|
||||
// cls($('canvas'))
|
||||
// draw_torus($('canvas'))
|
||||
// }
|
||||
// function init(ev) {
|
||||
// setInterval(update, 100)
|
||||
// // this doesn't work: $('fill').addEventListener('change', update, true)
|
||||
// // how do you do what I want to do there?
|
||||
// cls($('canvas'))
|
||||
// draw_torus($('canvas'))
|
||||
// }
|
||||
// window.addEventListener('load', init, true)
|
||||
|
||||
return {
|
||||
draw: draw,
|
||||
}
|
||||
}
|
@ -1,176 +1,14 @@
|
||||
var Viewer = function(viewer) {
|
||||
var className = "Viewer";
|
||||
var doodles = [];
|
||||
|
||||
//Object houd data bij van svg transformaties
|
||||
//(alle svg's worden automatisch hier in gezet, positie is relatief aan zijn html parent)
|
||||
// x -> x positie
|
||||
// y -> y positie
|
||||
// scale -> scale
|
||||
// svg -> svg object
|
||||
var svgsData = [];
|
||||
|
||||
console.log(className,viewer);
|
||||
var items = [];
|
||||
|
||||
function setDoodles(_doodles) {
|
||||
doodles = _doodles;
|
||||
console.log(className,'items',doodles);
|
||||
|
||||
for (var i=0; i<doodles.length; i++) {
|
||||
var doodle = doodles[i];
|
||||
var path = doodle.getPath();
|
||||
var svgData = doodle.getSvgPathDescription();
|
||||
var box = path.getBoundingBox();
|
||||
|
||||
var svg = $('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="640" height="540"><path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="black" stroke-width="2" d="'+svgData+'"></path></svg>');
|
||||
var item = new ViewerItem(doodles[i]);
|
||||
items.push(item);
|
||||
var svg = item.getSvg();
|
||||
viewer.append(svg);
|
||||
|
||||
var box = path.getBoundingBox();
|
||||
var viewbox = box.getX() + " " + box.getY() + " " + box.getWidth() + " " + box.getHeight();
|
||||
svg[0].setAttribute("viewBox", viewbox); //changig the viewBox with jQuery doesn't work (may be because of capital B)
|
||||
svg.attr('width',box.getWidth()+2);
|
||||
svg.attr('height',box.getHeight()+2);
|
||||
|
||||
initTouch(svg,doodle)
|
||||
}
|
||||
}
|
||||
|
||||
function initTouch(svg,doodle) {
|
||||
// $("svg").each(function () {
|
||||
// var svg = $(this);
|
||||
// var data = {
|
||||
// x: 0,
|
||||
// y: 0,
|
||||
// scale: 1,
|
||||
// };
|
||||
// svgsData.push(data);
|
||||
|
||||
var startX;
|
||||
var startY;
|
||||
var touchX;
|
||||
var touchY;
|
||||
var offsetX = 0;
|
||||
var offsetY = 0;
|
||||
var handleGesture = false;
|
||||
|
||||
var offsetZoom = 1;
|
||||
var zoom = 1;
|
||||
|
||||
var mouseDown = false;
|
||||
|
||||
svg.on("mousedown", function (e) {
|
||||
var event = e.originalEvent;
|
||||
mouseDown = true;
|
||||
|
||||
touchX = startX = event.pageX;
|
||||
touchY = startY = event.pageY;
|
||||
});
|
||||
|
||||
$(document).on("mousemove", function (e) {
|
||||
if (mouseDown) {
|
||||
var event = e.originalEvent;
|
||||
|
||||
touchX = event.pageX;
|
||||
touchY = event.pageY;
|
||||
|
||||
var dX = touchX - startX;
|
||||
var dY = touchY - startY;
|
||||
|
||||
svg.css({
|
||||
left: offsetX + dX,
|
||||
top: offsetY + dY
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
svg.on("mouseup", function (e) {
|
||||
var event = e.originalEvent;
|
||||
mouseDown = false;
|
||||
|
||||
offsetX = offsetX + touchX - startX;
|
||||
offsetY = offsetY + touchY - startY;
|
||||
|
||||
setData();
|
||||
});
|
||||
|
||||
svg.on("touchstart", function (e) {
|
||||
var event = e.originalEvent;
|
||||
event.preventDefault();
|
||||
|
||||
if (event.touches.length === 1) {
|
||||
var touch = event.touches[0];
|
||||
|
||||
touchX = startX = touch.pageX;
|
||||
touchY = startY = touch.pageY;
|
||||
}
|
||||
else {
|
||||
handleGesture = true;
|
||||
}
|
||||
});
|
||||
|
||||
svg.on("touchmove", function (e) {
|
||||
var event = e.originalEvent;
|
||||
event.preventDefault();
|
||||
|
||||
if (event.touches.length === 1 && !handleGesture) {
|
||||
var touch = event.touches[0];
|
||||
touchX = touch.pageX;
|
||||
touchY = touch.pageY;
|
||||
|
||||
var dX = touchX - startX;
|
||||
var dY = touchY - startY;
|
||||
|
||||
svg.css({
|
||||
left: offsetX + dX,
|
||||
top: offsetY + dY
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
svg.on("touchend", function (e) {
|
||||
var event = e.originalEvent;
|
||||
|
||||
if (event.touches.length === 0) {
|
||||
if (handleGesture) {
|
||||
handleGesture = false;
|
||||
}
|
||||
else {
|
||||
offsetX = offsetX + touchX - startX;
|
||||
offsetY = offsetY + touchY - startY;
|
||||
|
||||
setData();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
svg.swipe({
|
||||
pinchStatus: function (event, phase, direction, distance , duration , fingerCount, pinchZoom) {
|
||||
if (phase === "cancel" || phase === "end") {
|
||||
offsetZoom = offsetZoom*zoom;
|
||||
|
||||
setData();
|
||||
}
|
||||
else {
|
||||
zoom = pinchZoom;
|
||||
|
||||
svg.css({transform: "scale(" + offsetZoom*zoom + ")"});
|
||||
}
|
||||
},
|
||||
fingers: 2,
|
||||
pinchThreshold: 0
|
||||
});
|
||||
|
||||
function setData () {
|
||||
var offset = {
|
||||
x: offsetX - svg.width()*offsetZoom/2 + svg.width()/2,
|
||||
y: offsetY - svg.height()*offsetZoom/2 + svg.height()/2
|
||||
}
|
||||
doodle.setScale(offsetZoom);
|
||||
doodle.setOffset(offset);
|
||||
// data.scale = offsetZoom;
|
||||
// data.x = offsetX - svg.width()*offsetZoom/2 + svg.width()/2;
|
||||
// data.y = offsetY - svg.height()*offsetZoom/2 + svg.height()/2;
|
||||
// console.log(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
136
www/printmanager/js/ViewerItem.js
Normal file
136
www/printmanager/js/ViewerItem.js
Normal file
@ -0,0 +1,136 @@
|
||||
var ViewerItem = function(doodle) {
|
||||
|
||||
var path = doodle.getPath();
|
||||
var svgData = doodle.getSvgPathDescription();
|
||||
var box = path.getBoundingBox();
|
||||
var svg = $('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="640" height="540"><path xmlns="http://www.w3.org/2000/svg" fill="none" stroke="black" d="'+svgData+'"></path></svg>');
|
||||
var viewbox = box.getX() + " " + box.getY() + " " + box.getWidth() + " " + box.getHeight();
|
||||
var xDown = 0, yDown = 0;
|
||||
var xCur = 0, yCur = 0;
|
||||
var dragging = false;
|
||||
var prevDist = 0;
|
||||
|
||||
svg[0].setAttribute("viewBox", viewbox); //changig the viewBox with jQuery doesn't work (may be because of capital B)
|
||||
svg.attr('width',box.getWidth()+2);
|
||||
svg.attr('height',box.getHeight()+2);
|
||||
|
||||
updateView();
|
||||
|
||||
function updateView() {
|
||||
var box = path.getBoundingBox();
|
||||
var scaledCenterX = box.getCenter().x * (1-doodle.getScale());
|
||||
var scaledCenterY = box.getCenter().y * (1-doodle.getScale());
|
||||
svg[0].setAttribute("stroke-width", 1/doodle.getScale());
|
||||
svg.css({
|
||||
transform: "scale(" + doodle.getScale() + ")",
|
||||
left: doodle.getOffset().x - scaledCenterX + xCur - xDown,
|
||||
top: doodle.getOffset().y - scaledCenterY + yCur - yDown
|
||||
});
|
||||
}
|
||||
|
||||
function getDoodle() {
|
||||
return doodle;
|
||||
}
|
||||
|
||||
function getSvg() {
|
||||
return svg;
|
||||
}
|
||||
|
||||
function startDrag(x,y) {
|
||||
if (!dragging) {
|
||||
dragging = true;
|
||||
xDown = x;
|
||||
yDown = y;
|
||||
updateDrag(x,y);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDrag(x,y) {
|
||||
xCur = x;
|
||||
yCur = y;
|
||||
updateView();
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
doodle.getOffset().x += xCur - xDown;
|
||||
doodle.getOffset().y += yCur - yDown;
|
||||
xCur = 0;
|
||||
yCur = 0;
|
||||
xDown = 0;
|
||||
yDown = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function distance(x1,y1,x2,y2) {
|
||||
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||
}
|
||||
|
||||
function zoomBy(f) {
|
||||
var newScale = doodle.getScale()+f;
|
||||
var box = path.getBoundingBox();
|
||||
var anchorpoint = box.getCenter();
|
||||
if (newScale*box.getWidth()<100) return;
|
||||
if (newScale*box.getHeight()<100) return;
|
||||
doodle.setScale(newScale);
|
||||
doodle.getOffset().x -= f*anchorpoint.x;
|
||||
doodle.getOffset().y -= f*anchorpoint.y;
|
||||
updateView();
|
||||
}
|
||||
|
||||
svg.on("mousedown", function (e) {
|
||||
startDrag(e.originalEvent.pageX,e.originalEvent.pageY);
|
||||
});
|
||||
|
||||
$(document).on("mousemove", function (e) {
|
||||
if (dragging) {
|
||||
updateDrag(e.originalEvent.pageX,e.originalEvent.pageY);
|
||||
}
|
||||
});
|
||||
|
||||
svg.on("mouseup", function (e) {
|
||||
stopDrag();
|
||||
});
|
||||
|
||||
$(document).on("mouseup", function (e) {
|
||||
stopDrag();
|
||||
});
|
||||
|
||||
svg.on("touchstart", function (e) {
|
||||
var event = e.originalEvent;
|
||||
event.preventDefault();
|
||||
var touch = event.touches[0];
|
||||
startDrag(touch.pageX,touch.pageY); //drag
|
||||
if (event.touches.length === 2) { //zoom
|
||||
var touch2 = event.touches[1];
|
||||
prevDist = distance(touch.pageX,touch.pageY,touch2.pageX,touch2.pageY);
|
||||
}
|
||||
});
|
||||
|
||||
svg.on("touchmove", function (e) {
|
||||
var event = e.originalEvent;
|
||||
event.preventDefault();
|
||||
if (dragging) {
|
||||
var touch = event.touches[0];
|
||||
updateDrag(touch.pageX,touch.pageY);
|
||||
if (event.touches.length === 2) {
|
||||
var touch2 = event.touches[1];
|
||||
var dist = distance(touch.pageX,touch.pageY,touch2.pageX,touch2.pageY);
|
||||
var scaler = (dist-prevDist) / 100; //100 is an arbitrary scaler
|
||||
zoomBy(scaler);
|
||||
prevDist = dist;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
svg.on("touchend", function (e) {
|
||||
var event = e.originalEvent;
|
||||
stopDrag();
|
||||
});
|
||||
|
||||
return {
|
||||
getDoodle: getDoodle,
|
||||
getSvg: getSvg,
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user