Bläddra i källkod

Merge pull request #193 from sents/selector

Replace mover tool by selector tool
dev_h
Ophir LOJKINE 4 år sedan
förälder
incheckning
91273404f7
Inget konto är kopplat till bidragsgivarens mejladress

+ 0
- 1
client-data/board.css Visa fil

@@ -275,7 +275,6 @@ circle.opcursor {
275 275
 	transition: 0s;
276 276
 }
277 277
 
278
-
279 278
 /* Internet Explorer specific CSS */
280 279
 @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
281 280
 	#chooseColor {

+ 1
- 0
client-data/board.html Visa fil

@@ -88,6 +88,7 @@
88 88
 	<script type="application/json" id="configuration">{{{ json configuration }}}</script>
89 89
 	<script src="../js/path-data-polyfill.js"></script>
90 90
 	<script src="../js/minitpl.js"></script>
91
+	<script src="../js/intersect.js"></script>
91 92
 	<script src="../js/board.js"></script>
92 93
 	<script src="../tools/pencil/wbo_pencil_point.js"></script>
93 94
 	<script src="../tools/pencil/pencil.js"></script>

+ 92
- 0
client-data/js/intersect.js Visa fil

@@ -0,0 +1,92 @@
1
+/**
2
+ *                        INTERSEC
3
+ *********************************************************
4
+ * @licstart  The following is the entire license notice for the 
5
+ *  JavaScript code in this page.
6
+ *
7
+ * Copyright (C) 2021  Ophir LOJKINE
8
+ *
9
+ *
10
+ * The JavaScript code in this page is free software: you can
11
+ * redistribute it and/or modify it under the terms of the GNU
12
+ * General Public License (GNU GPL) as published by the Free Software
13
+ * Foundation, either version 3 of the License, or (at your option)
14
+ * any later version.  The code is distributed WITHOUT ANY WARRANTY;
15
+ * without even the implied warranty of MERCHANTABILITY or FITNESS
16
+ * FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
17
+ *
18
+ * As additional permission under GNU GPL version 3 section 7, you
19
+ * may distribute non-source (e.g., minimized or compacted) forms of
20
+ * that code without the copy of the GNU GPL normally required by
21
+ * section 4, provided you include this license notice and a URL
22
+ * through which recipients can access the Corresponding Source.
23
+ *
24
+ * @licend
25
+ */
26
+
27
+if (!SVGGraphicsElement.prototype.transformedBBox || !SVGGraphicsElement.prototype.transformedBBoxContains) {
28
+    [pointInTransformedBBox,
29
+     transformedBBoxIntersects] = (function () {
30
+
31
+	let applyTransform = function (m,t) {
32
+	    return [
33
+		m.a*t[0]+m.c*t[1],
34
+		m.b*t[0]+m.d*t[1]
35
+	    ]
36
+	}
37
+
38
+	SVGGraphicsElement.prototype.transformedBBox = function (scale=1) {
39
+	    bbox = this.getBBox();
40
+	    tmatrix = this.getCTM();
41
+	    return {
42
+		r: [bbox.x + tmatrix.e/scale, bbox.y + tmatrix.f/scale],
43
+		a: applyTransform(tmatrix,[bbox.width/scale,0]),
44
+		b: applyTransform(tmatrix,[0,bbox.height/scale])
45
+	    }
46
+	}
47
+
48
+	SVGSVGElement.prototype.transformedBBox = function (scale=1) {
49
+	    bbox = {
50
+		x: this.x.baseVal.value,
51
+		y: this.y.baseVal.value,
52
+		width: this.width.baseVal.value,
53
+		height: this.height.baseVal.value
54
+	    };
55
+	    tmatrix = this.getCTM();
56
+	    return {
57
+		r: [bbox.x + tmatrix.e/scale, bbox.y + tmatrix.f/scale],
58
+		a: applyTransform(tmatrix,[bbox.width/scale,0]),
59
+		b: applyTransform(tmatrix,[0,bbox.height/scale])
60
+	    }
61
+	}
62
+
63
+	let pointInTransformedBBox = function ([x,y],{r,a,b}) {
64
+	    var d = [x-r[0],y-r[1]];
65
+	    var idet = (a[0]*b[1]-a[1]*b[0]);
66
+	    var c1 = (d[0]*b[1]-d[1]*b[0]) / idet;
67
+	    var c2 = (d[1]*a[0]-d[0]*a[1]) / idet;
68
+	    return (c1>=0 && c1<=1 && c2>=0 && c2<=1)
69
+	}
70
+
71
+	SVGGraphicsElement.prototype.transformedBBoxContains = function (x,y) {
72
+	    return pointInTransformedBBox([x, y], this.transformedBBox())
73
+	}
74
+
75
+	function transformedBBoxIntersects(bbox_a,bbox_b) {
76
+	    var corners = [
77
+		bbox_b.r,
78
+		[bbox_b.r[0] + bbox_b.a[0], bbox_b.r[1] + bbox_b.a[1]],
79
+		[bbox_b.r[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.b[1]],
80
+		[bbox_b.r[0] + bbox_b.a[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.a[1] + bbox_b.b[1]]
81
+	    ]
82
+	    return corners.every(corner=>pointInTransformedBBox(corner,bbox_a))
83
+	}
84
+
85
+	SVGGraphicsElement.prototype.transformedBBoxIntersects= function (bbox) {
86
+	    return transformedBBoxIntersects(this.transformedBBox(),bbox)
87
+	}
88
+
89
+	 return [pointInTransformedBBox,
90
+		 transformedBBoxIntersects]
91
+    })();
92
+}

+ 161
- 24
client-data/tools/hand/hand.js Visa fil

@@ -25,23 +25,115 @@
25 25
  */
26 26
 
27 27
 (function hand() { //Code isolation
28
+	const selectorStates = {
29
+		pointing: 0,
30
+		selecting: 1,
31
+		moving: 2
32
+	}
28 33
 	var selected = null;
34
+	var selected_els = [];
35
+	var selectionRect = createSelectorRect();
36
+	var selectionRectTranslation;
37
+	var translation_elements = [];
38
+	var selectorState = selectorStates.pointing;
29 39
 	var last_sent = 0;
30 40
 
41
+	function getParentMathematics(el) {
42
+		var target
43
+		var a = el
44
+		var els = [];
45
+		while (a) {
46
+			els.unshift(a);
47
+			a = a.parentElement;
48
+		}
49
+		var parentMathematics = els.find(el => el.getAttribute("class") === "MathElement");
50
+		if ((parentMathematics) && parentMathematics.tagName === "svg") {
51
+			target = parentMathematics;
52
+		}
53
+		return target ?? el;
54
+	}
55
+
56
+	function createSelectorRect() {
57
+		var shape = Tools.createSVGElement("rect");
58
+		shape.id = "selectionRect";
59
+		shape.x.baseVal.value = 0;
60
+		shape.y.baseVal.value = 0;
61
+		shape.width.baseVal.value = 0;
62
+		shape.height.baseVal.value = 0;
63
+		shape.setAttribute("stroke", "black");
64
+		shape.setAttribute("stroke-width", 1);
65
+		shape.setAttribute("vector-effect", "non-scaling-stroke");
66
+		shape.setAttribute("fill", "none");
67
+		shape.setAttribute("stroke-dasharray", "5 5");
68
+		shape.setAttribute("opacity", 1);
69
+		Tools.svg.appendChild(shape);
70
+		return shape;
71
+	}
31 72
 
32
-	function startMovingElement(x, y, evt) {
33
-		//Prevent the press from being interpreted by the browser
73
+	function startMovingElements(x, y, evt) {
34 74
 		evt.preventDefault();
35
-		if (!evt.target || !Tools.drawingArea.contains(evt.target)) return;
36
-		var tmatrix = get_translate_matrix(evt.target);
37
-		selected = { x: x - tmatrix.e, y: y - tmatrix.f, elem: evt.target };
75
+		selectorState = selectorStates.moving;
76
+		selected = { x: x, y: y };
77
+		// Some of the selected elements could have been deleted
78
+		selected_els = selected_els.filter(el => {
79
+			return Tools.svg.getElementById(el.id) !== null
80
+		});
81
+		translation_elements = selected_els.map(el => {
82
+			let tmatrix = get_translate_matrix(el);
83
+			return { x: tmatrix.e, y: tmatrix.f }
84
+		});
85
+		{
86
+			let tmatrix = get_translate_matrix(selectionRect);
87
+			selectionRectTranslation = { x: tmatrix.e, y: tmatrix.f };
88
+		}
38 89
 	}
39 90
 
40
-	function moveElement(x, y) {
41
-		if (!selected) return;
42
-		var deltax = x - selected.x;
43
-		var deltay = y - selected.y;
44
-		var msg = { type: "update", id: selected.elem.id, deltax: deltax, deltay: deltay };
91
+	function startSelector(x, y, evt) {
92
+		evt.preventDefault();
93
+		selected = { x: x, y: y };
94
+		selected_els = [];
95
+		selectorState = selectorStates.selecting;
96
+		selectionRect.x.baseVal.value = x;
97
+		selectionRect.y.baseVal.value = y;
98
+		selectionRect.width.baseVal.value = 0;
99
+		selectionRect.height.baseVal.value = 0;
100
+		selectionRect.style.display = "";
101
+		tmatrix = get_translate_matrix(selectionRect);
102
+		tmatrix.e = 0;
103
+		tmatrix.f = 0;
104
+	}
105
+
106
+
107
+	function calculateSelection() {
108
+		var scale = Tools.drawingArea.getCTM().a;
109
+		var selectionTBBox = selectionRect.transformedBBox(scale);
110
+		return Array.from(Tools.drawingArea.children).filter(el => {
111
+			return transformedBBoxIntersects(
112
+				selectionTBBox,
113
+				el.transformedBBox(scale)
114
+			)
115
+		});
116
+	}
117
+
118
+	function moveSelection(x, y) {
119
+		var dx = x - selected.x;
120
+		var dy = y - selected.y;
121
+		var msgs = selected_els.map((el, i) => {
122
+			return {
123
+				type: "update",
124
+				id: el.id,
125
+				deltax: dx + translation_elements[i].x,
126
+				deltay: dy + translation_elements[i].y
127
+			}
128
+		})
129
+		var msg = {
130
+			_children: msgs
131
+		};
132
+		{
133
+			let tmatrix = get_translate_matrix(selectionRect);
134
+			tmatrix.e = dx + selectionRectTranslation.x;
135
+			tmatrix.f = dy + selectionRectTranslation.y;
136
+		}
45 137
 		var now = performance.now();
46 138
 		if (now - last_sent > 70) {
47 139
 			last_sent = now;
@@ -51,6 +143,13 @@
51 143
 		}
52 144
 	}
53 145
 
146
+	function updateRect(x, y, rect) {
147
+		rect.x.baseVal.value = Math.min(x, selected.x);
148
+		rect.y.baseVal.value = Math.min(y, selected.y);
149
+		rect.width.baseVal.value = Math.abs(x - selected.x);
150
+		rect.height.baseVal.value = Math.abs(y - selected.y);
151
+	}
152
+
54 153
 	function get_translate_matrix(elem) {
55 154
 		// Returns the first translate or transform matrix or makes one
56 155
 		var translate = null;
@@ -71,17 +170,54 @@
71 170
 	}
72 171
 
73 172
 	function draw(data) {
74
-		switch (data.type) {
75
-			case "update":
76
-				var elem = Tools.svg.getElementById(data.id);
77
-				if (!elem) throw new Error("Mover: Tried to move an element that does not exist.");
78
-				var tmatrix = get_translate_matrix(elem);
79
-				tmatrix.e = data.deltax || 0;
80
-				tmatrix.f = data.deltay || 0;
81
-				break;
173
+		if (data._children) {
174
+			batchCall(draw, data._children);
175
+		}
176
+		else {
177
+			switch (data.type) {
178
+				case "update":
179
+					var elem = Tools.svg.getElementById(data.id);
180
+					if (!elem) throw new Error("Mover: Tried to move an element that does not exist.");
181
+					var tmatrix = get_translate_matrix(elem);
182
+					tmatrix.e = data.deltax || 0;
183
+					tmatrix.f = data.deltay || 0;
184
+					break;
185
+				default:
186
+					throw new Error("Mover: 'move' instruction with unknown type. ", data);
187
+			}
188
+		}
189
+	}
190
+
191
+	function clickSelector(x, y, evt) {
192
+		var scale = Tools.drawingArea.getCTM().a
193
+		selectionRect = selectionRect ?? createSelectorRect();
194
+		if (pointInTransformedBBox([x, y], selectionRect.transformedBBox(scale))) {
195
+			startMovingElements(x, y, evt);
196
+		} else if (Tools.drawingArea.contains(evt.target)) {
197
+			selectionRect.style.display = "none";
198
+			selected_els = [getParentMathematics(evt.target)];
199
+			startMovingElements(x, y, evt);
200
+		} else {
201
+			startSelector(x, y, evt);
202
+		}
203
+	}
204
+
205
+	function releaseSelector(x, y, evt) {
206
+		if (selectorState == selectorStates.selecting) {
207
+			selected_els = calculateSelection();
208
+			if (selected_els.length == 0) {
209
+				selectionRect.style.display = "none";
210
+			}
211
+		}
212
+		translation_elements = [];
213
+		selectorState = selectorStates.pointing;
214
+	}
82 215
 
83
-			default:
84
-				throw new Error("Mover: 'move' instruction with unknown type. ", data);
216
+	function moveSelector(x, y, evt) {
217
+		if (selectorState == selectorStates.selecting) {
218
+			updateRect(x, y, selectionRect);
219
+		} else if (selectorState == selectorStates.moving) {
220
+			moveSelection(x, y, selectionRect);
85 221
 		}
86 222
 	}
87 223
 
@@ -101,17 +237,18 @@
101 237
 
102 238
 	function press(x, y, evt, isTouchEvent) {
103 239
 		if (!handTool.secondary.active) startHand(x, y, evt, isTouchEvent);
104
-		else startMovingElement(x, y, evt, isTouchEvent);
240
+		else clickSelector(x, y, evt, isTouchEvent);
105 241
 	}
106 242
 
107 243
 
108 244
 	function move(x, y, evt, isTouchEvent) {
109 245
 		if (!handTool.secondary.active) moveHand(x, y, evt, isTouchEvent);
110
-		else moveElement(x, y, evt, isTouchEvent);
246
+		else moveSelector(x, y, evt, isTouchEvent);
111 247
 	}
112 248
 
113 249
 	function release(x, y, evt, isTouchEvent) {
114 250
 		move(x, y, evt, isTouchEvent);
251
+		if (handTool.secondary.active) releaseSelector(x, y, evt, isTouchEvent);
115 252
 		selected = null;
116 253
 	}
117 254
 
@@ -128,8 +265,8 @@
128 265
 			"release": release,
129 266
 		},
130 267
 		"secondary": {
131
-			"name": "Mover",
132
-			"icon": "tools/hand/mover.svg",
268
+			"name": "Selector",
269
+			"icon": "tools/hand/selector.svg",
133 270
 			"active": false,
134 271
 			"switch": switchTool,
135 272
 		},

+ 19
- 0
client-data/tools/hand/selector.svg Visa fil

@@ -0,0 +1,19 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<svg version="1.1" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
3
+ <metadata>
4
+  <rdf:RDF>
5
+   <cc:Work rdf:about="">
6
+    <dc:format>image/svg+xml</dc:format>
7
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
8
+    <dc:title/>
9
+   </cc:Work>
10
+  </rdf:RDF>
11
+ </metadata>
12
+ <use transform="rotate(90 35 35)" href="#arrow"/>
13
+ <use transform="rotate(180 35 35)" href="#arrow"/>
14
+ <use transform="rotate(-90 35 35)" href="#arrow"/>
15
+ <g transform="rotate(-30 39.03 29.781)" stroke-width="11.584">
16
+  <path transform="matrix(1 0 0 1.3596 0 -.11858)" d="m48.33 29.711h-33.927l16.964-29.382z"/>
17
+  <rect x="27.761" y="38.278" width="7.2115" height="22.076"/>
18
+ </g>
19
+</svg>

+ 1
- 1
package.json Visa fil

@@ -1,7 +1,7 @@
1 1
 {
2 2
   "name": "whitebophir",
3 3
   "description": "Online collaborative whiteboard",
4
-  "version": "1.10.2",
4
+  "version": "1.11.0",
5 5
   "keywords": [
6 6
     "collaborative",
7 7
     "whiteboard"

+ 38
- 0
server/boardData.js Visa fil

@@ -109,6 +109,44 @@ class BoardData {
109 109
     this.delaySave();
110 110
   }
111 111
 
112
+  /** Process a batch of messages
113
+   * @typedef {{
114
+   *  id:string,
115
+   *  type: "delete" | "update" | "child",
116
+   *  parent?: string,
117
+   *  _children?: BoardMessage[],
118
+   * } & BoardElem } BoardMessage
119
+   * @param {BoardMessage[]} children array of messages to be delegated to the other methods
120
+   */
121
+  processMessageBatch(children) {
122
+    for (const message of children) {
123
+      this.processMessage(message);
124
+    }
125
+  }
126
+
127
+  /** Process a single message
128
+   * @param {BoardMessage} message instruction to apply to the board
129
+   */
130
+  processMessage(message) {
131
+    if (message._children) return this.processMessageBatch(message._children);
132
+    let id = message.id;
133
+    switch (message.type) {
134
+      case "delete":
135
+        if (id) this.delete(id);
136
+        break;
137
+      case "update":
138
+        if (id) this.update(id, message);
139
+        break;
140
+      case "child":
141
+        this.addChild(message.parent, message);
142
+        break;
143
+      default:
144
+        //Add data
145
+        if (!id) throw new Error("Invalid message: ", message);
146
+        this.set(id, message);
147
+    }
148
+  }
149
+
112 150
   /** Reads data from the board
113 151
    * @param {string} id - Identifier of the element to get.
114 152
    * @returns {BoardElem} The element with the given id, or undefined if no element has this id

+ 5
- 17
server/sockets.js Visa fil

@@ -4,7 +4,7 @@ var iolib = require("socket.io"),
4 4
   config = require("./configuration");
5 5
 
6 6
 /** Map from name to *promises* of BoardData
7
-	@type {Object<string, Promise<BoardData>>}
7
+  @type {Object<string, Promise<BoardData>>}
8 8
 */
9 9
 var boards = {};
10 10
 
@@ -159,23 +159,11 @@ function handleMessage(boardName, message, socket) {
159 159
 }
160 160
 
161 161
 async function saveHistory(boardName, message) {
162
-  var id = message.id;
163
-  var board = await getBoard(boardName);
164
-  switch (message.type) {
165
-    case "delete":
166
-      if (id) board.delete(id);
167
-      break;
168
-    case "update":
169
-      if (id) board.update(id, message);
170
-      break;
171
-    case "child":
172
-      board.addChild(message.parent, message);
173
-      break;
174
-    default:
175
-      //Add data
176
-      if (!id) throw new Error("Invalid message: ", message);
177
-      board.set(id, message);
162
+  if (!message.tool && !message._children) {
163
+    console.error("Received a badly formatted message (no tool). ", message);
178 164
   }
165
+  var board = await getBoard(boardName);
166
+  board.processMessage(message);
179 167
 }
180 168
 
181 169
 function generateUID(prefix, suffix) {

Laddar…
Avbryt
Spara