mirror of
https://github.com/lovasoa/whitebophir
synced 2024-11-12 23:37:14 +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;
|
transition: 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#board #selectionRect {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Internet Explorer specific CSS */
|
/* Internet Explorer specific CSS */
|
||||||
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
|
@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 type="application/json" id="configuration">{{{ json configuration }}}</script>
|
||||||
<script src="../js/path-data-polyfill.js"></script>
|
<script src="../js/path-data-polyfill.js"></script>
|
||||||
<script src="../js/minitpl.js"></script>
|
<script src="../js/minitpl.js"></script>
|
||||||
|
<script src="../js/intersect.js"></script>
|
||||||
<script src="../js/board.js"></script>
|
<script src="../js/board.js"></script>
|
||||||
<script src="../tools/pencil/wbo_pencil_point.js"></script>
|
<script src="../tools/pencil/wbo_pencil_point.js"></script>
|
||||||
<script src="../tools/pencil/pencil.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
|
(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;
|
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) {
|
function intersectRect(rect1 , rect2) {
|
||||||
//Prevent the press from being interpreted by the browser
|
return !(
|
||||||
evt.preventDefault();
|
(rect1.x+rect1.width<=rect2.x) ||
|
||||||
if (!evt.target || !Tools.drawingArea.contains(evt.target)) return;
|
(rect2.x+rect2.width<=rect1.x) ||
|
||||||
var tmatrix = get_translate_matrix(evt.target);
|
(rect1.y+rect1.height<=rect2.y) ||
|
||||||
selected = { x: x - tmatrix.e, y: y - tmatrix.f, elem: evt.target };
|
(rect2.y+rect2.height<=rect1.y)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function moveElement(x, y) {
|
function getParentMathematics(el) {
|
||||||
if (!selected) return;
|
var target
|
||||||
var deltax = x - selected.x;
|
var a = el
|
||||||
var deltay = y - selected.y;
|
var els = [];
|
||||||
var msg = { type: "update", id: selected.elem.id, deltax: deltax, deltay: deltay };
|
while (a) {
|
||||||
var now = performance.now();
|
els.unshift(a);
|
||||||
if (now - last_sent > 70) {
|
a = a.parentElement;
|
||||||
last_sent = now;
|
|
||||||
Tools.drawAndSend(msg);
|
|
||||||
} else {
|
|
||||||
draw(msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
function get_translate_matrix(elem) {
|
||||||
// Returns the first translate or transform matrix or makes one
|
// Returns the first translate or transform matrix or makes one
|
||||||
|
@ -70,21 +183,66 @@
|
||||||
return translate.matrix;
|
return translate.matrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw(data) {
|
function draw(data) {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "update":
|
case "batch":
|
||||||
var elem = Tools.svg.getElementById(data.id);
|
for ([i,msg] of data.msgs.entries()) {
|
||||||
if (!elem) throw new Error("Mover: Tried to move an element that does not exist.");
|
switch (msg.type) {
|
||||||
var tmatrix = get_translate_matrix(elem);
|
case "update":
|
||||||
tmatrix.e = data.deltax || 0;
|
let tmatrix = get_translate_matrix(Tools.svg.getElementById(msg.id));
|
||||||
tmatrix.f = data.deltay || 0;
|
tmatrix.e = msg.deltax || 0;
|
||||||
break;
|
tmatrix.f = msg.deltay || 0;
|
||||||
|
break;
|
||||||
default:
|
// Eventually also "delete"?
|
||||||
throw new Error("Mover: 'move' instruction with unknown type. ", data);
|
}
|
||||||
|
}
|
||||||
|
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) {
|
function startHand(x, y, evt, isTouchEvent) {
|
||||||
if (!isTouchEvent) {
|
if (!isTouchEvent) {
|
||||||
selected = {
|
selected = {
|
||||||
|
@ -101,17 +259,18 @@
|
||||||
|
|
||||||
function press(x, y, evt, isTouchEvent) {
|
function press(x, y, evt, isTouchEvent) {
|
||||||
if (!handTool.secondary.active) startHand(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) {
|
function move(x, y, evt, isTouchEvent) {
|
||||||
if (!handTool.secondary.active) moveHand(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) {
|
function release(x, y, evt, isTouchEvent) {
|
||||||
move(x, y, evt, isTouchEvent);
|
move(x, y, evt, isTouchEvent);
|
||||||
|
if (handTool.secondary.active) releaseSelector(x, y, evt, isTouchEvent);
|
||||||
selected = null;
|
selected = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,8 +287,8 @@
|
||||||
"release": release,
|
"release": release,
|
||||||
},
|
},
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"name": "Mover",
|
"name": "Selector",
|
||||||
"icon": "tools/hand/mover.svg",
|
"icon": "tools/hand/selector.svg",
|
||||||
"active": false,
|
"active": false,
|
||||||
"switch": switchTool,
|
"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();
|
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
|
/** Reads data from the board
|
||||||
* @param {string} id - Identifier of the element to get.
|
* @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
|
* @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":
|
case "child":
|
||||||
board.addChild(message.parent, message);
|
board.addChild(message.parent, message);
|
||||||
break;
|
break;
|
||||||
|
case "batch":
|
||||||
|
board.batch(message);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
//Add data
|
//Add data
|
||||||
if (!id) throw new Error("Invalid message: ", message);
|
if (!id) throw new Error("Invalid message: ", message);
|
||||||
|
|
Loading…
Reference in a new issue