Merge pull request #193 from sents/selector

Replace mover tool by selector tool
This commit is contained in:
Ophir LOJKINE 2021-05-24 09:50:06 +02:00 committed by GitHub
commit 91273404f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 319 additions and 45 deletions

View file

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

View file

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

View file

@ -0,0 +1,92 @@
/**
* INTERSEC
*********************************************************
* @licstart The following is the entire license notice for the
* JavaScript code in this page.
*
* Copyright (C) 2021 Ophir LOJKINE
*
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend
*/
if (!SVGGraphicsElement.prototype.transformedBBox || !SVGGraphicsElement.prototype.transformedBBoxContains) {
[pointInTransformedBBox,
transformedBBoxIntersects] = (function () {
let applyTransform = function (m,t) {
return [
m.a*t[0]+m.c*t[1],
m.b*t[0]+m.d*t[1]
]
}
SVGGraphicsElement.prototype.transformedBBox = function (scale=1) {
bbox = this.getBBox();
tmatrix = this.getCTM();
return {
r: [bbox.x + tmatrix.e/scale, bbox.y + tmatrix.f/scale],
a: applyTransform(tmatrix,[bbox.width/scale,0]),
b: applyTransform(tmatrix,[0,bbox.height/scale])
}
}
SVGSVGElement.prototype.transformedBBox = function (scale=1) {
bbox = {
x: this.x.baseVal.value,
y: this.y.baseVal.value,
width: this.width.baseVal.value,
height: this.height.baseVal.value
};
tmatrix = this.getCTM();
return {
r: [bbox.x + tmatrix.e/scale, bbox.y + tmatrix.f/scale],
a: applyTransform(tmatrix,[bbox.width/scale,0]),
b: applyTransform(tmatrix,[0,bbox.height/scale])
}
}
let pointInTransformedBBox = function ([x,y],{r,a,b}) {
var d = [x-r[0],y-r[1]];
var idet = (a[0]*b[1]-a[1]*b[0]);
var c1 = (d[0]*b[1]-d[1]*b[0]) / idet;
var c2 = (d[1]*a[0]-d[0]*a[1]) / idet;
return (c1>=0 && c1<=1 && c2>=0 && c2<=1)
}
SVGGraphicsElement.prototype.transformedBBoxContains = function (x,y) {
return pointInTransformedBBox([x, y], this.transformedBBox())
}
function transformedBBoxIntersects(bbox_a,bbox_b) {
var corners = [
bbox_b.r,
[bbox_b.r[0] + bbox_b.a[0], bbox_b.r[1] + bbox_b.a[1]],
[bbox_b.r[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.b[1]],
[bbox_b.r[0] + bbox_b.a[0] + bbox_b.b[0], bbox_b.r[1] + bbox_b.a[1] + bbox_b.b[1]]
]
return corners.every(corner=>pointInTransformedBBox(corner,bbox_a))
}
SVGGraphicsElement.prototype.transformedBBoxIntersects= function (bbox) {
return transformedBBoxIntersects(this.transformedBBox(),bbox)
}
return [pointInTransformedBBox,
transformedBBoxIntersects]
})();
}

View file

@ -25,23 +25,115 @@
*/
(function hand() { //Code isolation
const selectorStates = {
pointing: 0,
selecting: 1,
moving: 2
}
var selected = null;
var selected_els = [];
var selectionRect = createSelectorRect();
var selectionRectTranslation;
var translation_elements = [];
var selectorState = selectorStates.pointing;
var last_sent = 0;
function startMovingElement(x, y, evt) {
//Prevent the press from being interpreted by the browser
evt.preventDefault();
if (!evt.target || !Tools.drawingArea.contains(evt.target)) return;
var tmatrix = get_translate_matrix(evt.target);
selected = { x: x - tmatrix.e, y: y - tmatrix.f, elem: evt.target };
function getParentMathematics(el) {
var target
var a = el
var els = [];
while (a) {
els.unshift(a);
a = a.parentElement;
}
var parentMathematics = els.find(el => el.getAttribute("class") === "MathElement");
if ((parentMathematics) && parentMathematics.tagName === "svg") {
target = parentMathematics;
}
return target ?? el;
}
function moveElement(x, y) {
if (!selected) return;
var deltax = x - selected.x;
var deltay = y - selected.y;
var msg = { type: "update", id: selected.elem.id, deltax: deltax, deltay: deltay };
function createSelectorRect() {
var shape = Tools.createSVGElement("rect");
shape.id = "selectionRect";
shape.x.baseVal.value = 0;
shape.y.baseVal.value = 0;
shape.width.baseVal.value = 0;
shape.height.baseVal.value = 0;
shape.setAttribute("stroke", "black");
shape.setAttribute("stroke-width", 1);
shape.setAttribute("vector-effect", "non-scaling-stroke");
shape.setAttribute("fill", "none");
shape.setAttribute("stroke-dasharray", "5 5");
shape.setAttribute("opacity", 1);
Tools.svg.appendChild(shape);
return shape;
}
function startMovingElements(x, y, evt) {
evt.preventDefault();
selectorState = selectorStates.moving;
selected = { x: x, y: y };
// Some of the selected elements could have been deleted
selected_els = selected_els.filter(el => {
return Tools.svg.getElementById(el.id) !== null
});
translation_elements = selected_els.map(el => {
let tmatrix = get_translate_matrix(el);
return { x: tmatrix.e, y: tmatrix.f }
});
{
let tmatrix = get_translate_matrix(selectionRect);
selectionRectTranslation = { x: tmatrix.e, y: tmatrix.f };
}
}
function startSelector(x, y, evt) {
evt.preventDefault();
selected = { x: x, y: y };
selected_els = [];
selectorState = selectorStates.selecting;
selectionRect.x.baseVal.value = x;
selectionRect.y.baseVal.value = y;
selectionRect.width.baseVal.value = 0;
selectionRect.height.baseVal.value = 0;
selectionRect.style.display = "";
tmatrix = get_translate_matrix(selectionRect);
tmatrix.e = 0;
tmatrix.f = 0;
}
function calculateSelection() {
var scale = Tools.drawingArea.getCTM().a;
var selectionTBBox = selectionRect.transformedBBox(scale);
return Array.from(Tools.drawingArea.children).filter(el => {
return transformedBBoxIntersects(
selectionTBBox,
el.transformedBBox(scale)
)
});
}
function moveSelection(x, y) {
var dx = x - selected.x;
var dy = y - selected.y;
var msgs = selected_els.map((el, i) => {
return {
type: "update",
id: el.id,
deltax: dx + translation_elements[i].x,
deltay: dy + translation_elements[i].y
}
})
var msg = {
_children: msgs
};
{
let tmatrix = get_translate_matrix(selectionRect);
tmatrix.e = dx + selectionRectTranslation.x;
tmatrix.f = dy + selectionRectTranslation.y;
}
var now = performance.now();
if (now - last_sent > 70) {
last_sent = now;
@ -51,6 +143,13 @@
}
}
function updateRect(x, y, rect) {
rect.x.baseVal.value = Math.min(x, selected.x);
rect.y.baseVal.value = Math.min(y, selected.y);
rect.width.baseVal.value = Math.abs(x - selected.x);
rect.height.baseVal.value = Math.abs(y - selected.y);
}
function get_translate_matrix(elem) {
// Returns the first translate or transform matrix or makes one
var translate = null;
@ -71,17 +170,54 @@
}
function draw(data) {
switch (data.type) {
case "update":
var elem = Tools.svg.getElementById(data.id);
if (!elem) throw new Error("Mover: Tried to move an element that does not exist.");
var tmatrix = get_translate_matrix(elem);
tmatrix.e = data.deltax || 0;
tmatrix.f = data.deltay || 0;
break;
if (data._children) {
batchCall(draw, data._children);
}
else {
switch (data.type) {
case "update":
var elem = Tools.svg.getElementById(data.id);
if (!elem) throw new Error("Mover: Tried to move an element that does not exist.");
var tmatrix = get_translate_matrix(elem);
tmatrix.e = data.deltax || 0;
tmatrix.f = data.deltay || 0;
break;
default:
throw new Error("Mover: 'move' instruction with unknown type. ", data);
}
}
}
default:
throw new Error("Mover: 'move' instruction with unknown type. ", data);
function clickSelector(x, y, evt) {
var scale = Tools.drawingArea.getCTM().a
selectionRect = selectionRect ?? createSelectorRect();
if (pointInTransformedBBox([x, y], selectionRect.transformedBBox(scale))) {
startMovingElements(x, y, evt);
} else if (Tools.drawingArea.contains(evt.target)) {
selectionRect.style.display = "none";
selected_els = [getParentMathematics(evt.target)];
startMovingElements(x, y, evt);
} else {
startSelector(x, y, evt);
}
}
function releaseSelector(x, y, evt) {
if (selectorState == selectorStates.selecting) {
selected_els = calculateSelection();
if (selected_els.length == 0) {
selectionRect.style.display = "none";
}
}
translation_elements = [];
selectorState = selectorStates.pointing;
}
function moveSelector(x, y, evt) {
if (selectorState == selectorStates.selecting) {
updateRect(x, y, selectionRect);
} else if (selectorState == selectorStates.moving) {
moveSelection(x, y, selectionRect);
}
}
@ -101,17 +237,18 @@
function press(x, y, evt, isTouchEvent) {
if (!handTool.secondary.active) startHand(x, y, evt, isTouchEvent);
else startMovingElement(x, y, evt, isTouchEvent);
else clickSelector(x, y, evt, isTouchEvent);
}
function move(x, y, evt, isTouchEvent) {
if (!handTool.secondary.active) moveHand(x, y, evt, isTouchEvent);
else moveElement(x, y, evt, isTouchEvent);
else moveSelector(x, y, evt, isTouchEvent);
}
function release(x, y, evt, isTouchEvent) {
move(x, y, evt, isTouchEvent);
if (handTool.secondary.active) releaseSelector(x, y, evt, isTouchEvent);
selected = null;
}
@ -128,8 +265,8 @@
"release": release,
},
"secondary": {
"name": "Mover",
"icon": "tools/hand/mover.svg",
"name": "Selector",
"icon": "tools/hand/selector.svg",
"active": false,
"switch": switchTool,
},

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<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#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<use transform="rotate(90 35 35)" href="#arrow"/>
<use transform="rotate(180 35 35)" href="#arrow"/>
<use transform="rotate(-90 35 35)" href="#arrow"/>
<g transform="rotate(-30 39.03 29.781)" stroke-width="11.584">
<path transform="matrix(1 0 0 1.3596 0 -.11858)" d="m48.33 29.711h-33.927l16.964-29.382z"/>
<rect x="27.761" y="38.278" width="7.2115" height="22.076"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 861 B

View file

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

View file

@ -109,6 +109,44 @@ class BoardData {
this.delaySave();
}
/** Process a batch of messages
* @typedef {{
* id:string,
* type: "delete" | "update" | "child",
* parent?: string,
* _children?: BoardMessage[],
* } & BoardElem } BoardMessage
* @param {BoardMessage[]} children array of messages to be delegated to the other methods
*/
processMessageBatch(children) {
for (const message of children) {
this.processMessage(message);
}
}
/** Process a single message
* @param {BoardMessage} message instruction to apply to the board
*/
processMessage(message) {
if (message._children) return this.processMessageBatch(message._children);
let id = message.id;
switch (message.type) {
case "delete":
if (id) this.delete(id);
break;
case "update":
if (id) this.update(id, message);
break;
case "child":
this.addChild(message.parent, message);
break;
default:
//Add data
if (!id) throw new Error("Invalid message: ", message);
this.set(id, message);
}
}
/** Reads data from the board
* @param {string} id - Identifier of the element to get.
* @returns {BoardElem} The element with the given id, or undefined if no element has this id

View file

@ -4,7 +4,7 @@ var iolib = require("socket.io"),
config = require("./configuration");
/** Map from name to *promises* of BoardData
@type {Object<string, Promise<BoardData>>}
@type {Object<string, Promise<BoardData>>}
*/
var boards = {};
@ -159,23 +159,11 @@ function handleMessage(boardName, message, socket) {
}
async function saveHistory(boardName, message) {
var id = message.id;
var board = await getBoard(boardName);
switch (message.type) {
case "delete":
if (id) board.delete(id);
break;
case "update":
if (id) board.update(id, message);
break;
case "child":
board.addChild(message.parent, message);
break;
default:
//Add data
if (!id) throw new Error("Invalid message: ", message);
board.set(id, message);
if (!message.tool && !message._children) {
console.error("Received a badly formatted message (no tool). ", message);
}
var board = await getBoard(boardName);
board.processMessage(message);
}
function generateUID(prefix, suffix) {