#let prooftree( spacing: ( horizontal: 1em, vertical: 0.5em, lateral: 0.5em, ), label: ( // TODO: split offset into horizontal and vertical offset: -0.1em, side: left, padding: 0.2em, ), line-stroke: 0.5pt, ..rules ) = context { // Check parameters and compute normalized settings let settings = { // Check basic validity of `rules`. if rules.pos().len() == 0 { panic("The `rules` argument cannot be empty.") } // Check the types of the parameters. assert( type(spacing) == "dictionary", message: "The value `" + repr(spacing) + "` of the `spacing` argument was expected" + "to have type `dictionary` but instead had type `" + type(spacing) + "`." ) assert( type(label) == "dictionary", message: "The value `" + repr(label) + "` of the `label` argument was expected" + "to have type `dictionary` but instead had type `" + type(label) + "`." ) assert( type(line-stroke) == "length", message: "The value `" + repr(line-stroke) + "` of the `line-stroke` argument was expected" + "to have type `length` but instead had type `" + type(line-stroke) + "`." ) // Check validity of `spacing`'s keys. for (key, value) in spacing { if key not in ("horizontal", "vertical", "lateral", "h", "v", "l") { panic("The key `" + key + "` in the `spacing` argument `" + repr(spacing) + "` was not expected.") } if type(value) != "length" { panic( "The value `" + repr(value) + "` of the key `" + key + "` in the `spacing` argument `" + repr(spacing) + "` was expected to have type `length` but instead had type `" + type(value) + "`." ) } } // Check exclusivity of `spacing`'s keys. let mutually_exclusive(key1, key2, keys) = { assert( key1 not in keys or key2 not in keys, message: "The keys `" + key1 + "` and `" + key2 + "` in the `spacing` argument `" + repr(spacing) + "` are mutually exclusive." ) } mutually_exclusive("horizontal", "h", spacing.keys()) mutually_exclusive("vertical", "v", spacing.keys()) mutually_exclusive("lateral", "l", spacing.keys()) // Check validity of `label`'s keys. let expected = ("offset": "length", "side": "alignment", "padding": "length") for (key, value) in label { if key not in expected { panic("The key `" + key + "` in the `label` argument `" + repr(label) + "` was not expected.") } if type(value) != expected.at(key) { panic( "The value `" + repr(value) + "` of the key `" + key + "` in the `label` argument `" + repr(label) + "` was expected to have type `" + type.at(key) + "` but instead had type `" + type(value) + "`." ) } } if "side" in label { assert( label.side == left or label.side == right, message: "The value for the key `side` in the argument `label` can only be either " + "`left` (default) or `right`, but instead was `" + repr(label.side) + "`." ) } ( spacing: ( horizontal: spacing.at("horizontal", default: spacing.at("h", default: 1.5em)).to-absolute(), vertical: spacing.at("vertical", default: spacing.at("v", default: 0.5em)).to-absolute(), lateral: spacing.at("lateral", default: spacing.at("l", default: 0.5em)).to-absolute(), ), label: ( offset: label.at("offset", default: -0.1em).to-absolute(), side: label.at("side", default: left), padding: label.at("padding", default: 0.2em).to-absolute(), ), line-stroke: line-stroke.to-absolute(), ) } // Holds the current "pending" rules, i.e. those without a parent let stack = () // Holds all the measures let layouts = () // First pass: compute the layout of each rule given the one of its children for (i, rule) in rules.pos().enumerate() { let to_pop = rule.__prooftree_to_pop let measure_func = rule.__prooftree_measure_func assert( to_pop <= stack.len(), message: "The rule `" + repr(rule.__prooftree_raw) + "` was expecting at least " + str(to_pop) + " rules in the stack, but only " + str(stack.len()) + " were present." ) // Remove the children from the stack let children = stack.slice(stack.len() - to_pop) stack = stack.slice(0, stack.len() - to_pop) // Compute the layout and push let layout = measure_func(i, settings, children) stack.push(layout) layouts.push(layout) } assert( stack.len() == 1, message: "Some rule remained unmatched: " + str(stack.len()) + " roots were found but only 1 was expected." ) let last = stack.pop() let content = { let offsets = range(rules.pos().len()).map(_ => (0pt, 0pt)) // Second pass: backward draw each rule and compute offset of children for (i, rule) in rules.pos().enumerate().rev() { let (dx, dy) = offsets.at(i) let layout = layouts.at(i) // Update the offsets of the children for (j, cdx, cdy) in layout.at("children_offsets", default: ()) { offsets.at(j) = (dx + cdx, dy + cdy) } // Draw at the correct offset let draw_func = rule.__prooftree_draw_func place(left + bottom, dx: dx, dy: -dy, draw_func(settings, layout)) } } block(width: last.width, height: last.height, content) } #let axiom(label: none, body) = { // Check arguments { // Check the type of `label`. assert( type(label) in ("string", "content", "none"), message: "The type of the `label` argument `" + repr(label) + "` was expected to be " + "`none`, `string` or `content` but was instead `" + type(label) + "`." ) } // TODO: allow the label to be aligned on left, right or center (default and current). ( __prooftree_raw: body, __prooftree_to_pop: 0, __prooftree_measure_func: (i, settings, children) => { // Compute the size of the body let body_size = measure(body) let body_width = body_size.width.to-absolute() let body_height = body_size.height.to-absolute() // Compute width of the base (including space) let base_width = body_width + 2 * settings.spacing.lateral // Update layout if a label is present let (width, height) = (base_width, body_height) let base_side = 0pt let (label_left, label_bottom) = (0pt, 0pt) if label != none { // Compute the size of the label let label_size = measure(label) let label_width = label_size.width let label_height = label_size.height // Update width and offsets from the left width = calc.max(base_width, label_width) base_side = (width - base_width) / 2 label_left = (width - label_width) / 2 // Compute bottom offset and update height label_bottom = height + 1.5 * settings.spacing.vertical height = label_bottom + label_height } return ( index: i, width: width, height: height, base_left: base_side, base_right: base_side, main_left: base_side, main_right: base_side, // Extra for draw body_left: base_side + settings.spacing.lateral, label_left: label_left, label_bottom: label_bottom, ) }, __prooftree_draw_func: (settings, l) => { // Draw body place(left + bottom, dx: l.body_left, body) // Draw label if label != none { place(left + bottom, dx: l.label_left, dy: -l.label_bottom, label) } } ) } #let rule( n: 1, label: none, root ) = { // Check arguments { // Check validity of the `n` parameter assert( type(n) == "integer", message: "The type of the `n` argument `" + repr(n) + "` was expected to be " + "`integer` but was instead `" + type(n) + "`." ) // Check the type of `label`. assert( type(label) in ("string", "dictionary", "content", "none"), message: "The type of the `label` argument `" + repr(label) + "` was expected to be " + "`none`, `string`, `content` or `dictionary` but was instead `" + type(label) + "`." ) // If the type of `label` was string then it's good, otherwise we need to check its keys. if type(label) == "dictionary" { for (key, value) in label { // TODO: maybe consider allowing `top`, `top-left` and `top-right` if `rule(n: 0)` gets changed. if key not in ("left", "right") { panic("The key `" + key + "` in the `label` argument `" + repr(label) + "` was not expected.") } if type(value) not in ("string", "content") { panic( "The value `" + repr(value) + "` of the key `" + key + "` in the `label` argument `" + repr(label) + "` was expected to have type `string` or `content` but instead had type `" + type(value) + "`." ) } } } } ( __prooftree_raw: root, __prooftree_to_pop: n, __prooftree_measure_func: (i, settings, children) => { let width(it) = measure(it).width.to-absolute() let height(it) = measure(it).height.to-absolute() let label = label if type(label) == "none" { label = (left: none, right: none) } if type(label) in ("string", "content") { label = ( left: if settings.label.side == left { label } else { none }, right: if settings.label.side == right { label } else { none } ) } label = ( left: label.at("left", default: none), right: label.at("right", default: none), ) // Size of root let root_width = width(root) let root_height = height(root) // Width of base, which includes spacing as well let base_width = 2 * settings.spacing.lateral + root_width // Bottom offset of the line and children let line_bottom = root_height + settings.spacing.vertical let children_bottom = line_bottom + settings.spacing.vertical // Left/right offset of bases of extreme children let (child_base_left, child_base_right) = (0pt, 0pt) if n != 0 { child_base_left = children.first().base_left child_base_right = children.last().base_right } // Width and height of children, and width of their combined bases let children_width = children .map(c => c.width) .intersperse(settings.spacing.horizontal) .sum() let children_height = children.map(c => c.height).fold(0pt, calc.max) let children_base_width = children_width - child_base_left - child_base_right // Width of the line let line_width = calc.max(children_base_width, base_width) // Left/right offsets of lateral children main let (child_main_left, child_main_right) = (0pt, 0pt) if n != 0 { child_main_left = children.first().main_left child_main_right = children.last().main_right } // Offset of bases from line start (same for left/right) let base_from_line = (line_width - base_width) / 2 let children_base_from_line = (line_width - children_base_width) / 2 // Space for labels let (label_left_width, label_right_width) = (0pt, 0pt) let (label_left_height, label_right_height) = (0pt, 0pt) if label.left != none { label_left_width = width(label.left) + settings.label.padding label_left_height = height(label.left) } if label.right != none { label_right_width = width(label.right) + settings.label.padding label_right_height = height(label.right) } // Left/right offsets of line = max of labels and children main let line_left = calc.max(label_left_width, child_base_left - children_base_from_line) let line_right = calc.max(label_right_width, child_base_right - children_base_from_line) // Left/right offsets of base let base_left = line_left + base_from_line let base_right = line_right + base_from_line // Left/right offsets of children let children_left = line_left + children_base_from_line - child_base_left let children_right = line_right + children_base_from_line - child_base_right // Left/right offsets of main let main_left = calc.min(line_left, children_left + child_main_left) let main_right = calc.min(line_right, children_right + child_main_right) // Full width and height let width = line_left + line_width + line_right let height = children_bottom + children_height // Incrementally compute the relative offset of each child let children_offsets = () for c in children { children_offsets.push((c.index, children_left, children_bottom)) children_left += c.width + settings.spacing.horizontal } ( index: i, width: width, height: height, base_left: base_left, base_right: base_right, main_left: main_left, main_right: main_right, children_offsets: children_offsets, // Extra for draw label: label, root_left: base_left + settings.spacing.lateral, line_left: line_left, line_bottom: line_bottom, line_width: line_width, label_left: line_left - label_left_width, label_right: line_left + line_width + settings.label.padding, label_left_bottom: root_height + settings.spacing.vertical + settings.line-stroke / 2 - label_left_height / 2 - settings.label.offset, label_right_bottom: root_height + settings.spacing.vertical + settings.line-stroke / 2 - label_right_height / 2 - settings.label.offset, ) }, __prooftree_draw_func: (settings, l) => { // Draw root content place(left + bottom, dx: l.root_left, root) // Draw line place(left + bottom, dx: l.line_left, dy: -l.line_bottom, line(length: l.line_width, stroke: settings.line-stroke)) // Draw labels if l.label.left != none { place(left + bottom, dx: l.label_left, dy: -l.label_left_bottom, l.label.left) } if l.label.right != none { place(left + bottom, dx: l.label_right, dy: -l.label_right_bottom, l.label.right) } } ) }