Typicalc/frontend/src/mathjax-proof-tree.ts

320 lines
15 KiB
TypeScript
Raw Normal View History

2021-01-28 13:50:25 +00:00
import {MathjaxAdapter} from "./mathjax-adapter";
2021-02-10 15:45:36 +00:00
declare let window: {
2021-03-08 11:42:32 +00:00
svgPanZoomFun: (svg: SVGElement, options: {
fit: boolean;
controlIconsEnabled: boolean;
customEventsHandler: {
init: (options: any) => void;
haltEventListeners: string[];
destroy: () => void
};
}) => void;
2021-02-10 15:45:36 +00:00
}
// these attributes and functions are supported by major browsers, but TS does not know about them
declare global {
interface SVGElement {
2021-03-11 13:56:01 +00:00
getElementById: (id: string) => SVGGraphicsElement | null;
2021-02-10 15:45:36 +00:00
viewBox: SVGAnimatedRect;
}
2021-02-27 20:28:17 +00:00
2021-02-10 15:45:36 +00:00
interface SVGGraphicsElement {
getTransformToElement: (other: SVGGraphicsElement) => SVGMatrix;
}
2021-02-27 20:28:17 +00:00
2021-02-10 15:45:36 +00:00
interface SVGTransformList {
[index: number]: SVGTransform;
2021-01-28 13:50:25 +00:00
}
2021-02-10 15:45:36 +00:00
}
interface ProofTreeServer {
setStepCount: (count: number) => void;
}
2021-01-28 13:50:25 +00:00
2021-02-10 15:45:36 +00:00
class MathjaxProofTree extends MathjaxAdapter {
private steps: [SVGElement, SVGElement[]][] = [];
private $server: ProofTreeServer | undefined;
2021-01-28 13:50:25 +00:00
protected showStep(n: number): void {
for (let current = 0; current < this.steps.length; current++) {
this.steps[current][0].style.display = "none";
for (const node of this.steps[current][1]) {
node.style.display = "none";
}
}
2021-02-04 19:45:51 +00:00
for (let current = 0; current < this.steps.length && current <= n; current++) {
this.steps[current][0].style.display = "";
for (const node of this.steps[current][1]) {
node.style.display = "";
}
}
2021-01-28 13:50:25 +00:00
}
protected calculateSteps(): void {
2021-02-10 15:45:36 +00:00
const semanticsMatch = (semantics: string) => semantics.indexOf("bspr_inference:") >= 0;
const inferenceRuleSelector = 'g[semantics="bspr_inferenceRule:down"]';
const labelSelector = 'g[semantics="bspr_prooflabel:left"]';
const stepSelector = 'g[typicalc="step"]';
// space between inference premises
const padding = 300;
2021-03-07 19:24:08 +00:00
console.log("calculating steps..");
2021-02-10 15:45:36 +00:00
if (this.shadowRoot !== null) {
2021-02-10 09:32:52 +00:00
console.time('stepCalculation');
2021-02-06 09:41:28 +00:00
const root = this.shadowRoot;
// first, enumerate all of the steps
let stepIdx = 0;
2021-02-10 15:45:36 +00:00
for (const a of root.querySelectorAll<SVGElement>("g[semantics]")) {
let semantics = a.getAttribute("semantics")!;
if (semanticsMatch(semantics)) {
a.setAttribute("typicalc", "step");
a.setAttribute("id", "step" + stepIdx);
stepIdx++;
}
}
// then create the steps
2021-02-10 15:45:36 +00:00
let steps: [SVGElement, SVGElement[]][] = [];
stepIdx = 0;
2021-02-10 15:45:36 +00:00
for (const a of root.querySelectorAll<SVGElement>("g[semantics]")) {
let semantics = a.getAttribute("semantics")!;
if (semanticsMatch(semantics)) {
const id = "step" + stepIdx;
2021-02-10 15:45:36 +00:00
const idSelector = `#${id} `;
stepIdx++;
// find the next one/two steps above this one
2021-02-10 15:45:36 +00:00
const aboveStep1 = a.querySelector<SVGElement>(idSelector + stepSelector);
let above = [];
if (aboveStep1 != null) {
2021-02-10 15:45:36 +00:00
const parent = aboveStep1.parentElement!.parentElement!;
parent.setAttribute("id", "typicalc-selector");
2021-02-10 15:45:36 +00:00
for (const node of parent.querySelectorAll<SVGElement>('#typicalc-selector > g > ' + stepSelector)) {
above.push(node);
}
parent.removeAttribute("id");
}
2021-02-10 15:45:36 +00:00
const rule = a.querySelector(idSelector + inferenceRuleSelector);
if (rule !== null) {
let i = 0;
2021-02-10 15:45:36 +00:00
for (const node of rule.children) {
if (i !== 1) {
2021-02-10 15:45:36 +00:00
above.push(node as SVGElement);
}
i += 1;
}
}
2021-02-10 15:45:36 +00:00
const label = a.querySelector<SVGElement>(idSelector + labelSelector);
if (label) {
above.push(label);
}
if (stepIdx === 1) {
2021-02-10 15:45:36 +00:00
// initial step should not show premises
steps.push([a, []]);
}
steps.push([a, above]);
}
}
const svg = root.querySelector<SVGElement>("svg")!;
// limit start zoom
svg.viewBox.baseVal.width = Math.min(100000, svg.viewBox.baseVal.width);
svg.viewBox.baseVal.width = Math.max(20000, svg.viewBox.baseVal.width);
2021-02-06 14:57:53 +00:00
// MathJax layout of bussproofs is sometimes wrong:
// https://github.com/mathjax/MathJax/issues/2270
// https://github.com/mathjax/MathJax/issues/2626
// we fix it in two phases (executed at the same time):
2021-02-10 09:32:52 +00:00
// 1. fix overlapping by iterating over "rows" in the SVG created by MathJax
2021-02-06 14:57:53 +00:00
// in each row, the elements are arranged to not overlap
2021-02-10 09:32:52 +00:00
// 2. place inference conclusions under the center of their line
2021-02-27 20:28:17 +00:00
const nodeIterator = [...svg.querySelectorAll<SVGGraphicsElement>("g[data-mml-node='mtr']," + inferenceRuleSelector)];
console.log(`working with ${nodeIterator.length} nodes`);
// start layout fixes in the innermost part of the SVG
nodeIterator.reverse();
for (const row of nodeIterator) {
const semantics = row.getAttribute("semantics");
if (semantics === "bspr_inferenceRule:down") {
const conclusion = row.children[1].querySelector<SVGGraphicsElement>('g[data-mml-node="mstyle"]')!;
const conclusionBox = conclusion.getBBox();
const line = row.children[2] as SVGGraphicsElement;
const bbox = line.getBBox();
const offset2 = line.getTransformToElement(conclusion);
let dx = bbox.width / 2 + offset2.e - conclusionBox.width / 2;
dx += Number(line.getAttribute("x1"));
let table = row.parentNode as SVGGraphicsElement;
let prevNode;
let magicNumber = 0;
while (table.getAttribute("semantics") !== "bspr_inferenceRule:down") {
prevNode = table;
table = table.parentNode as SVGGraphicsElement;
if (table.getAttribute("data-mml-node") === "mtr" && table.childNodes.length === 3) {
for (let i = 0; i < table.childNodes.length; i++) {
if (table.childNodes[i] === prevNode) {
magicNumber = i;
}
}
}
if (table === svg) {
break;
}
}
conclusion.transform.baseVal[0].matrix.e += dx;
if (magicNumber !== 0) {
continue;
}
if (table === svg) {
break; // last step
}
const lineBelow = table.children[2] as SVGGraphicsElement;
let lineStart = 0;
if (lineBelow) {
const offset1 = lineBelow.getTransformToElement(conclusion);
lineStart = -offset1.e;
const x1 = Number(lineBelow.getAttribute("x1"));
const x2 = Number(lineBelow.getAttribute("x2"));
lineBelow.setAttribute("x1", String(x1 + lineStart));
lineBelow.setAttribute("x2", String(x2 + lineStart));
}
const label = table.parentElement!.children[1] as SVGGraphicsElement;
if (label && label.transform) {
label.transform.baseVal[0].matrix.e += lineStart;
}
} else {
2021-02-10 09:32:52 +00:00
let left = null;
let i = 0;
2021-02-10 15:45:36 +00:00
for (const rawNode of row.children) {
2021-02-10 09:32:52 +00:00
const node = rawNode as SVGGraphicsElement;
if (i === 1 || i === 3) {
i += 1;
2021-02-10 15:45:36 +00:00
continue; // spacing node
2021-02-10 09:32:52 +00:00
}
const bbox = node.getBBox();
const mat = node.transform.baseVal[0];
2021-02-10 15:45:36 +00:00
if (mat) {
2021-02-10 09:32:52 +00:00
bbox.x += mat.matrix.e;
}
// move box, and add padding between inference steps
if (left == null) {
2021-02-10 15:45:36 +00:00
// first box
2021-02-10 09:32:52 +00:00
left = bbox.x + bbox.width;
} else {
2021-02-10 15:45:36 +00:00
// this box has some elements left of it
// -> move to free space after other elements
2021-02-10 09:32:52 +00:00
mat.matrix.e -= bbox.x - left - padding;
left = bbox.x + mat.matrix.e + bbox.width;
}
if (i == 2) {
2021-02-10 15:45:36 +00:00
let parentNode = node.parentNode! as SVGGraphicsElement;
2021-02-10 09:32:52 +00:00
while (parentNode.getAttribute("semantics") !== "bspr_inferenceRule:down") {
2021-02-10 15:45:36 +00:00
parentNode = parentNode.parentNode! as SVGGraphicsElement;
2021-02-10 09:32:52 +00:00
}
2021-02-10 15:45:36 +00:00
parentNode = parentNode.children[2] as SVGGraphicsElement;
const rule = node.querySelector<SVGGraphicsElement>(inferenceRuleSelector);
if (rule) {
2021-02-10 09:32:52 +00:00
// this selector should be checked again when updating MathJax
2021-02-10 15:45:36 +00:00
const term = rule.children[1].children[0].children[0].children[1].children[0] as SVGGraphicsElement;
2021-02-10 09:32:52 +00:00
let w = -parentNode.getTransformToElement(term).e;
w += term.getBBox().width;
w += padding;
parentNode.setAttribute("x2", w.toString());
}
}
i += 1;
}
2021-02-10 09:32:52 +00:00
}
}
2021-02-10 15:45:36 +00:00
const conclusion0 = svg.querySelector<SVGElement>(inferenceRuleSelector)!.children[1].children[0].children[0].children[1] as SVGGraphicsElement;
2021-02-10 09:32:52 +00:00
const conclusionWidth = conclusion0.getBBox().width;
const svgWidth = svg.viewBox.baseVal.width;
2021-02-10 15:45:36 +00:00
const offset = (svg.children[1] as SVGGraphicsElement).getTransformToElement(conclusion0);
(svg.children[1] as SVGGraphicsElement).transform.baseVal[0].matrix.e += offset.e + svgWidth / 2 - conclusionWidth / 2;
2021-02-10 09:32:52 +00:00
console.timeEnd('stepCalculation');
2021-02-27 20:28:17 +00:00
if (nodeIterator.length >= 3) {
// should not be used on empty SVGs
2021-03-08 11:42:32 +00:00
window.svgPanZoomFun(svg, {
fit: false,
controlIconsEnabled: true,
customEventsHandler: {
// Halt all touch events
haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel'],
// Init custom events handler
init: function(options) {
const instance = options.instance;
// Init Hammer
// @ts-ignore
this.hammer = Hammer(options.svgElement);
// @ts-ignore
2021-03-11 14:07:47 +00:00
this.hammer.get('pinch').set({enable: true});
2021-03-08 11:42:32 +00:00
// Handle double tap
// @ts-ignore
2021-03-11 14:07:47 +00:00
this.hammer.on('doubletap', () => {
2021-03-08 11:42:32 +00:00
options.instance.zoomIn()
});
let pannedX = 0;
let pannedY = 0;
// Handle pan
// @ts-ignore
2021-03-11 14:07:47 +00:00
this.hammer.on('panstart panmove', ev => {
2021-03-08 11:42:32 +00:00
// On pan start reset panned variables
if (ev.type === 'panstart') {
pannedX = 0
pannedY = 0
}
// Pan only the difference
instance.panBy({x: ev.deltaX - pannedX, y: ev.deltaY - pannedY})
pannedX = ev.deltaX
pannedY = ev.deltaY
2021-03-11 14:07:47 +00:00
});
2021-03-08 11:42:32 +00:00
let initialScale = 1;
// Handle pinch
// @ts-ignore
this.hammer.on('pinchstart pinchmove', function(ev){
// On pinch start remember initial zoom
if (ev.type === 'pinchstart') {
initialScale = instance.getZoom()
instance.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y})
}
instance.zoomAtPoint(initialScale * ev.scale, {x: ev.center.x, y: ev.center.y})
2021-03-11 14:07:47 +00:00
});
2021-03-08 11:42:32 +00:00
// Prevent moving the page on some devices when panning over SVG
options.svgElement.addEventListener('touchmove', function(e: TouchEvent){ e.preventDefault(); });
}
// Destroy custom events handler
, destroy: function(){
// @ts-ignore
this.hammer.destroy()
}
}
});
2021-03-11 13:56:01 +00:00
// add tooltips to buttons
const zoomIn = document.createElementNS("http://www.w3.org/2000/svg", "title");
zoomIn.append(document.createTextNode("zoom in"));
svg.getElementById("svg-pan-zoom-zoom-in")!.children[0].appendChild(zoomIn);
const zoomOut = document.createElementNS("http://www.w3.org/2000/svg", "title");
zoomOut.append(document.createTextNode("zoom out"));
svg.getElementById("svg-pan-zoom-zoom-out")!.appendChild(zoomOut);
// move control to upper left corner
2021-03-11 13:56:01 +00:00
let matrix = svg.getElementById("svg-pan-zoom-controls")!.transform.baseVal[0].matrix;
matrix.e = 0;
matrix.f = 0;
}
this.steps = steps;
this.showStep(0);
2021-02-10 15:45:36 +00:00
this.$server!.setStepCount(steps.length);
}
2021-01-28 13:50:25 +00:00
}
}
customElements.define('tc-proof-tree', MathjaxProofTree);