diff --git a/src/bot/bot.ts b/src/bot/bot.ts index 37fe834..fbee156 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -75,6 +75,7 @@ export class KunaBot { tall: false, tree, renderer: denotationRenderText, + showArrows: false, }), ), ); diff --git a/src/cli.ts b/src/cli.ts index 75e6c24..f8f923a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -134,6 +134,7 @@ yargs tall: argv.semantics, tree: trees[0], renderer: denotationRenderText, + showArrows: false, }); const png = canvas.toBuffer('image/png'); fs.writeFileSync(argv.output as string, png); diff --git a/src/syntax/recover.ts b/src/syntax/recover.ts index be1b472..cb159b2 100644 --- a/src/syntax/recover.ts +++ b/src/syntax/recover.ts @@ -172,6 +172,7 @@ class Scope { class Recoverer { private nextBinding = 0; private nextCoindex = 0; + private nextMovementId = 0; constructor() {} @@ -187,6 +188,14 @@ class Recoverer { return fixSerial(serial, terms, () => this.newCoindex()); } + private move(source: Tree, target: Tree) { + assertLeaf(source); + source.id ??= 'r' + this.nextMovementId++; + assertLeaf(target); + target.id ??= 'r' + this.nextMovementId++; + source.movedTo = target.id; + } + recover(tree: Tree, scope: Scope | undefined): StrictTree { if ('children' in tree) { if (tree.label === '*𝘷P') { @@ -255,6 +264,22 @@ class Recoverer { } } + // v-to-Asp movement + if (tree.label === 'AspP' && right.label === '𝘷P') { + assertBranch(right); + const v = + right.left.label === '𝘷' + ? right.left + : (right.right as Branch).left; + this.move(v, left); + } + + // Asp-to-T movement + if (tree.label === 'TP' && right.label === 'AspP') { + assertBranch(right); + this.move(right.left, left); + } + return fixed; } else { return tree; diff --git a/src/syntax/serial.ts b/src/syntax/serial.ts index ae40b8d..2c27958 100644 --- a/src/syntax/serial.ts +++ b/src/syntax/serial.ts @@ -1,6 +1,15 @@ import { Impossible, Ungrammatical, Unimplemented } from '../core/error'; import { splitNonEmpty } from '../core/misc'; -import { Branch, Label, Leaf, Tree, effectiveLabel, makeNull } from '../tree'; +import { + Branch, + Label, + Leaf, + Tree, + assertBranch, + assertLeaf, + effectiveLabel, + makeNull, +} from '../tree'; /** * Toaq serials are too complicated to parse directly in the context-free @@ -83,6 +92,11 @@ export function getFrame(verb: Tree): string { } } +let _i = 0; +function makeId(): string { + return 's' + _i++; +} + function makevP(verb: Tree, args: Tree[]): Tree { const agent = 'word' in verb && @@ -90,13 +104,15 @@ function makevP(verb: Tree, args: Tree[]): Tree { verb.word.entry?.type === 'predicate' && verb.word.entry.subject === 'agent'; + const v = makeId(); + switch (args.length) { case 0: { if (!('word' in verb)) throw new Impossible('Weird nullary verb'); return { label: '𝘷P', - left: { label: '𝘷', word: { covert: true, value: 'BE' } }, - right: { label: 'VP', word: verb.word }, + left: { label: '𝘷', word: { covert: true, value: 'BE' }, id: v }, + right: { id: makeId(), label: 'VP', word: verb.word, movedTo: v }, }; } case 1: { @@ -109,15 +125,19 @@ function makevP(verb: Tree, args: Tree[]): Tree { left: subject, right: { label: "𝘷'", - left: { label: '𝘷', word: { covert: true, value: 'CAUSE' } }, - right: { label: 'VP', word: verb.word }, + left: { label: '𝘷', word: { covert: true, value: 'CAUSE' }, id: v }, + right: { id: makeId(), label: 'VP', word: verb.word, movedTo: v }, }, }; } else { return { label: '𝘷P', - left: { label: '𝘷', word: { covert: true, value: 'BE' } }, - right: { label: 'VP', left: verb, right: subject }, + left: { label: '𝘷', word: { covert: true, value: 'BE' }, id: v }, + right: { + label: 'VP', + left: { id: makeId(), ...verb, movedTo: v }, + right: subject, + }, }; } } @@ -131,8 +151,13 @@ function makevP(verb: Tree, args: Tree[]): Tree { left: { label: '𝘷', word: { covert: true, value: agent ? 'CAUSE' : 'BE' }, + id: v, + }, + right: { + label: 'VP', + left: { id: makeId(), ...verb, movedTo: v }, + right: directObject, }, - right: { label: 'VP', left: verb, right: directObject }, }, }; } @@ -143,11 +168,15 @@ function makevP(verb: Tree, args: Tree[]): Tree { left: subject, right: { label: "𝘷'", - left: { label: '𝘷', word: { covert: true, value: 'CAUSE' } }, + left: { label: '𝘷', word: { covert: true, value: 'CAUSE' }, id: v }, right: { label: 'VP', left: indirectObject, - right: { label: "V'", left: verb, right: directObject }, + right: { + label: "V'", + left: { id: makeId(), ...verb, movedTo: v }, + right: directObject, + }, }, }, }; @@ -211,8 +240,17 @@ function serialTovP( args.push(makeNull('DP')); } const innerArgs: Tree[] = [...pros, ...args.slice(cCount)]; - args.push(serialTovP(verbs.slice(1), innerArgs, newCoindex)); - return makevP(verbs[0], args); + const v0 = verbs[0]; + assertLeaf(v0); + const vP = serialTovP(verbs.slice(1), innerArgs, newCoindex); + assertBranch(vP); + const v = vP.left.label === '𝘷' ? vP.left : (vP.right as Branch).left; + assertLeaf(v); + v.id ??= makeId(); + v0.id ??= makeId(); + v.movedTo = v0.id; + args.push(vP); + return makevP(v0, args); } } diff --git a/src/tree/draw.ts b/src/tree/draw.ts index 403f827..a9bce77 100644 --- a/src/tree/draw.ts +++ b/src/tree/draw.ts @@ -33,6 +33,7 @@ class TreeDrawer { constructor( private theme: Theme, private layerHeight: number, + private showArrows: boolean, ) { const width = 8400; const height = 4400; @@ -167,12 +168,12 @@ class TreeDrawer { this.ctx.beginPath(); const start = this.locations.get(i)!; const end = this.locations.get(j)!; - const x0 = start.x - start.width / 2 - 15; - const y0 = start.y; + const x0 = start.x - start.width / 2; + const y0 = Math.max(end.y + 50, start.y + 20); const x1 = end.x; - const y1 = end.y + 25; + const y1 = end.y + 20; this.ctx.moveTo(x0, y0); - this.ctx.quadraticCurveTo(x1, y0, x1, y1); + this.ctx.bezierCurveTo((x0 + x1) / 2, y0 + 40, x1, y0, x1, y1); this.ctx.stroke(); for (const dx of [-8, 8]) { this.drawLine(x1, y1, x1 + dx, y1 + 8); @@ -183,7 +184,7 @@ class TreeDrawer { private fitCanvasToContents() { const { minX, maxX, minY, maxY } = this.extent; const cropWidth = maxX - minX; - const cropHeight = maxY - minY; + const cropHeight = maxY - minY + 30; const temp = this.ctx.getImageData(minX, minY, cropWidth, cropHeight); this.canvas.width = cropWidth; this.canvas.height = cropHeight; @@ -202,7 +203,7 @@ class TreeDrawer { const placed = placer.placeTree(tree); this.drawTree(this.rootX, this.rootY, placed); await Promise.all(this.promises); - this.drawArrows(); + if (this.showArrows) this.drawArrows(); this.fitCanvasToContents(); return this.canvas; } @@ -216,9 +217,10 @@ export function drawTreeToCanvas(options: { denotation: CompactExpr, theme: Theme, ) => RenderedDenotation; + showArrows: boolean; }): Promise { - const { theme, tall, tree, renderer } = options; + const { theme, tall, tree, renderer, showArrows } = options; const layerHeight = tall ? 150 : 100; - const drawer = new TreeDrawer(themes[theme], layerHeight); + const drawer = new TreeDrawer(themes[theme], layerHeight, showArrows); return drawer.drawToCanvas(tree, renderer); } diff --git a/src/web/Main.tsx b/src/web/Main.tsx index 55da7f2..3845132 100644 --- a/src/web/Main.tsx +++ b/src/web/Main.tsx @@ -134,7 +134,6 @@ export function Main(props: MainProps) { ); case 'png-latex': case 'png-text': { - const theme = darkMode.isDarkMode ? 'dark' : 'light'; const baseRenderer = treeFormat === 'png-latex' ? denotationRenderLatex @@ -144,8 +143,13 @@ export function Main(props: MainProps) { ? (e: CompactExpr, t: Theme) => baseRenderer(compactDenotation(e), t) : baseRenderer; - const tall = mode.includes('semantics'); - drawTreeToCanvas({ theme, tall, tree, renderer }).then(canvas => { + drawTreeToCanvas({ + theme: darkMode.isDarkMode ? 'dark' : 'light', + tall: mode.includes('semantics'), + tree, + renderer, + showArrows: mode === 'syntax-tree', + }).then(canvas => { setTimeout(() => { if (treeImg.current) { treeImg.current.src = canvas.toDataURL();