Add selector tool

This commit is contained in:
Finn Krein 2021-05-20 16:04:10 +02:00
parent 729dbdbda1
commit 6bbb8c8d60
7 changed files with 339 additions and 36 deletions

View file

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

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,31 +25,144 @@
*/
(function hand() { //Code isolation
var selected = null;
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 inRect(x ,y , rect) {
return (x>=rect.x && x<=rect.x+rect.width) &&
(y>=rect.y && y>=rect.w+rect.height)
}
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 intersectRect(rect1 , rect2) {
return !(
(rect1.x+rect1.width<=rect2.x) ||
(rect2.x+rect2.width<=rect1.x) ||
(rect1.y+rect1.height<=rect2.y) ||
(rect2.y+rect2.height<=rect1.y)
)
}
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 };
var now = performance.now();
if (now - last_sent > 70) {
last_sent = now;
Tools.drawAndSend(msg);
} else {
draw(msg);
}
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 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", 3);
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 = {
type: "batch",
msgs: 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;
Tools.drawAndSend(msg);
} else {
draw(msg);
}
}
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
@ -70,21 +183,66 @@
return translate.matrix;
}
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;
default:
throw new Error("Mover: 'move' instruction with unknown type. ", data);
function draw(data) {
switch (data.type) {
case "batch":
for ([i,msg] of data.msgs.entries()) {
switch (msg.type) {
case "update":
let tmatrix = get_translate_matrix(Tools.svg.getElementById(msg.id));
tmatrix.e = msg.deltax || 0;
tmatrix.f = msg.deltay || 0;
break;
// Eventually also "delete"?
}
}
break;
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);
}
}
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);
}
}
function startHand(x, y, evt, isTouchEvent) {
if (!isTouchEvent) {
selected = {
@ -101,17 +259,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 +287,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

@ -109,6 +109,32 @@ class BoardData {
this.delaySave();
}
/** Process a batch of messages
* @param {envelope} array of messages to be delegated to the other methods
*/
batch(envelope) {
for (const message of envelope.msgs) {
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;
case "batch":
throw new Error("Nested batch message: ", message);
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

@ -171,6 +171,9 @@ async function saveHistory(boardName, message) {
case "child":
board.addChild(message.parent, message);
break;
case "batch":
board.batch(message);
break;
default:
//Add data
if (!id) throw new Error("Invalid message: ", message);