mirror of
https://github.com/lovasoa/whitebophir
synced 2024-11-10 06:24:17 +00:00
Add selector tool
This commit is contained in:
parent
729dbdbda1
commit
6bbb8c8d60
7 changed files with 339 additions and 36 deletions
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
92
client-data/js/intersect.js
Normal file
92
client-data/js/intersect.js
Normal 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]
|
||||
})();
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
|
|
19
client-data/tools/hand/selector.svg
Normal file
19
client-data/tools/hand/selector.svg
Normal 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 |
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue