Merge pull request #200 from sents/selector_buttons

Selector buttons
This commit is contained in:
Ophir LOJKINE 2021-06-05 02:02:24 +02:00 committed by GitHub
commit 2aee70e26d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 403 additions and 71 deletions

View file

@ -363,9 +363,9 @@ function messageForTool(message) {
else Tools.pendingMessages[name].push(message);
}
if (message.tool !== 'Hand' && message.deltax != null && message.deltay != null) {
if (message.tool !== 'Hand' && message.transform != null) {
//this message has special info for the mover
messageForTool({ tool: 'Hand', type: 'update', deltax: message.deltax || 0, deltay: message.deltay || 0, id: message.id });
messageForTool({ tool: 'Hand', type: 'update', transform: message.transform, id: message.id});
}
}
@ -685,8 +685,8 @@ Tools.svg.height.baseVal.value = document.body.clientHeight;
(function () {
let pos = {top: 0, scroll:0};
let menu = document.getElementById("menu");
var pos = {top: 0, scroll:0};
var menu = document.getElementById("menu");
function menu_mousedown(evt) {
pos = {
top: menu.scrollTop,
@ -696,7 +696,7 @@ Tools.svg.height.baseVal.value = document.body.clientHeight;
document.addEventListener("mouseup", menu_mouseup);
}
function menu_mousemove(evt) {
const dy = evt.clientY - pos.scroll;
var dy = evt.clientY - pos.scroll;
menu.scrollTop = pos.top - dy;
}
function menu_mouseup(evt) {

View file

@ -28,20 +28,48 @@ if (!SVGGraphicsElement.prototype.transformedBBox || !SVGGraphicsElement.prototy
[pointInTransformedBBox,
transformedBBoxIntersects] = (function () {
let applyTransform = function (m,t) {
var get_transform_matrix = function (elem) {
// Returns the first translate or transform matrix or makes one
var transform = null;
for (var i = 0; i < elem.transform.baseVal.numberOfItems; ++i) {
var baseVal = elem.transform.baseVal[i];
// quick tests showed that even if one changes only the fields e and f or uses createSVGTransformFromMatrix
// the brower may add a SVG_TRANSFORM_MATRIX instead of a SVG_TRANSFORM_TRANSLATE
if (baseVal.type === SVGTransform.SVG_TRANSFORM_MATRIX) {
transform = baseVal;
break;
}
}
if (transform == null) {
transform = elem.transform.baseVal.createSVGTransformFromMatrix(Tools.svg.createSVGMatrix());
elem.transform.baseVal.appendItem(transform);
}
return transform.matrix;
}
var transformRelative = function (m,t) {
return [
m.a*t[0]+m.c*t[1],
m.b*t[0]+m.d*t[1]
]
}
var transformAbsolute = function (m,t) {
return [
m.a*t[0]+m.c*t[1]+m.e,
m.b*t[0]+m.d*t[1]+m.f
]
}
SVGGraphicsElement.prototype.transformedBBox = function (scale=1) {
bbox = this.getBBox();
tmatrix = this.getCTM();
tmatrix = get_transform_matrix(this);
tmatrix.e /= scale;
tmatrix.f /= scale;
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])
r: transformAbsolute(tmatrix,[bbox.x/scale,bbox.y/scale]),
a: transformRelative(tmatrix,[bbox.width/scale,0]),
b: transformRelative(tmatrix,[0,bbox.height/scale])
}
}
@ -52,15 +80,17 @@ if (!SVGGraphicsElement.prototype.transformedBBox || !SVGGraphicsElement.prototy
width: this.width.baseVal.value,
height: this.height.baseVal.value
};
tmatrix = this.getCTM();
tmatrix = get_transform_matrix(this);
tmatrix.e /= scale;
tmatrix.f /= scale;
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])
r: transformAbsolute(tmatrix,[bbox.x/scale,bbox.y/scale]),
a: transformRelative(tmatrix,[bbox.width/scale,0]),
b: transformRelative(tmatrix,[0,bbox.height/scale])
}
}
let pointInTransformedBBox = function ([x,y],{r,a,b}) {
var 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;
@ -79,7 +109,9 @@ if (!SVGGraphicsElement.prototype.transformedBBox || !SVGGraphicsElement.prototy
[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))
return corners.every(function(corner) {
return pointInTransformedBBox(corner, bbox_a);
})
}
SVGGraphicsElement.prototype.transformedBBoxIntersects= function (bbox) {

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg role="img" version="1.1" viewBox="0 0 24 24" 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#" id="root">
<title>Delete</title>
<path stroke="red" stroke-width="2" d="M 2 2 L 22 22 M 2 22 L 22 2"></path>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg role="img" version="1.1" viewBox="0 0 24 24" 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#" id="root">
<title>Duplicate</title>
<path d="m7.1549 7.1542v-3.5315c0-0.65725 0.52912-1.1864 1.1864-1.1864h11.991c0.65725 0 1.1864 0.52912 1.1864 1.1864v12.036c0 0.65725-0.52912 1.1864-1.1864 1.1864h-3.4867" fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" />
<rect x="2.481" y="7.155" width="14.364" height="14.409" ry="1.1864" fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" />
<rect transform="translate(-20.734 -16.126) scale(1,-1)" x="30.056" y="-33.755" width=".6819" height="6.5387" ry=".34186" />
<rect transform="translate(-20.734 -16.126) matrix(0,1,1,0,0,0)" x="30.145" y="27.128" width=".6819" height="6.5387" ry=".34186" />
</svg>

After

Width:  |  Height:  |  Size: 969 B

View file

@ -25,32 +25,108 @@
*/
(function hand() { //Code isolation
const selectorStates = {
var selectorStates = {
pointing: 0,
selecting: 1,
moving: 2
transform: 2
}
var selected = null;
var selected_els = [];
var selectionRect = createSelectorRect();
var selectionRectTranslation;
var translation_elements = [];
var selectionRectTransform;
var currentTransform = null;
var transform_elements = [];
var selectorState = selectorStates.pointing;
var last_sent = 0;
var blockedSelectionButtons = Tools.server_config.BLOCKED_SELECTION_BUTTONS;
var selectionButtons = [
createButton("delete", "delete", 24, 24,
function (me, bbox, s) {
me.width.baseVal.value = me.origWidth / s;
me.height.baseVal.value = me.origHeight / s;
me.x.baseVal.value = bbox.r[0];
me.y.baseVal.value = bbox.r[1] - (me.origHeight + 3) / s;
me.style.display = "";
},
deleteSelection),
createButton("duplicate", "duplicate", 24, 24,
function (me, bbox, s) {
me.width.baseVal.value = me.origWidth / s;
me.height.baseVal.value = me.origHeight / s;
me.x.baseVal.value = bbox.r[0] + (me.origWidth + 2) / s;
me.y.baseVal.value = bbox.r[1] - (me.origHeight + 3) / s;
me.style.display = "";
},
duplicateSelection),
createButton("scaleHandle", "handle", 14, 14,
function (me, bbox, s) {
me.width.baseVal.value = me.origWidth / s;
me.height.baseVal.value = me.origHeight / s;
me.x.baseVal.value = bbox.r[0] + bbox.a[0] - me.origWidth / (2 * s);
me.y.baseVal.value = bbox.r[1] + bbox.b[1] - me.origHeight / (2 * s);
me.style.display = "";
},
startScalingTransform)
];
for (i in blockedSelectionButtons) {
delete selectionButtons[blockedSelectionButtons[i]];
}
var getScale = Tools.getScale;
function getParentMathematics(el) {
var target
var a = 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");
var parentMathematics = els.find(function (el) {
return el.getAttribute("class") === "MathElement";
});
if ((parentMathematics) && parentMathematics.tagName === "svg") {
target = parentMathematics;
}
return target ?? el;
return target || el;
}
function deleteSelection() {
var msgs = selected_els.map(function (el) {
return ({
"type": "delete",
"id": el.id
});
});
var data = {
_children: msgs
}
Tools.drawAndSend(data);
selected_els = [];
hideSelectionUI();
}
function duplicateSelection() {
if (!(selectorState == selectorStates.pointing)
|| (selected_els.length == 0)) return;
var msgs = [];
var newids = [];
for (var i = 0; i < selected_els.length; i++) {
var id = selected_els[i].id;
msgs[i] = {
type: "copy",
id: id,
newid: Tools.generateUID(id[0])
};
newids[i] = id;
}
Tools.drawAndSend({ _children: msgs });
selected_els = newids.map(function (id) {
return Tools.svg.getElementById(id);
});
}
function createSelectorRect() {
@ -70,22 +146,82 @@
return shape;
}
function createButton(name, icon, width, height, drawCallback, clickCallback) {
var shape = Tools.createSVGElement("use", {href: "tools/hand/" + icon + ".svg#root"});
shape.style.display = "none";
shape.origWidth = width;
shape.origHeight = height;
shape.drawCallback = drawCallback;
shape.clickCallback = clickCallback;
Tools.svg.appendChild(shape);
return shape;
}
function showSelectionButtons() {
var scale = getScale();
var selectionBBox = selectionRect.transformedBBox();
for (var i = 0; i < selectionButtons.length; i++) {
selectionButtons[i].drawCallback(selectionButtons[i],
selectionBBox,
scale);
}
}
function hideSelectionButtons() {
for (var i = 0; i < selectionButtons.length; i++) {
selectionButtons[i].style.display = "none";
}
}
function hideSelectionUI() {
hideSelectionButtons();
selectionRect.style.display = "none";
}
function startMovingElements(x, y, evt) {
evt.preventDefault();
selectorState = selectorStates.moving;
selectorState = selectorStates.transform;
currentTransform = moveSelection;
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
selected_els = selected_els.filter(function (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 }
transform_elements = selected_els.map(function (el) {
var tmatrix = get_transform_matrix(el);
return {
a: tmatrix.a, b: tmatrix.b, c: tmatrix.c,
d: tmatrix.d, e: tmatrix.e, f: tmatrix.f
};
});
{
let tmatrix = get_translate_matrix(selectionRect);
selectionRectTranslation = { x: tmatrix.e, y: tmatrix.f };
}
var tmatrix = get_transform_matrix(selectionRect);
selectionRectTransform = { x: tmatrix.e, y: tmatrix.f };
}
function startScalingTransform(x, y, evt) {
evt.preventDefault();
hideSelectionButtons();
selectorState = selectorStates.transform;
var bbox = selectionRect.transformedBBox();
selected = {
x: bbox.r[0],
y: bbox.r[1],
w: bbox.a[0],
h: bbox.b[1],
};
transform_elements = selected_els.map(function (el) {
var tmatrix = get_transform_matrix(el);
return {
a: tmatrix.a, b: tmatrix.b, c: tmatrix.c,
d: tmatrix.d, e: tmatrix.e, f: tmatrix.f
};
});
var tmatrix = get_transform_matrix(selectionRect);
selectionRectTransform = {
a: tmatrix.a, d: tmatrix.d,
e: tmatrix.e, f: tmatrix.f
};
currentTransform = scaleSelection;
}
function startSelector(x, y, evt) {
@ -98,42 +234,93 @@
selectionRect.width.baseVal.value = 0;
selectionRect.height.baseVal.value = 0;
selectionRect.style.display = "";
tmatrix = get_translate_matrix(selectionRect);
tmatrix = get_transform_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)
)
});
var selectionTBBox = selectionRect.transformedBBox();
var elements = Tools.drawingArea.children;
var selected = [];
for (var i = 0; i < elements.length; i++) {
if (transformedBBoxIntersects(selectionTBBox, elements[i].transformedBBox()))
selected.push(Tools.drawingArea.children[i]);
}
return selected;
}
function moveSelection(x, y) {
var dx = x - selected.x;
var dy = y - selected.y;
var msgs = selected_els.map((el, i) => {
var msgs = selected_els.map(function (el, i) {
var oldTransform = transform_elements[i];
return {
type: "update",
id: el.id,
deltax: dx + translation_elements[i].x,
deltay: dy + translation_elements[i].y
}
transform: {
a: oldTransform.a,
b: oldTransform.b,
c: oldTransform.c,
d: oldTransform.d,
e: dx + oldTransform.e,
f: dy + oldTransform.f
}
};
})
var msg = {
_children: msgs
};
{
let tmatrix = get_translate_matrix(selectionRect);
tmatrix.e = dx + selectionRectTranslation.x;
tmatrix.f = dy + selectionRectTranslation.y;
var tmatrix = get_transform_matrix(selectionRect);
tmatrix.e = dx + selectionRectTransform.x;
tmatrix.f = dy + selectionRectTransform.y;
var now = performance.now();
if (now - last_sent > 70) {
last_sent = now;
Tools.drawAndSend(msg);
} else {
draw(msg);
}
}
function scaleSelection(x, y) {
var rx = (x - selected.x) / (selected.w);
var ry = (y - selected.y) / (selected.h);
var msgs = selected_els.map(function (el, i) {
var oldTransform = transform_elements[i];
var x = el.transformedBBox().r[0];
var y = el.transformedBBox().r[1];
var a = oldTransform.a * rx;
var d = oldTransform.d * ry;
var e = selected.x * (1 - rx) - x * a +
(x * oldTransform.a + oldTransform.e) * rx
var f = selected.y * (1 - ry) - y * d +
(y * oldTransform.d + oldTransform.f) * ry
return {
type: "update",
id: el.id,
transform: {
a: a,
b: oldTransform.b,
c: oldTransform.c,
d: d,
e: e,
f: f
}
};
})
var msg = {
_children: msgs
};
var tmatrix = get_transform_matrix(selectionRect);
tmatrix.a = rx;
tmatrix.d = ry;
tmatrix.e = selectionRectTransform.e +
selectionRect.x.baseVal.value * (selectionRectTransform.a - rx)
tmatrix.f = selectionRectTransform.f +
selectionRect.y.baseVal.value * (selectionRectTransform.d - ry)
var now = performance.now();
if (now - last_sent > 70) {
last_sent = now;
@ -150,23 +337,34 @@
rect.height.baseVal.value = Math.abs(y - selected.y);
}
function get_translate_matrix(elem) {
function resetSelectionRect() {
var bbox = selectionRect.transformedBBox();
var tmatrix = get_transform_matrix(selectionRect);
selectionRect.x.baseVal.value = bbox.r[0];
selectionRect.y.baseVal.value = bbox.r[1];
selectionRect.width.baseVal.value = bbox.a[0];
selectionRect.height.baseVal.value = bbox.b[1];
tmatrix.a = 1; tmatrix.b = 0; tmatrix.c = 0;
tmatrix.d = 1; tmatrix.e = 0; tmatrix.f = 0;
}
function get_transform_matrix(elem) {
// Returns the first translate or transform matrix or makes one
var translate = null;
var transform = null;
for (var i = 0; i < elem.transform.baseVal.numberOfItems; ++i) {
var baseVal = elem.transform.baseVal[i];
// quick tests showed that even if one changes only the fields e and f or uses createSVGTransformFromMatrix
// the brower may add a SVG_TRANSFORM_MATRIX instead of a SVG_TRANSFORM_TRANSLATE
if (baseVal.type === SVGTransform.SVG_TRANSFORM_TRANSLATE || baseVal.type === SVGTransform.SVG_TRANSFORM_MATRIX) {
translate = baseVal;
if (baseVal.type === SVGTransform.SVG_TRANSFORM_MATRIX) {
transform = baseVal;
break;
}
}
if (translate == null) {
translate = elem.transform.baseVal.createSVGTransformFromMatrix(Tools.svg.createSVGMatrix());
elem.transform.baseVal.appendItem(translate);
if (transform == null) {
transform = elem.transform.baseVal.createSVGTransformFromMatrix(Tools.svg.createSVGMatrix());
elem.transform.baseVal.appendItem(transform);
}
return translate.matrix;
return transform.matrix;
}
function draw(data) {
@ -178,9 +376,19 @@
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;
var tmatrix = get_transform_matrix(elem);
for (i in data.transform) {
tmatrix[i] = data.transform[i]
}
break;
case "copy":
var newElement = Tools.svg.getElementById(data.id).cloneNode(true);
newElement.id = data.newid;
Tools.drawingArea.appendChild(newElement);
break;
case "delete":
data.tool = "Eraser";
messageForTool(data);
break;
default:
throw new Error("Mover: 'move' instruction with unknown type. ", data);
@ -189,15 +397,23 @@
}
function clickSelector(x, y, evt) {
var scale = Tools.drawingArea.getCTM().a
selectionRect = selectionRect ?? createSelectorRect();
if (pointInTransformedBBox([x, y], selectionRect.transformedBBox(scale))) {
selectionRect = selectionRect || createSelectorRect();
for (var i = 0; i < selectionButtons.length; i++) {
if (selectionButtons[i].contains(evt.target)) {
var button = selectionButtons[i];
}
}
if (button) {
button.clickCallback(x, y, evt);
} else if (pointInTransformedBBox([x, y], selectionRect.transformedBBox())) {
hideSelectionButtons();
startMovingElements(x, y, evt);
} else if (Tools.drawingArea.contains(evt.target)) {
selectionRect.style.display = "none";
hideSelectionUI();
selected_els = [getParentMathematics(evt.target)];
startMovingElements(x, y, evt);
} else {
hideSelectionButtons();
startSelector(x, y, evt);
}
}
@ -206,18 +422,20 @@
if (selectorState == selectorStates.selecting) {
selected_els = calculateSelection();
if (selected_els.length == 0) {
selectionRect.style.display = "none";
hideSelectionUI();
}
}
translation_elements = [];
} else if (selectorState == selectorStates.transform)
resetSelectionRect();
if (selected_els.length != 0) showSelectionButtons();
transform_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);
} else if (selectorState == selectorStates.transform && currentTransform) {
currentTransform(x, y);
}
}
@ -252,8 +470,31 @@
selected = null;
}
function deleteShortcut(e) {
if (e.key == "Delete" &&
!e.target.matches("input[type=text], textarea"))
deleteSelection();
}
function duplicateShortcut(e) {
if (e.key == "d" &&
!e.target.matches("input[type=text], textarea"))
duplicateSelection();
}
function switchTool() {
onquit();
if (handTool.secondary.active) {
window.addEventListener("keydown", deleteShortcut);
window.addEventListener("keydown", duplicateShortcut);
}
}
function onquit() {
selected = null;
hideSelectionUI();
window.removeEventListener("keydown", deleteShortcut);
window.removeEventListener("keydown", duplicateShortcut);
}
var handTool = { //The new tool
@ -264,6 +505,7 @@
"move": move,
"release": release,
},
"onquit": onquit,
"secondary": {
"name": "Selector",
"icon": "tools/hand/selector.svg",

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg role="img" version="1.1" viewBox="0 0 24 24" 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#">
<title>Instagram icon</title>
<g transform="matrix(2.3668 2.3668 -2.3668 2.3668 13.98 -131.11)" fill="#ff002d">
<g transform="translate(-.5821 .16648)" fill="#f00">
<rect transform="rotate(-45)" x="-2.9234" y="40.131" width="5.7222" height="5.8388" ry=".34186" fill="#bdaddf" fill-opacity=".99078" stroke="#000" stroke-linejoin="round" stroke-width=".23901"/>
</g>
</g>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:title>Instagram icon</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 784 B

View file

@ -100,6 +100,28 @@ class BoardData {
this.delaySave();
}
/** Copy elements in the board
* @param {string} id - Identifier of the data to copy.
* @param {BoardElem} data - Object containing the id of the new copied element.
*/
copy(id, data) {
var obj = this.board[id];
var newid = data.newid;
if (obj) {
var newobj = JSON.parse(JSON.stringify(obj));
newobj.id = newid;
if (newobj._children) {
for (var child of newobj._children) {
child.parent = newid;
}
}
this.board[newid] = newobj;
} else {
log("Copied object does not exist in board.", {object: id});
}
this.delaySave();
}
/** Removes data from the board
* @param {string} id - Identifier of the data to delete.
*/
@ -137,6 +159,9 @@ class BoardData {
case "update":
if (id) this.update(id, message);
break;
case "copy":
if (id) this.copy(id, message);
break;
case "child":
this.addChild(message.parent, message);
break;

View file

@ -6,5 +6,6 @@ module.exports = {
MAX_EMIT_COUNT: config.MAX_EMIT_COUNT,
MAX_EMIT_COUNT_PERIOD: config.MAX_EMIT_COUNT_PERIOD,
BLOCKED_TOOLS: config.BLOCKED_TOOLS,
BLOCKED_SELECTION_BUTTONS: config.BLOCKED_SELECTION_BUTTONS,
AUTO_FINGER_WHITEOUT: config.AUTO_FINGER_WHITEOUT,
};

View file

@ -42,6 +42,9 @@ module.exports = {
/** Blocked Tools. A comma-separated list of tools that should not appear on boards. */
BLOCKED_TOOLS: (process.env["WBO_BLOCKED_TOOLS"] || "").split(","),
/** Selection Buttons. A comma-separated list of selection buttons that should not be available. */
BLOCKED_SELECTION_BUTTONS: (process.env["WBO_BLOCKED_SELECTION_BUTTONS"] || "").split(","),
/** Automatically switch to White-out on finger touch after drawing
with Pencil using a stylus. Only supported on iPad with Apple Pencil. */
AUTO_FINGER_WHITEOUT: process.env['AUTO_FINGER_WHITEOUT'] !== "disabled",