From 5f6cc8a78092f6b7b0849deb0e57002a61e7953d Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Thu, 21 Apr 2022 08:24:59 -0400 Subject: [PATCH 1/4] Handle border and padding CSS in CHTML and SVG output --- ts/output/chtml/Wrappers/math.ts | 2 +- ts/output/chtml/Wrappers/msqrt.ts | 2 +- ts/output/chtml/Wrappers/munderover.ts | 14 +-- ts/output/common/OutputJax.ts | 2 +- ts/output/common/Wrapper.ts | 24 ++++- ts/output/common/Wrappers/maction.ts | 2 +- ts/output/common/Wrappers/mfenced.ts | 2 +- ts/output/common/Wrappers/mfrac.ts | 16 +-- ts/output/common/Wrappers/mmultiscripts.ts | 2 +- ts/output/common/Wrappers/mroot.ts | 4 +- ts/output/common/Wrappers/mrow.ts | 2 +- ts/output/common/Wrappers/msqrt.ts | 4 +- ts/output/common/Wrappers/msubsup.ts | 10 +- ts/output/common/Wrappers/munderover.ts | 14 +-- ts/output/common/Wrappers/scriptbase.ts | 24 ++--- ts/output/svg.ts | 2 +- ts/output/svg/Wrapper.ts | 119 ++++++++++++++++++++- ts/output/svg/Wrappers/math.ts | 2 +- ts/output/svg/Wrappers/mfrac.ts | 10 +- ts/output/svg/Wrappers/mmultiscripts.ts | 4 +- ts/output/svg/Wrappers/mroot.ts | 2 +- ts/output/svg/Wrappers/msqrt.ts | 2 +- ts/output/svg/Wrappers/munderover.ts | 6 +- ts/util/BBox.ts | 28 ++--- 24 files changed, 215 insertions(+), 84 deletions(-) diff --git a/ts/output/chtml/Wrappers/math.ts b/ts/output/chtml/Wrappers/math.ts index 5f44937f8..7ab6fa160 100644 --- a/ts/output/chtml/Wrappers/math.ts +++ b/ts/output/chtml/Wrappers/math.ts @@ -112,7 +112,7 @@ CommonMathMixin>(CHTMLWrapper) { if (this.bbox.pwidth === BBox.fullWidth) { adaptor.setAttribute(parent, 'width', 'full'); if (this.jax.table) { - let {L, w, R} = this.jax.table.getBBox(); + let {L, w, R} = this.jax.table.getOuterBBox(); if (align === 'right') { R = Math.max(R || -shift, -shift); } else if (align === 'left') { diff --git a/ts/output/chtml/Wrappers/msqrt.ts b/ts/output/chtml/Wrappers/msqrt.ts index 1f85bb175..484d4d778 100644 --- a/ts/output/chtml/Wrappers/msqrt.ts +++ b/ts/output/chtml/Wrappers/msqrt.ts @@ -78,7 +78,7 @@ export class CHTMLmsqrt extends CommonMsqrtMixin, Constructor, Constructor, Constructor(); - let bbox = this.factory.wrap(math.root).getBBox(); + let bbox = this.factory.wrap(math.root).getOuterBBox(); this.nodeMap = null; return bbox; } diff --git a/ts/output/common/Wrapper.ts b/ts/output/common/Wrapper.ts index 72c5493f5..6d93c38d5 100644 --- a/ts/output/common/Wrapper.ts +++ b/ts/output/common/Wrapper.ts @@ -314,6 +314,26 @@ export class CommonWrapper< return bbox; } + /** + * Return the wrapped node's bounding box that includes borders and padding + * + * @param {boolean} save Whether to cache the bbox or not (used for stretchy elements) + * @return {BBox} The computed bounding box + */ + public getOuterBBox(save: boolean = true): BBox { + const bbox = this.getBBox(save); + if (!this.styles) return bbox; + const obox = new BBox(); + Object.assign(obox, bbox); + for (const [name, side] of BBox.StyleAdjust) { + const x = this.styles.get(name); + if (x) { + (obox as any)[side] += this.length2em(this.styles.get(name), 1, obox.rscale); + } + } + return obox; + } + /** * @param {BBox} bbox The bounding box to modify (either this.bbox, or an empty one) * @param {boolean} recompute True if we are recomputing due to changes in children that have percentage widths @@ -321,7 +341,7 @@ export class CommonWrapper< protected computeBBox(bbox: BBox, recompute: boolean = false) { bbox.empty(); for (const child of this.childNodes) { - bbox.append(child.getBBox()); + bbox.append(child.getOuterBBox()); } bbox.clean(); if (this.fixesPWidth && this.setChildPWidths(recompute)) { @@ -348,7 +368,7 @@ export class CommonWrapper< } let changed = false; for (const child of this.childNodes) { - const cbox = child.getBBox(); + const cbox = child.getOuterBBox(); if (cbox.pwidth && child.setChildPWidths(recompute, w === null ? cbox.w : w, clear)) { changed = true; } diff --git a/ts/output/common/Wrappers/maction.ts b/ts/output/common/Wrappers/maction.ts index 913d1d281..8dcc75a8c 100644 --- a/ts/output/common/Wrappers/maction.ts +++ b/ts/output/common/Wrappers/maction.ts @@ -184,7 +184,7 @@ export function CommonMactionMixin< * @override */ public computeBBox(bbox: BBox, recompute: boolean = false) { - bbox.updateFrom(this.selected.getBBox()); + bbox.updateFrom(this.selected.getOuterBBox()); this.selected.setChildPWidths(recompute); } diff --git a/ts/output/common/Wrappers/mfenced.ts b/ts/output/common/Wrappers/mfenced.ts index e4294427d..36a7ad56e 100644 --- a/ts/output/common/Wrappers/mfenced.ts +++ b/ts/output/common/Wrappers/mfenced.ts @@ -135,7 +135,7 @@ export function CommonMfencedMixin(Base: T): Mfenc * @override */ public computeBBox(bbox: BBox, recompute: boolean = false) { - bbox.updateFrom(this.mrow.getBBox()); + bbox.updateFrom(this.mrow.getOuterBBox()); this.setChildPWidths(recompute); } diff --git a/ts/output/common/Wrappers/mfrac.ts b/ts/output/common/Wrappers/mfrac.ts index fde6a0fa4..a4ab0aa21 100644 --- a/ts/output/common/Wrappers/mfrac.ts +++ b/ts/output/common/Wrappers/mfrac.ts @@ -159,8 +159,8 @@ export function CommonMfracMixin(Base: T): MfracCo * @param {number} t The thickness of the line */ public getFractionBBox(bbox: BBox, display: boolean, t: number) { - const nbox = this.childNodes[0].getBBox(); - const dbox = this.childNodes[1].getBBox(); + const nbox = this.childNodes[0].getOuterBBox(); + const dbox = this.childNodes[1].getOuterBBox(); const tex = this.font.params; const a = tex.axis_height; const {T, u, v} = this.getTUV(display, t); @@ -204,8 +204,8 @@ export function CommonMfracMixin(Base: T): MfracCo * the separation between the two, and the bboxes themselves. */ public getUVQ(display: boolean): {u: number, v: number, q: number, nbox: BBox, dbox: BBox} { - const nbox = this.childNodes[0].getBBox() as BBox; - const dbox = this.childNodes[1].getBBox() as BBox; + const nbox = this.childNodes[0].getOuterBBox(); + const dbox = this.childNodes[1].getOuterBBox(); const tex = this.font.params; // // Initial offsets (u, v) @@ -234,7 +234,7 @@ export function CommonMfracMixin(Base: T): MfracCo */ public getBevelledBBox(bbox: BBox, display: boolean) { const {u, v, delta, nbox, dbox} = this.getBevelData(display); - const lbox = this.bevel.getBBox(); + const lbox = this.bevel.getOuterBBox(); bbox.combine(nbox, 0, u); bbox.combine(lbox, bbox.w - delta / 2, 0); bbox.combine(dbox, bbox.w - delta / 2, v); @@ -249,8 +249,8 @@ export function CommonMfracMixin(Base: T): MfracCo public getBevelData(display: boolean): { H: number, delta: number, u: number, v: number, nbox: BBox, dbox: BBox } { - const nbox = this.childNodes[0].getBBox() as BBox; - const dbox = this.childNodes[1].getBBox() as BBox; + const nbox = this.childNodes[0].getOuterBBox(); + const dbox = this.childNodes[1].getOuterBBox(); const delta = (display ? .4 : .15); const H = Math.max(nbox.scale * (nbox.h + nbox.d), dbox.scale * (dbox.h + dbox.d)) + 2 * delta; const a = this.font.params.axis_height; @@ -282,7 +282,7 @@ export function CommonMfracMixin(Base: T): MfracCo public getWrapWidth(i: number) { const attributes = this.node.attributes; if (attributes.get('bevelled')) { - return this.childNodes[i].getBBox().w; + return this.childNodes[i].getOuterBBox().w; } const w = this.getBBox().w; const thickness = this.length2em(attributes.get('linethickness')); diff --git a/ts/output/common/Wrappers/mmultiscripts.ts b/ts/output/common/Wrappers/mmultiscripts.ts index 891865286..4937b8f98 100644 --- a/ts/output/common/Wrappers/mmultiscripts.ts +++ b/ts/output/common/Wrappers/mmultiscripts.ts @@ -254,7 +254,7 @@ export function CommonMmultiscriptsMixin< if (child.node.isKind('mprescripts')) { script = 'psubList'; } else { - lists[script].push(child.getBBox()); + lists[script].push(child.getOuterBBox()); script = NextScript[script]; } } diff --git a/ts/output/common/Wrappers/mroot.ts b/ts/output/common/Wrappers/mroot.ts index 9e5e4df97..635c8e951 100644 --- a/ts/output/common/Wrappers/mroot.ts +++ b/ts/output/common/Wrappers/mroot.ts @@ -66,7 +66,7 @@ export function CommonMrootMixin(Base: T): MrootCons * @override */ public combineRootBBox(BBOX: BBox, sbox: BBox, H: number) { - const bbox = this.childNodes[this.root].getBBox(); + const bbox = this.childNodes[this.root].getOuterBBox(); const h = this.getRootDimens(sbox, H)[1]; BBOX.combine(bbox, 0, h); } @@ -76,7 +76,7 @@ export function CommonMrootMixin(Base: T): MrootCons */ public getRootDimens(sbox: BBox, H: number) { const surd = this.childNodes[this.surd] as CommonMo; - const bbox = this.childNodes[this.root].getBBox(); + const bbox = this.childNodes[this.root].getOuterBBox(); const offset = (surd.size < 0 ? .5 : .6) * sbox.w; const {w, rscale} = bbox; const W = Math.max(w, offset / rscale); diff --git a/ts/output/common/Wrappers/mrow.ts b/ts/output/common/Wrappers/mrow.ts index 0fe436b89..b936fa38b 100644 --- a/ts/output/common/Wrappers/mrow.ts +++ b/ts/output/common/Wrappers/mrow.ts @@ -102,7 +102,7 @@ export function CommonMrowMixin(Base: T): MrowCons for (const child of this.childNodes) { const noStretch = (child.stretch.dir === DIRECTION.None); if (all || noStretch) { - let {h, d, rscale} = child.getBBox(noStretch); + let {h, d, rscale} = child.getOuterBBox(noStretch); h *= rscale; d *= rscale; if (h > H) H = h; diff --git a/ts/output/common/Wrappers/msqrt.ts b/ts/output/common/Wrappers/msqrt.ts index 46c779b77..b53ca81a4 100644 --- a/ts/output/common/Wrappers/msqrt.ts +++ b/ts/output/common/Wrappers/msqrt.ts @@ -125,7 +125,7 @@ export function CommonMsqrtMixin(Base: T): MsqrtCo super(...args); const surd = this.createMo('\u221A'); surd.canStretch(DIRECTION.Vertical); - const {h, d} = this.childNodes[this.base].getBBox(); + const {h, d} = this.childNodes[this.base].getOuterBBox(); const t = this.font.params.rule_thickness; const p = (this.node.attributes.get('displaystyle') ? this.font.params.x_height : t); this.surdH = h + d + 2 * t + p / 4; @@ -146,7 +146,7 @@ export function CommonMsqrtMixin(Base: T): MsqrtCo */ public computeBBox(bbox: BBox, recompute: boolean = false) { const surdbox = this.childNodes[this.surd].getBBox(); - const basebox = new BBox(this.childNodes[this.base].getBBox()); + const basebox = new BBox(this.childNodes[this.base].getOuterBBox()); const q = this.getPQ(surdbox)[1]; const t = this.font.params.rule_thickness; const H = basebox.h + q + t; diff --git a/ts/output/common/Wrappers/msubsup.ts b/ts/output/common/Wrappers/msubsup.ts index 555bbcea9..34f7bbe3a 100644 --- a/ts/output/common/Wrappers/msubsup.ts +++ b/ts/output/common/Wrappers/msubsup.ts @@ -217,8 +217,8 @@ export function CommonMsubsupMixin< * @override */ public computeBBox(bbox: BBox, recompute: boolean = false) { - const basebox = this.baseChild.getBBox(); - const [subbox, supbox] = [this.subChild.getBBox(), this.supChild.getBBox()]; + const basebox = this.baseChild.getOuterBBox(); + const [subbox, supbox] = [this.subChild.getOuterBBox(), this.supChild.getOuterBBox()]; bbox.empty(); bbox.append(basebox); const w = this.getBaseWidth(); @@ -239,10 +239,10 @@ export function CommonMsubsupMixin< * @return {number[]} The vertical offsets for super and subscripts, and the space between them */ public getUVQ( - subbox: BBox = this.subChild.getBBox(), - supbox: BBox = this.supChild.getBBox() + subbox: BBox = this.subChild.getOuterBBox(), + supbox: BBox = this.supChild.getOuterBBox() ): number[] { - const basebox = this.baseCore.getBBox(); + const basebox = this.baseCore.getOuterBBox(); if (this.UVQ) return this.UVQ; const tex = this.font.params; const t = 3 * tex.rule_thickness; diff --git a/ts/output/common/Wrappers/munderover.ts b/ts/output/common/Wrappers/munderover.ts index 86e4ff092..8bb72fa35 100644 --- a/ts/output/common/Wrappers/munderover.ts +++ b/ts/output/common/Wrappers/munderover.ts @@ -82,8 +82,8 @@ export function CommonMunderMixin< return; } bbox.empty(); - const basebox = this.baseChild.getBBox(); - const underbox = this.scriptChild.getBBox(); + const basebox = this.baseChild.getOuterBBox(); + const underbox = this.scriptChild.getOuterBBox(); const v = this.getUnderKV(basebox, underbox)[1]; const delta = (this.isLineBelow ? 0 : this.getDelta(true)); const [bw, uw] = this.getDeltaW([basebox, underbox], [0, -delta]); @@ -153,8 +153,8 @@ export function CommonMoverMixin< return; } bbox.empty(); - const basebox = this.baseChild.getBBox(); - const overbox = this.scriptChild.getBBox(); + const basebox = this.baseChild.getOuterBBox(); + const overbox = this.scriptChild.getOuterBBox(); if (this.node.attributes.get('accent')) { basebox.h = Math.max(basebox.h, this.font.params.x_height * basebox.scale); } @@ -262,9 +262,9 @@ export function CommonMunderoverMixin< return; } bbox.empty(); - const overbox = this.overChild.getBBox(); - const basebox = this.baseChild.getBBox(); - const underbox = this.underChild.getBBox(); + const overbox = this.overChild.getOuterBBox(); + const basebox = this.baseChild.getOuterBBox(); + const underbox = this.underChild.getOuterBBox(); if (this.node.attributes.get('accent')) { basebox.h = Math.max(basebox.h, this.font.params.x_height * basebox.scale); } diff --git a/ts/output/common/Wrappers/scriptbase.ts b/ts/output/common/Wrappers/scriptbase.ts index a59f3f266..7bc015af7 100644 --- a/ts/output/common/Wrappers/scriptbase.ts +++ b/ts/output/common/Wrappers/scriptbase.ts @@ -435,7 +435,7 @@ export function CommonScriptbaseMixin< let child = this.baseCore as any; let scale = 1; while (child && child !== this) { - const bbox = child.getBBox(); + const bbox = child.getOuterBBox(); scale *= bbox.rscale; child = child.parent; } @@ -446,14 +446,14 @@ export function CommonScriptbaseMixin< * The base's italic correction (properly scaled) */ public getBaseIc(): number { - return this.baseCore.getBBox().ic * this.baseScale; + return this.baseCore.getOuterBBox().ic * this.baseScale; } /** * An adjusted italic correction (for slightly better results) */ public getAdjustedIc(): number { - const bbox = this.baseCore.getBBox(); + const bbox = this.baseCore.getOuterBBox(); return (bbox.ic ? 1.05 * bbox.ic + .05 : 0) * this.baseScale; } @@ -501,7 +501,7 @@ export function CommonScriptbaseMixin< * @return {number} The base child's width without the base italic correction (if not needed) */ public getBaseWidth(): number { - const bbox = this.baseChild.getBBox(); + const bbox = this.baseChild.getOuterBBox(); return bbox.w * bbox.rscale - (this.baseRemoveIc ? this.baseIc : 0) + this.font.params.extra_ic; } @@ -514,8 +514,8 @@ export function CommonScriptbaseMixin< public computeBBox(bbox: BBox, recompute: boolean = false) { const w = this.getBaseWidth(); const [x, y] = this.getOffset(); - bbox.append(this.baseChild.getBBox()); - bbox.combine(this.scriptChild.getBBox(), w + x, y); + bbox.append(this.baseChild.getOuterBBox()); + bbox.combine(this.scriptChild.getOuterBBox(), w + x, y); bbox.w += this.font.params.scriptspace; bbox.clean(); this.setChildPWidths(recompute); @@ -546,8 +546,8 @@ export function CommonScriptbaseMixin< * @return {number} The vertical offset for the script */ public getV(): number { - const bbox = this.baseCore.getBBox(); - const sbox = this.scriptChild.getBBox(); + const bbox = this.baseCore.getOuterBBox(); + const sbox = this.scriptChild.getOuterBBox(); const tex = this.font.params; const subscriptshift = this.length2em(this.node.attributes.get('subscriptshift'), tex.sub1); return Math.max( @@ -563,8 +563,8 @@ export function CommonScriptbaseMixin< * @return {number} The vertical offset for the script */ public getU(): number { - const bbox = this.baseCore.getBBox(); - const sbox = this.scriptChild.getBBox(); + const bbox = this.baseCore.getOuterBBox(); + const sbox = this.scriptChild.getOuterBBox(); const tex = this.font.params; const attr = this.node.attributes.getList('displaystyle', 'superscriptshift'); const prime = this.node.getProperty('texprimestyle'); @@ -661,7 +661,7 @@ export function CommonScriptbaseMixin< */ public getDelta(noskew: boolean = false): number { const accent = this.node.attributes.get('accent'); - const {sk, ic} = this.baseCore.getBBox(); + const {sk, ic} = this.baseCore.getOuterBBox(); return ((accent && !noskew ? sk : 0) + this.font.skewIcFactor * ic) * this.baseScale; } @@ -691,7 +691,7 @@ export function CommonScriptbaseMixin< for (const child of this.childNodes) { const noStretch = (child.stretch.dir === DIRECTION.None); if (all || noStretch) { - const {w, rscale} = child.getBBox(noStretch); + const {w, rscale} = child.getOuterBBox(noStretch); if (w * rscale > W) W = w * rscale; } } diff --git a/ts/output/svg.ts b/ts/output/svg.ts index 525cceab4..b9f32e18d 100644 --- a/ts/output/svg.ts +++ b/ts/output/svg.ts @@ -237,7 +237,7 @@ CommonOutputJax, SVGWrapperFactory, SVGFon * @return {[N, N]} The svg and g nodes for the math */ protected createRoot(wrapper: SVGWrapper): [N, N] { - const {w, h, d, pwidth} = wrapper.getBBox(); + const {w, h, d, pwidth} = wrapper.getOuterBBox(); const px = wrapper.metrics.em / 1000; const W = Math.max(w, px); // make sure we are at least one px wide (needed for e.g. \llap) const H = Math.max(h + d, px); // make sure we are at least one px tall (needed for e.g., \smash) diff --git a/ts/output/svg/Wrapper.ts b/ts/output/svg/Wrapper.ts index e6881c128..68b51d35a 100644 --- a/ts/output/svg/Wrapper.ts +++ b/ts/output/svg/Wrapper.ts @@ -22,6 +22,7 @@ */ import {OptionList} from '../../util/Options.js'; +import {BBox} from '../../util/BBox.js'; import {CommonWrapper, AnyWrapperClass, Constructor} from '../common/Wrapper.js'; import {SVG, XLINKNS} from '../svg.js'; import {SVGWrapperFactory} from './WrapperFactory.js'; @@ -70,6 +71,11 @@ CommonWrapper< */ public static kind: string = 'unknown'; + /** + * A fuzz factor for borders to avoid anti-alias problems at the edges + */ + public static borderFuzz = 0.005; + /** * The factory used to create more SVGWrappers */ @@ -89,6 +95,11 @@ CommonWrapper< */ public element: N = null; + /** + * Offset due to border/padding + */ + public dx: number = 0; + /** * @override */ @@ -131,6 +142,7 @@ CommonWrapper< const svg = this.createSVGnode(parent); this.handleStyles(); this.handleScale(); + this.handleBorder(); this.handleColor(); this.handleAttributes(); return svg; @@ -164,6 +176,13 @@ CommonWrapper< if (styles) { this.adaptor.setAttribute(this.element, 'style', styles); } + BBox.StyleAdjust.forEach(([name, , lr]) => { + if (lr !== 0) return; + const x = this.styles.get(name); + if (x) { + this.dx += this.length2em(x, 1, this.bbox.rscale); + } + }); } /** @@ -188,15 +207,16 @@ CommonWrapper< const color = attributes.getExplicit('color') as string; const mathbackground = attributes.getExplicit('mathbackground') as string; const background = attributes.getExplicit('background') as string; + const bgcolor = (this.styles?.get('background-color') || ''); if (mathcolor || color) { adaptor.setAttribute(this.element, 'fill', mathcolor || color); adaptor.setAttribute(this.element, 'stroke', mathcolor || color); } - if (mathbackground || background) { - let {h, d, w} = this.getBBox(); + if (mathbackground || background || bgcolor) { + let {h, d, w} = this.getOuterBBox(); let rect = this.svg('rect', { - fill: mathbackground || background, - x: 0, y: this.fixed(-d), + fill: mathbackground || background || bgcolor, + x: this.fixed(-this.dx), y: this.fixed(-d), width: this.fixed(w), height: this.fixed(h + d), 'data-bgcolor': true @@ -210,6 +230,96 @@ CommonWrapper< } } + /** + * Create the borders, if any are requested. + */ + protected handleBorder() { + if (!this.styles) return; + const width = Array(4).fill(0); + const style = Array(4); + const color = Array(4); + for (const [name, i] of [['Top', 0], ['Right', 1], ['Bottom', 2], ['Left', 3]] as [string, number][]) { + const key = 'border' + name; + const w = this.styles.get(key + 'Width'); + if (!w) continue; + width[i] = Math.max(0, this.length2em(w, 1, this.bbox.rscale)); + style[i] = this.styles.get(key + 'Style') || 'solid'; + color[i] = this.styles.get(key + 'Color') || 'currentColor'; + } + const f = SVGWrapper.borderFuzz; + const bbox = this.getOuterBBox(); + const [h, d, w] = [bbox.h + f, bbox.d + f, bbox.w + f]; + const paths: [number, number][][] = [ + [[-f, h], [w, h], [w - width[1], h - width[0]], [-f + width[3], h - width[0]]], + [[w, h], [w, -d], [w - width[1], -d + width[2]], [w - width[1], h - width[0]]], + [[w, -d], [-f, -d], [-f + width[3], -d + width[2]], [w - width[1], -d + width[2]]], + [[-f, -d], [-f, h], [-f + width[3], h - width[0]], [-f + width[3], -d + width[2]]] + ]; + const adaptor = this.adaptor; + const child = adaptor.firstChild(this.element) as N; + for (const i of [0, 1, 2, 3]) { + if (!width[i]) continue; + const path = paths[i]; + if (style[i] === 'dashed' || style[i] === 'dotted') { + this.addBorderBroken(path, color[i], style[i], width[i], !!(i % 2)); + } else { + this.addBorderSolid(path, color[i], child); + } + } + } + + /** + * Create a solid border piece with the given color + * + * @param {[number, number][]} path The points for the border segment + * @param {string} color The color to use + * @param {N} child Insert the border before this child, if any + */ + protected addBorderSolid(path: [number, number][], color: string, child: N) { + const border = this.svg('polygon', { + points: path.map(([x, y]) => `${this.fixed(x - this.dx)},${this.fixed(y)}`).join(' '), + stroke: 'none', + fill: color + }); + if (child) { + this.adaptor.insert(border, child); + } else { + this.adaptor.append(this.element, border); + } + } + + /** + * Create a dashed or dotted border line with the given width and color + * + * @param {[number, number][]} path The points for the border segment + * @param {string} color The color to use + * @param {string} style Either 'dotted' or 'dashed' + * @param {number} t The thickness for the border line + * @param {boolean} vertical True if the line is vertical, false for horizontal + */ + protected addBorderBroken(path: [number, number][], color: string, style: string, t: number, vertical: boolean) { + const dot = (style === 'dotted'); + const [A, B, C, D] = path; + const x1 = (A[0] + D[0]) / 2 - this.dx, y1 = (A[1] + D[1]) / 2; + const x2 = (B[0] + C[0]) / 2 - this.dx, y2 = (B[1] + C[1]) / 2; + const W = Math.abs(vertical ? y2 - y1 : x2 - x1); + const n = (dot ? Math.ceil(W / (2 * t)) : Math.ceil((W - t) / (4 * t))); + const m = W / (4 * n + 1); + const line = this.svg('line', { + x1: this.fixed(x1), y1: this.fixed(y1), + x2: this.fixed(x2), y2: this.fixed(y2), + 'stroke-width': this.fixed(t), stroke: color, 'stroke-linecap': dot ? 'round' : 'square', + 'stroke-dasharray': dot ? [1, this.fixed(W / n - .002)].join(' ') : [this.fixed(m), this.fixed(3 * m)].join(' ') + }); + const adaptor = this.adaptor; + const child = adaptor.firstChild(this.element); + if (child) { + adaptor.insert(line, child); + } else { + adaptor.append(this.element, line); + } + } + /** * Copy RDFa, aria, and other tags from the MathML to the SVG output nodes. * Don't copy those in the skipAttributes list, or anything that already exists @@ -243,6 +353,7 @@ CommonWrapper< * @param {N} element The element to be placed */ public place(x: number, y: number, element: N = null) { + x += this.dx; if (!(x || y)) return; if (!element) { element = this.element; diff --git a/ts/output/svg/Wrappers/math.ts b/ts/output/svg/Wrappers/math.ts index 3e61225e0..b2326baef 100644 --- a/ts/output/svg/Wrappers/math.ts +++ b/ts/output/svg/Wrappers/math.ts @@ -92,7 +92,7 @@ CommonMathMixin>(SVGWrapper) { if (this.bbox.pwidth === BBox.fullWidth) { this.adaptor.setAttribute(this.jax.container, 'width', 'full'); if (this.jax.table) { - let {L, w, R} = this.jax.table.getBBox(); + let {L, w, R} = this.jax.table.getOuterBBox(); if (align === 'right') { R = Math.max(R || -shift, -shift); } else if (align === 'left') { diff --git a/ts/output/svg/Wrappers/mfrac.ts b/ts/output/svg/Wrappers/mfrac.ts index 3802377c4..db86266e1 100644 --- a/ts/output/svg/Wrappers/mfrac.ts +++ b/ts/output/svg/Wrappers/mfrac.ts @@ -77,8 +77,8 @@ export class SVGmfrac extends CommonMfracMixin extends CommonMfracMixin extends CommonMfracMixin, Constructor, Constructor extends CommonMrootMixin, sbox: BBox, H: number) { root.toSVG(ROOT); const [x, h, dx] = this.getRootDimens(sbox, H); - const bbox = root.getBBox(); + const bbox = root.getOuterBBox(); root.place(dx * bbox.rscale, h); this.dx = x; } diff --git a/ts/output/svg/Wrappers/msqrt.ts b/ts/output/svg/Wrappers/msqrt.ts index bb7e2f3ba..38f7f2f1e 100644 --- a/ts/output/svg/Wrappers/msqrt.ts +++ b/ts/output/svg/Wrappers/msqrt.ts @@ -57,7 +57,7 @@ export class SVGmsqrt extends CommonMsqrtMixin, Constructor> const svg = this.standardSVGnode(parent); const [base, script] = [this.baseChild, this.scriptChild]; - const [bbox, sbox] = [base.getBBox(), script.getBBox()]; + const [bbox, sbox] = [base.getOuterBBox(), script.getOuterBBox()]; base.toSVG(svg); script.toSVG(svg); @@ -99,7 +99,7 @@ CommonMoverMixin, Constructor>> } const svg = this.standardSVGnode(parent); const [base, script] = [this.baseChild, this.scriptChild]; - const [bbox, sbox] = [base.getBBox(), script.getBBox()]; + const [bbox, sbox] = [base.getOuterBBox(), script.getOuterBBox()]; base.toSVG(svg); script.toSVG(svg); @@ -141,7 +141,7 @@ CommonMunderoverMixin, Constructor Date: Thu, 21 Apr 2022 10:03:47 -0400 Subject: [PATCH 2/4] Fix placement of hit boxes and placement in rows --- ts/output/svg/Wrapper.ts | 7 ++++--- ts/output/svg/Wrappers/maction.ts | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ts/output/svg/Wrapper.ts b/ts/output/svg/Wrapper.ts index 68b51d35a..56c026cca 100644 --- a/ts/output/svg/Wrapper.ts +++ b/ts/output/svg/Wrapper.ts @@ -123,10 +123,11 @@ CommonWrapper< let x = 0; for (const child of this.childNodes) { child.toSVG(parent); + const bbox = child.getOuterBBox(); if (child.element) { - child.place(x + child.bbox.L * child.bbox.rscale, 0); + child.place(x + bbox.L * bbox.rscale, 0); } - x += (child.bbox.L + child.bbox.w + child.bbox.R) * child.bbox.rscale; + x += (bbox.L + bbox.w + bbox.R) * bbox.rscale; } } @@ -157,7 +158,7 @@ CommonWrapper< const href = this.node.attributes.get('href'); if (href) { parent = this.adaptor.append(parent, this.svg('a', {href: href})) as N; - const {h, d, w} = this.getBBox(); + const {h, d, w} = this.getOuterBBox(); this.adaptor.append(this.element, this.svg('rect', { 'data-hitbox': true, fill: 'none', stroke: 'none', 'pointer-events': 'all', width: this.fixed(w), height: this.fixed(h + d), y: this.fixed(-d) diff --git a/ts/output/svg/Wrappers/maction.ts b/ts/output/svg/Wrappers/maction.ts index 05e0eb3ba..90bf4e69e 100644 --- a/ts/output/svg/Wrappers/maction.ts +++ b/ts/output/svg/Wrappers/maction.ts @@ -205,12 +205,16 @@ CommonMactionMixin, SVGConstructor>(SVG public toSVG(parent: N) { const svg = this.standardSVGnode(parent); const child = this.selected; - const {h, d, w} = child.getBBox(); + const {h, d, w} = child.getOuterBBox(); this.adaptor.append(this.element, this.svg('rect', { width: this.fixed(w), height: this.fixed(h + d), y: this.fixed(-d), fill: 'none', 'pointer-events': 'all' })); child.toSVG(svg); + const bbox = child.getOuterBBox(); + if (child.element) { + child.place(bbox.L * bbox.rscale, 0); + } this.action(this, this.data); } From 1a1caeffeda94769a05a3d5fa910268392a3ba16 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Thu, 21 Apr 2022 10:26:22 -0400 Subject: [PATCH 3/4] Handle missing or different sized dashed and dotten borders better --- ts/output/svg/Wrapper.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/ts/output/svg/Wrapper.ts b/ts/output/svg/Wrapper.ts index 56c026cca..c103de99b 100644 --- a/ts/output/svg/Wrapper.ts +++ b/ts/output/svg/Wrapper.ts @@ -252,8 +252,8 @@ CommonWrapper< const [h, d, w] = [bbox.h + f, bbox.d + f, bbox.w + f]; const paths: [number, number][][] = [ [[-f, h], [w, h], [w - width[1], h - width[0]], [-f + width[3], h - width[0]]], - [[w, h], [w, -d], [w - width[1], -d + width[2]], [w - width[1], h - width[0]]], - [[w, -d], [-f, -d], [-f + width[3], -d + width[2]], [w - width[1], -d + width[2]]], + [[w, -d], [w, h], [w - width[1], h - width[0]], [w - width[1], -d + width[2]]], + [[-f, -d], [w, -d], [w - width[1], -d + width[2]], [-f + width[3], -d + width[2]]], [[-f, -d], [-f, h], [-f + width[3], h - width[0]], [-f + width[3], -d + width[2]]] ]; const adaptor = this.adaptor; @@ -262,7 +262,7 @@ CommonWrapper< if (!width[i]) continue; const path = paths[i]; if (style[i] === 'dashed' || style[i] === 'dotted') { - this.addBorderBroken(path, color[i], style[i], width[i], !!(i % 2)); + this.addBorderBroken(path, color[i], style[i], width[i], i); } else { this.addBorderSolid(path, color[i], child); } @@ -296,14 +296,16 @@ CommonWrapper< * @param {string} color The color to use * @param {string} style Either 'dotted' or 'dashed' * @param {number} t The thickness for the border line - * @param {boolean} vertical True if the line is vertical, false for horizontal + * @param {number} i The side being drawn */ - protected addBorderBroken(path: [number, number][], color: string, style: string, t: number, vertical: boolean) { + protected addBorderBroken(path: [number, number][], color: string, style: string, t: number, i: number) { const dot = (style === 'dotted'); - const [A, B, C, D] = path; - const x1 = (A[0] + D[0]) / 2 - this.dx, y1 = (A[1] + D[1]) / 2; - const x2 = (B[0] + C[0]) / 2 - this.dx, y2 = (B[1] + C[1]) / 2; - const W = Math.abs(vertical ? y2 - y1 : x2 - x1); + const t2 = t / 2; + const [tx1, ty1, tx2, ty2] = [[t2, -t2, -t2, -t2], [-t2, t2, -t2, -t2], [t2, t2, -t2, t2], [t2, t2, t2, -t2]][i]; + const [A, B] = path; + const x1 = A[0] + tx1 - this.dx, y1 = A[1] + ty1; + const x2 = B[0] + tx2 - this.dx, y2 = B[1] + ty2; + const W = Math.abs(i % 2 ? y2 - y1 : x2 - x1); const n = (dot ? Math.ceil(W / (2 * t)) : Math.ceil((W - t) / (4 * t))); const m = W / (4 * n + 1); const line = this.svg('line', { From 5979969248c1f116a85b10adc9979092a130d39d Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sun, 1 May 2022 09:31:22 -0400 Subject: [PATCH 4/4] Updates requeste by Volker's review --- ts/output/common/Wrapper.ts | 2 +- ts/output/svg/Wrapper.ts | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ts/output/common/Wrapper.ts b/ts/output/common/Wrapper.ts index 6d93c38d5..8ea122935 100644 --- a/ts/output/common/Wrapper.ts +++ b/ts/output/common/Wrapper.ts @@ -328,7 +328,7 @@ export class CommonWrapper< for (const [name, side] of BBox.StyleAdjust) { const x = this.styles.get(name); if (x) { - (obox as any)[side] += this.length2em(this.styles.get(name), 1, obox.rscale); + (obox as any)[side] += this.length2em(x, 1, obox.rscale); } } return obox; diff --git a/ts/output/svg/Wrapper.ts b/ts/output/svg/Wrapper.ts index c103de99b..989698e53 100644 --- a/ts/output/svg/Wrapper.ts +++ b/ts/output/svg/Wrapper.ts @@ -250,11 +250,19 @@ CommonWrapper< const f = SVGWrapper.borderFuzz; const bbox = this.getOuterBBox(); const [h, d, w] = [bbox.h + f, bbox.d + f, bbox.w + f]; - const paths: [number, number][][] = [ - [[-f, h], [w, h], [w - width[1], h - width[0]], [-f + width[3], h - width[0]]], - [[w, -d], [w, h], [w - width[1], h - width[0]], [w - width[1], -d + width[2]]], - [[-f, -d], [w, -d], [w - width[1], -d + width[2]], [-f + width[3], -d + width[2]]], - [[-f, -d], [-f, h], [-f + width[3], h - width[0]], [-f + width[3], -d + width[2]]] + const outerRT = [w, h]; + const outerLT = [-f, h]; + const outerRB = [w, -d]; + const outerLB = [-f, -d]; + const innerRT = [w - width[1], h - width[0]]; + const innerLT = [-f + width[3], h - width[0]]; + const innerRB = [w - width[1], -d + width[2]]; + const innerLB = [-f + width[3],-d + width[2]]; + const paths: number[][][] = [ + [outerLT, outerRT, innerRT, innerLT], + [outerRB, outerRT, innerRT, innerRB], + [outerLB, outerRB, innerRB, innerLB], + [outerLB, outerLT, innerLT, innerLB] ]; const adaptor = this.adaptor; const child = adaptor.firstChild(this.element) as N; @@ -276,7 +284,7 @@ CommonWrapper< * @param {string} color The color to use * @param {N} child Insert the border before this child, if any */ - protected addBorderSolid(path: [number, number][], color: string, child: N) { + protected addBorderSolid(path: number[][], color: string, child: N) { const border = this.svg('polygon', { points: path.map(([x, y]) => `${this.fixed(x - this.dx)},${this.fixed(y)}`).join(' '), stroke: 'none', @@ -298,7 +306,7 @@ CommonWrapper< * @param {number} t The thickness for the border line * @param {number} i The side being drawn */ - protected addBorderBroken(path: [number, number][], color: string, style: string, t: number, i: number) { + protected addBorderBroken(path: number[][], color: string, style: string, t: number, i: number) { const dot = (style === 'dotted'); const t2 = t / 2; const [tx1, ty1, tx2, ty2] = [[t2, -t2, -t2, -t2], [-t2, t2, -t2, -t2], [t2, t2, -t2, t2], [t2, t2, t2, -t2]][i];