Skip to content

Commit

Permalink
feat: accurate Equation measurements (#1445)
Browse files Browse the repository at this point in the history
  • Loading branch information
wodeni committed Jun 2, 2023
1 parent d7e101e commit 93d30f5
Show file tree
Hide file tree
Showing 26 changed files with 259 additions and 216 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ It's specified by the following trio of Domain, Substance, and Style programs
x.text = Equation {
string : x.label
fontSize : "25px"
fontSize : "32px"
}
ensure contains(x.icon, x.text)
Expand Down
224 changes: 91 additions & 133 deletions docs/assets/output.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/core/src/compiler/shapeChecker/CheckShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,21 @@ export const checkEquation = (
const string = checkString(path, trans);
if (string.isErr()) return err(string.error);

const ascent = checkProp(path, "ascent", trans, checkFloatV);
if (ascent.isErr()) return err(ascent.error);

const descent = checkProp(path, "descent", trans, checkFloatV);
if (descent.isErr()) return err(descent.error);

return ok({
...named.value,
...fill.value,
...center.value,
...rect.value,
...rotate.value,
...string.value,
ascent: ascent.value,
descent: descent.value,
passthrough: new Map(),
shapeType: "Equation",
});
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/engine/EngineUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ const mapEquation = <T, S>(f: (arg: T) => S, v: Equation<T>): Equation<S> => {
...mapRect(f, v),
...mapRotate(f, v),
...mapString(f, v),
ascent: mapFloat(f, v.ascent),
descent: mapFloat(f, v.descent),
passthrough: mapPassthrough(f, v.passthrough),
};
};
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/overall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,20 +232,20 @@ describe("Energy API", () => {

describe("Cross-instance energy eval", () => {
test("correct - subsets", async () => {
const twosets = `Set A, B\nAutoLabel All`;
const twosets = `Set A, B\nNot(Intersecting(A, B))\nAutoLabel All`;
const twoSubsets = `Set A, B\nIsSubset(B, A)\nAutoLabel All`;
// compile and optimize both states
const state1 = await compileTrio({
substance: twosets,
style: vennStyle,
domain: setDomain,
variation: "cross-instance state1",
variation: "cross-instance state0",
});
const state2 = await compileTrio({
substance: twoSubsets,
style: vennStyle,
domain: setDomain,
variation: "cross-instance state2",
variation: "cross-instance state1",
});
if (state1.isOk() && state2.isOk()) {
const state1Done = stepUntilConvergence(await prepareState(state1.value));
Expand Down
20 changes: 18 additions & 2 deletions packages/core/src/shapes/Equation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FloatV } from "src/types/value.js";
import * as ad from "../types/ad.js";
import {
Center,
Expand All @@ -17,7 +18,10 @@ export interface EquationProps<T>
Center<T>,
Rect<T>,
Rotate<T>,
String<T> {}
String<T> {
ascent: FloatV<T>;
descent: FloatV<T>;
}

export const sampleEquation = (
context: Context,
Expand Down Expand Up @@ -48,9 +52,21 @@ export const sampleEquation = (
stages: new Set(),
})
),
descent: floatV(
context.makeInput({
init: { tag: "Pending", pending: 0 },
stages: new Set(),
})
),
ascent: floatV(
context.makeInput({
init: { tag: "Pending", pending: 0 },
stages: new Set(),
})
),
rotation: floatV(0),
string: strV("defaultLabelText"),
fontSize: strV("12pt"),
fontSize: strV("16px"),
ensureOnCanvas: boolV(true),
});

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface EquationData {
tag: "EquationData";
width: FloatV<number>;
height: FloatV<number>;
descent: FloatV<number>;
ascent: FloatV<number>;
rendered: HTMLElement;
}

Expand Down
117 changes: 87 additions & 30 deletions packages/core/src/utils/CollectLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ import { FloatV } from "../types/value.js";
import { Result, err, ok } from "./Error.js";
import { getAdValueAsString, getValueAsShapeList, safe } from "./Util.js";

// to re-scale baseline
const EX_CONSTANT = 10;

export const mathjaxInit = (): ((
input: string,
fontSize: string
input: string
) => Result<HTMLElement, string>) => {
// https://github.com/mathjax/MathJax-demos-node/blob/master/direct/tex2svg
// const adaptor = chooseAdaptor();
Expand All @@ -44,20 +40,14 @@ export const mathjaxInit = (): ((
const svg = new SVG({ fontCache: "none" });
const html = mathjax.document("", { InputJax: tex, OutputJax: svg });

const convert = (
input: string,
fontSize: string
): Result<HTMLElement, string> => {
// HACK: workaround for newlines
const convert = (input: string): Result<HTMLElement, string> => {
// HACK: workaround for newlines. This workaround will force MathJax to always return the same heights regardless of the text content.
// https://github.com/mathjax/MathJax/issues/2312#issuecomment-538185951
const newline_escaped = `\\displaylines{${input}}`;
// https://github.com/mathjax/MathJax-src/blob/master/ts/core/MathDocument.ts#L689
// https://github.com/mathjax/MathJax-demos-node/issues/3#issuecomment-497524041
// if(input) {
// const newline_escaped = `\\displaylines{${input}}`;
// }
try {
const node = html.convert(newline_escaped, { ex: EX_CONSTANT });
// Not sure if this call does anything:
// https://github.com/mathjax/MathJax-src/blob/master/ts/adaptors/liteAdaptor.ts#L523
adaptor.setStyle(node, "font-size", fontSize);
const node = html.convert(input, {});
return ok(node.firstChild);
} catch (error: any) {
return err(error.message);
Expand All @@ -70,14 +60,43 @@ type Output = {
body: HTMLElement;
width: number;
height: number;
descent: number;
ascent: number;
};

const parseFontSize = (
fontSize: string
): { number: number; unit: string } | undefined => {
const regex = /^(\d+(?:\.\d+)?)\s*(px|in|cm|mm)$/;
const match = fontSize.match(regex);

if (!match) {
return;
}

const number = parseFloat(match[1]);
const unit = match[2];

return { number, unit };
};

// Convert from a font size in absolute unit (px, in, cm, mm) to pixels
const toPxFontSize = (number: number, unit: string): number => {
const inPX: { [unit: string]: number } = {
px: 1,
in: 96, // 96 px to an inch
cm: 96 / 2.54, // 2.54 cm to an inch
mm: 96 / 25.4, // 10 mm to a cm
};
return inPX[unit] * number;
};

/**
* Call MathJax to render __non-empty__ labels.
*/
const tex2svg = async (
properties: Equation<ad.Num>,
convert: (input: string, fontSize: string) => Result<HTMLElement, string>
convert: (input: string) => Result<HTMLElement, string>
): Promise<Result<Output, string>> =>
new Promise((resolve) => {
const contents = getAdValueAsString(properties.string, "");
Expand All @@ -93,30 +112,60 @@ const tex2svg = async (
}

// Render the label
const output = convert(contents, fontSize);
const output = convert(contents);
if (output.isErr()) {
resolve(err(`MathJax could not render $${contents}$: ${output.error}`));
return;
}

const body = output.value;

const viewBox = body.getAttribute("viewBox");
if (viewBox === null) {
resolve(err(`No ViewBox found for MathJax output $${contents}$`));
return;
}

// Get the rendered viewBox dimensions
const viewBoxArr = viewBox.split(" ");
const width = parseFloat(viewBoxArr[2]);
const height = parseFloat(viewBoxArr[3]);

// Get re-scaled dimensions of label according to
// https://github.com/mathjax/MathJax-src/blob/32213009962a887e262d9930adcfb468da4967ce/ts/output/svg.ts#L248
const vAlignFloat = parseFloat(body.style.verticalAlign) * EX_CONSTANT;
const constHeight = parseFloat(fontSize) - vAlignFloat;
const scaledWidth = (constHeight / height) * width;
// all viewbox units are divided by 1000 because MathJax scaled them by 1000
// these viewbox props are in em units * 1000
const viewBoxArr = viewBox.split(" ");
const width = parseFloat(viewBoxArr[2]) / 1000;
const height = parseFloat(viewBoxArr[3]) / 1000;

// the vertical align adjustment and height in ex unit. This is used to avoid dealing with ex to px conversion
const d = -parseFloat(body.style.verticalAlign);
const exH = parseFloat(body.getAttribute("height")!);

resolve(ok({ body, width: scaledWidth, height: constHeight }));
// em is really the pixel value of the font size
const parsedFontSize = parseFontSize(fontSize);
if (parsedFontSize) {
const { number, unit } = parsedFontSize;
const em_to_px = (n: number) => n * toPxFontSize(number, unit);
const scaledWidth = em_to_px(width);
const scaledHeight = em_to_px(height);
const scaledD = (d / exH) * scaledHeight;
const scaledDescent = scaledD;
const scaledAscent = scaledHeight - scaledDescent; // HACK: interpreting ascent to be height - descent, which might be very wrong

resolve(
ok({
body,
width: scaledWidth,
height: scaledHeight,
descent: scaledDescent,
ascent: scaledAscent,
})
);
} else {
resolve(
err(
'Invalid font size format. Only "px", "in", "cm", and "mm" units are supported.'
)
);
return;
}
});

const floatV = (contents: number): FloatV<number> => ({
Expand All @@ -140,11 +189,15 @@ const textData = (
const equationData = (
width: number,
height: number,
ascent: number,
descent: number,
rendered: HTMLElement
): EquationData => ({
tag: "EquationData",
width: floatV(width),
height: floatV(height),
ascent: floatV(ascent),
descent: floatV(descent),
rendered,
});

Expand Down Expand Up @@ -183,7 +236,7 @@ export const toFontRule = <T>(properties: Text<T>): string => {
// https://stackoverflow.com/a/44564236
export const collectLabels = async (
allShapes: Shape<ad.Num>[],
convert: (input: string, fontSize: string) => Result<HTMLElement, string>
convert: (input: string) => Result<HTMLElement, string>
): Promise<Result<LabelCache, PenroseError>> => {
const labels: LabelCache = new Map();
for (const s of allShapes) {
Expand All @@ -199,13 +252,15 @@ export const collectLabels = async (
});
}

const { body, width, height } = svg.value;
const { body, width, height, ascent, descent } = svg.value;

// Instead of directly overwriting the properties, cache them temporarily
// NOTE: in the case of empty strings, `tex2svg` returns infinity sometimes. Convert to 0 to avoid NaNs in such cases.
const label: EquationData = equationData(
width === Infinity ? 0 : width,
height === Infinity ? 0 : height,
ascent,
descent,
body
);
labels.set(shapeName, label);
Expand Down Expand Up @@ -319,6 +374,8 @@ const insertPendingHelper = (
);
setPendingProperty(xs, inputs, s.width, labelData.width);
setPendingProperty(xs, inputs, s.height, labelData.height);
setPendingProperty(xs, inputs, s.ascent, labelData.ascent);
setPendingProperty(xs, inputs, s.descent, labelData.descent);
} else if (s.shapeType === "Text") {
const labelData = safe(labelCache.get(s.name.contents), "missing label");
if (labelData.tag !== "TextData")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Global {

-- some additional parameters to get consistent styling throughout
scalar lineThickness = 1.5
scalar fontSize = "4.5px"
scalar fontSize = "9px"
string fontFamily = "Linux Libertine"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const {
textPadding2 = 25.0
repelWeight = 0.7 -- TODO: Reverted from 0.0
repelWeight2 = 0.5
fontSize = "22pt"
fontSize = "25px"
containPadding = 50.0
rayLength = 100.0
pointRadius = 4.0
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/src/geometry-domain/euclidean.style
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const {
textPadding2 = 25.0
repelWeight = 0.7 -- TODO: Reverted from 0.0
repelWeight2 = 0.5
fontSize = "13.5px"
fontSize = "20px"
containPadding = 50.0
rayLength = 100.0
pointRadius = 4.0
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/src/group-theory/CayleyGraph.style
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ forall Element g
shape g.labelText = Equation {
string: g.label
center: g.icon.center
fontSize: "0.35px"
fontSize: "5px"
fillColor: colors.darkGray
}
}
Expand Down

0 comments on commit 93d30f5

Please sign in to comment.