diff --git a/src/bot/bot.ts b/src/bot/bot.ts index 53070a1..2c417a8 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -71,7 +71,7 @@ export class KunaBot { const canvases = await Promise.all( trees.map(tree => drawTreeToCanvas({ - theme: 'dark', + themeName: 'dark', tall: false, tree, renderer: denotationRenderText, diff --git a/src/cli.ts b/src/cli.ts index 95d5960..c1ceeee 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -144,7 +144,7 @@ yargs } const theme = argv.light ? 'light' : 'dark'; const canvas = await drawTreeToCanvas({ - theme, + themeName: theme, tall: argv.semantics, tree: trees[0], renderer: denotationRenderText, diff --git a/src/tree/draw.ts b/src/tree/draw.ts index c73a148..ff7b528 100644 --- a/src/tree/draw.ts +++ b/src/tree/draw.ts @@ -18,6 +18,13 @@ interface Location { width: number; } +interface TreeDrawerOptions { + theme: Theme; + layerHeight: number; + showMovement: boolean; + truncateLabels: string[]; +} + class TreeDrawer { private margin = 40; private font = '27px Noto Sans Math, Noto Sans'; @@ -30,17 +37,16 @@ class TreeDrawer { private locations: Map = new Map(); private arrows: Array<[MovementID, MovementID]> = []; private promises: Array> = []; + get theme(): Theme { + return this.options.theme; + } - constructor( - private theme: Theme, - private layerHeight: number, - private showMovement: boolean, - ) { + constructor(private options: TreeDrawerOptions) { const width = 8400; const height = 4400; this.canvas = createCanvas(width, height); this.ctx = this.canvas.getContext('2d'); - this.ctx.fillStyle = theme.backgroundColor; + this.ctx.fillStyle = options.theme.backgroundColor; this.ctx.fillRect(0, 0, width, height); this.ctx.font = this.font; this.ctx.textAlign = 'center'; @@ -55,7 +61,7 @@ class TreeDrawer { } private drawLine(x1: number, y1: number, x2: number, y2: number): void { - this.ctx.strokeStyle = this.theme.textColor; + this.ctx.strokeStyle = this.options.theme.textColor; this.ctx.lineWidth = 1; this.ctx.beginPath(); this.ctx.moveTo(x1, y1); @@ -114,10 +120,10 @@ class TreeDrawer { } const [wordColor, word] = - this.showMovement && tree.movement && tree.movement.text + this.options.showMovement && tree.movement && tree.movement.text ? [this.theme.movedWordColor, tree.movement.text] : [ - this.showMovement && tree.movement?.movedTo + this.options.showMovement && tree.movement?.movedTo ? this.theme.traceColor : this.theme.wordColor, tree.word, @@ -125,10 +131,18 @@ class TreeDrawer { if (word !== undefined) { const dy = 35 + (tree.denotation?.height(this.ctx) ?? 0); - this.drawLine(x, y + dy, x, y + this.layerHeight - 15); - this.drawText(word, x, y + this.layerHeight, wordColor); + const y1 = y + this.options.layerHeight - 15; + if (tree.roof) { + this.drawLine(x, y + dy, x - tree.width / 2, y1); + this.drawLine(x, y + dy, x + tree.width / 2, y1); + this.drawLine(x - tree.width / 2, y1, x + tree.width / 2, y1); + } else { + this.drawLine(x, y + dy, x, y1); + } + + this.drawText(word, x, y + this.options.layerHeight, wordColor); if (tree.word && tree.gloss) { - const yg = y + this.layerHeight + 30; + const yg = y + this.options.layerHeight + 30; this.drawText(tree.gloss, x, yg, this.theme.textColor); } } @@ -155,9 +169,9 @@ class TreeDrawer { const n = tree.children.length; for (let i = 0; i < n; i++) { const dx = (i - (n - 1) / 2) * tree.distanceBetweenChildren; - this.drawTree(x + dx, y + this.layerHeight, tree.children[i]); + this.drawTree(x + dx, y + this.options.layerHeight, tree.children[i]); const dy = 35 + (tree.denotation?.height(this.ctx) ?? 0); - this.drawLine(x, y + dy, x + dx, y + this.layerHeight - 15); + this.drawLine(x, y + dy, x + dx, y + this.options.layerHeight - 15); } } @@ -220,18 +234,21 @@ class TreeDrawer { theme: Theme, ) => RenderedDenotation, ): Promise { - const placer = new TreePlacer(this.ctx, this.theme, renderer); + const placer = new TreePlacer(this.ctx, renderer, { + theme: this.theme, + truncateLabels: this.options.truncateLabels, + }); const placed = placer.placeTree(tree); this.drawTree(this.rootX, this.rootY, placed); await Promise.all(this.promises); - if (this.showMovement) this.drawArrows(); + if (this.options.showMovement) this.drawArrows(); this.fitCanvasToContents(); return this.canvas; } } export function drawTreeToCanvas(options: { - theme: ThemeName; + themeName: ThemeName; tall: boolean; tree: Tree | DTree; renderer: ( @@ -239,9 +256,14 @@ export function drawTreeToCanvas(options: { theme: Theme, ) => RenderedDenotation; showMovement: boolean; + truncateLabels: string[]; }): Promise { - const { theme, tall, tree, renderer, showMovement } = options; - const layerHeight = tall ? 150 : 100; - const drawer = new TreeDrawer(themes[theme], layerHeight, showMovement); - return drawer.drawToCanvas(tree, renderer); + const layerHeight = options.tall ? 150 : 100; + const drawer = new TreeDrawer({ + theme: themes[options.themeName], + layerHeight, + showMovement: options.showMovement, + truncateLabels: options.truncateLabels, + }); + return drawer.drawToCanvas(options.tree, options.renderer); } diff --git a/src/tree/functions.ts b/src/tree/functions.ts index 0038825..9d6dd4d 100644 --- a/src/tree/functions.ts +++ b/src/tree/functions.ts @@ -85,6 +85,12 @@ export function leafText(tree: Tree): string { if (!('word' in tree)) { throw new Impossible('Unexpected non-leaf ' + tree.label); } + if (tree.movement && tree.movement.text) { + return tree.movement.text; + } + if (tree.movement && tree.movement.movedTo) { + return ''; + } if (tree.word.covert) return ''; return tree.word.text; } diff --git a/src/tree/movement.ts b/src/tree/movement.ts index 16ae8e0..4c56f9d 100644 --- a/src/tree/movement.ts +++ b/src/tree/movement.ts @@ -39,8 +39,8 @@ export function makeMovement(): Movement { export function moveUp(source: Leaf, target: Leaf) { source.movement ??= makeMovement(); target.movement ??= makeMovement(); - source.movement.movedTo = target.movement.id; const sourceText = source.movement.text ?? leafText(source); + source.movement.movedTo = target.movement.id; const text = (leafText(target) + ' ' + sourceText).trim(); target.movement.text = repairTones(text); source.movement.text = undefined; diff --git a/src/tree/place.test.ts b/src/tree/place.test.ts index 0396d1a..8c6bf7f 100644 --- a/src/tree/place.test.ts +++ b/src/tree/place.test.ts @@ -13,7 +13,7 @@ function summarize(tree: PlacedTree): any { test('it places trees', () => { const measureText = (text: string) => ({ width: text.length * 20 }); - const placer = new TreePlacer({ measureText }, themes.light, undefined!); + const placer = new TreePlacer({ measureText }, undefined!); const tree = parse('gaı jí máq rú hao jí')[0]; expect(summarize(placer.placeTree(tree))).toMatchInlineSnapshot(` { diff --git a/src/tree/place.ts b/src/tree/place.ts index ae020d3..fff2029 100644 --- a/src/tree/place.ts +++ b/src/tree/place.ts @@ -1,7 +1,7 @@ import { CanvasRenderingContext2D } from 'canvas'; import { DTree, Expr } from '../semantics/model'; import { toPlainText, toLatex, typeToPlainText } from '../semantics/render'; -import { Branch, Leaf, Rose, Tree } from '../tree'; +import { Branch, Leaf, Rose, Tree, treeText } from '../tree'; import { CompactExpr, compact } from '../semantics/compact'; import { mathjax } from 'mathjax-full/js/mathjax'; @@ -10,7 +10,7 @@ import { SVG } from 'mathjax-full/js/output/svg'; import { AllPackages } from 'mathjax-full/js/input/tex/AllPackages'; import { liteAdaptor } from 'mathjax-full/js/adaptors/liteAdaptor'; import { RegisterHTMLHandler } from 'mathjax-full/js/handlers/html'; -import { Theme } from './theme'; +import { Theme, themes } from './theme'; import { Movement } from './movement'; const adaptor = liteAdaptor(); @@ -57,6 +57,7 @@ interface PlacedLeafBase { denotation?: RenderedDenotation; movement?: Movement; coindex?: string; + roof?: boolean; } interface HasWord { @@ -184,16 +185,31 @@ function layerExtents( return extents; } +interface TreePlacerOptions { + theme: Theme; + horizontalMargin: number; + truncateLabels: string[]; +} + +const defaultOptions: TreePlacerOptions = { + theme: themes.light, + horizontalMargin: 30, + truncateLabels: [], +}; + export class TreePlacer { + private options: TreePlacerOptions; + constructor( private ctx: C, - private theme: Theme, private denotationRenderer: ( denotation: Expr, theme: Theme, ) => RenderedDenotation, - private horizontalMargin: number = 30, - ) {} + options: Partial = {}, + ) { + this.options = { ...defaultOptions, ...options }; + } private placeLeaf( leaf: Leaf | (Leaf & { denotation: Expr | null }), @@ -203,7 +219,7 @@ export class TreePlacer { const word = leaf.word.covert ? leaf.word.value : leaf.word.text; const denotation = 'denotation' in leaf && leaf.denotation !== null - ? this.denotationRenderer(leaf.denotation, this.theme) + ? this.denotationRenderer(leaf.denotation, this.options.theme) : undefined; const width = Math.max( this.ctx.measureText(label).width, @@ -222,6 +238,29 @@ export class TreePlacer { }; } + private placeRoof(tree: Tree | DTree): PlacedLeaf { + const label = getLabel(tree); + const denotation = + 'denotation' in tree && tree.denotation !== null + ? this.denotationRenderer(tree.denotation, this.options.theme) + : undefined; + const text = treeText(tree); + + const width = Math.max( + this.ctx.measureText(label).width, + this.ctx.measureText(text ?? '').width, + denotation ? denotation.width(this.ctx) : 0, + ); + return { + width, + label, + word: text, + gloss: undefined, + denotation, + roof: true, + }; + } + private makePlacedBranch( label: string, denotation: RenderedDenotation | undefined, @@ -239,7 +278,7 @@ export class TreePlacer { for (let j = 0; j < Math.min(r.length, l.length); j++) { distanceBetweenChildren = Math.max( distanceBetweenChildren, - l[j].right - r[j].left + this.horizontalMargin, + l[j].right - r[j].left + this.options.horizontalMargin, ); } } @@ -257,7 +296,7 @@ export class TreePlacer { ): PlacedBranch { const denotation = 'denotation' in branch && branch.denotation !== null - ? this.denotationRenderer(branch.denotation, this.theme) + ? this.denotationRenderer(branch.denotation, this.options.theme) : undefined; const children = [ this.placeTree(branch.left), @@ -272,10 +311,12 @@ export class TreePlacer { } public placeTree(tree: Tree | DTree): PlacedTree { - return 'word' in tree - ? this.placeLeaf(tree) - : 'children' in tree - ? this.placeRose(tree) - : this.placeBranch(tree); + if ('word' in tree) { + return this.placeLeaf(tree); + } else if (this.options.truncateLabels.includes(tree.label)) { + return this.placeRoof(tree); + } else { + return 'children' in tree ? this.placeRose(tree) : this.placeBranch(tree); + } } } diff --git a/src/tree/theme.ts b/src/tree/theme.ts index edfef38..f9d3d42 100644 --- a/src/tree/theme.ts +++ b/src/tree/theme.ts @@ -16,7 +16,7 @@ export const themes: Record = { denotationColor: '#FF4466', wordColor: '#99EEFF', movedWordColor: '#FF99EE', - traceColor: '#9999FF', + traceColor: '#DCDDDE', }, light: { backgroundColor: '#FFFFFF', @@ -24,6 +24,6 @@ export const themes: Record = { denotationColor: '#FF0000', wordColor: '#3399FF', movedWordColor: '#FF3399', - traceColor: '#AAAAFF', + traceColor: '#000000', }, }; diff --git a/src/web/App.css b/src/web/App.css index ae9e289..ed379da 100644 --- a/src/web/App.css +++ b/src/web/App.css @@ -213,8 +213,10 @@ h1 { margin-top: 8px; } -.toggles label select { - margin-inline-start: 8px; +.toggles label { + display: inline-block; + margin-top: 4px; + margin-inline-end: 8px; } .parses { diff --git a/src/web/Main.tsx b/src/web/Main.tsx index 54d5457..c147104 100644 --- a/src/web/Main.tsx +++ b/src/web/Main.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import _ from 'lodash'; +import _, { truncate } from 'lodash'; import { ReactElement, useEffect, useRef, useState } from 'react'; import { useDarkMode, useLocalStorage } from 'usehooks-ts'; @@ -76,6 +76,7 @@ export function Main(props: MainProps) { false, ); + const [advanced, setAdvanced] = useLocalStorage('advanced', false); const [latestMode, setLatestMode] = useState(props.mode); const [latestOutput, setLatestOutput] = useState( <>Output will appear here., @@ -84,6 +85,7 @@ export function Main(props: MainProps) { const treeImg = useRef(null); const [meaningCompact, setMeaningCompact] = useState(false); const [trimmed, setTrimmed] = useState(false); + const [truncateLabels, setTruncateLabels] = useState(''); const [treeFormat, setTreeFormat] = useState('png-latex'); useEffect(() => { @@ -144,11 +146,12 @@ export function Main(props: MainProps) { baseRenderer(compactDenotation(e), t) : baseRenderer; drawTreeToCanvas({ - theme: darkMode.isDarkMode ? 'dark' : 'light', + themeName: darkMode.isDarkMode ? 'dark' : 'light', tall: mode.includes('semantics'), tree, renderer, showMovement: mode === 'syntax-tree', + truncateLabels: truncateLabels.trim().split(/[\s,]+/), }).then(canvas => { setTimeout(() => { if (treeImg.current) { @@ -278,7 +281,7 @@ export function Main(props: MainProps) { )} {!props.input && ( -
+