lecture 1
This commit is contained in:
parent
527a7587bb
commit
d3fda0f2f4
5 changed files with 576 additions and 4 deletions
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
|||
use flake
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.direnv
|
||||
.*.aux
|
||||
*.pdf
|
168
Lecture1.typ
Normal file
168
Lecture1.typ
Normal file
|
@ -0,0 +1,168 @@
|
|||
#import "prooftree.typ": *
|
||||
#import "@preview/showybox:2.0.1": showybox
|
||||
#set page(width: 5.6in, height: 9in, margin: 0.4in)
|
||||
|
||||
= Type theory crash course
|
||||
|
||||
== MLTT + Sets
|
||||
|
||||
Important features in MLTT:
|
||||
|
||||
#let Nat = $sans("Nat")$
|
||||
#let Vect = $sans("Vect")$
|
||||
|
||||
- Dependent types and functions.
|
||||
e.g $ "concatenate": Pi_(m,n:"Nat") Vect(m) -> Vect(n) -> Vect(m+n) $
|
||||
- Function arrows always associate to the right.
|
||||
- All functions are total.
|
||||
|
||||
Goal: to write well-typed programs. (implementing an algorithm and proving a mathematical statement are the same)
|
||||
|
||||
=== Judgements
|
||||
|
||||
$ "context" tack.r "conclusion" $
|
||||
|
||||
#let defeq = $equiv$
|
||||
|
||||
#table(
|
||||
columns: 2,
|
||||
stroke: gray,
|
||||
$Gamma$, [sequence of variable declarations (contexts are always well-formed)],
|
||||
$Gamma tack.r A$, [$A$ is well-formed *type* in context $Gamma$],
|
||||
$Gamma tack.r a : A$, [*term* $a$ is well-formed and of type $A$],
|
||||
$Gamma tack.r A defeq B$, [types $A$ and $B$ are *convertible*],
|
||||
$Gamma tack.r a defeq b : A$, [$a$ is convertible to $b$ in type $A$],
|
||||
)
|
||||
|
||||
Example:
|
||||
|
||||
$ "isZero?"& : Nat -> "Bool" \
|
||||
"isZero?"& (n) :defeq "??"
|
||||
$
|
||||
|
||||
At this point, looking for $(n: Nat) tack.r "isZero?"(n) : "Bool"$.
|
||||
|
||||
=== Inference rules
|
||||
|
||||
#prooftree(
|
||||
axiom($J_1$),
|
||||
axiom($J_2$),
|
||||
axiom($J_3$),
|
||||
rule(n: 3, $J$),
|
||||
)
|
||||
|
||||
For example:
|
||||
|
||||
#prooftree(
|
||||
axiom($Gamma tack.r a defeq b : A$),
|
||||
rule($Gamma tack.r b defeq a : A$),
|
||||
)
|
||||
|
||||
#let subst(name, replacement, expr) = $#expr [ #replacement \/ #name ]$
|
||||
|
||||
=== Interpreting types as sets
|
||||
|
||||
You can interpret types as sets, where $a : A$ is interpreted as $floor(a) : floor(A)$.
|
||||
|
||||
- Univalent mathematics can _not_ be interpreted as sets. There are extra axioms that breaks the interpretation.
|
||||
- The judgement $a : A$ cannot be proved or disproved.
|
||||
- For ex. 2 of natural numbers and 2 of integers can be converted but are inherently different values.
|
||||
|
||||
=== Convertibility
|
||||
|
||||
If $x : A$ and $A defeq B$, then $x : B$. We are thinking of these types as literally the same.
|
||||
|
||||
If $a defeq a'$ then $f @ a defeq f @ a'$.
|
||||
|
||||
=== Declaring types and terms
|
||||
|
||||
4 types of rules:
|
||||
|
||||
#let typeIntroTable(formation, introduction, elimination, computation) = table(
|
||||
columns: 2,
|
||||
stroke: 0in,
|
||||
[#text(fill: blue, [Formation])], [#formation],
|
||||
[#text(fill: blue, [Introduction])], [#introduction],
|
||||
[#text(fill: blue, [Elimination])], [#elimination],
|
||||
[#text(fill: blue, [Computation])], [#computation],
|
||||
)
|
||||
|
||||
#typeIntroTable(
|
||||
[a way to construct a new type],
|
||||
[way to construct *canonical terms* of the type],
|
||||
[how to use a term of the new type to construct terms of other types],
|
||||
[what happens when one does Introduction followed by Elimination],
|
||||
)
|
||||
|
||||
Example (context $Gamma$ are elided):
|
||||
|
||||
#typeIntroTable(
|
||||
[If $A$ and $B$ are types, then $A -> B$ is a type
|
||||
#prooftree(axiom($Gamma tack.r A$), axiom($Gamma tack.r B$), rule(n: 2, $Gamma tack.r A -> B$))],
|
||||
[If $(x : A) tack.r b : B$, then $tack.r lambda (x : A) . b(x) : A -> B$
|
||||
- $b$ is an expression that might involve $x$],
|
||||
[If we have a function $f : A -> B$, and $a : A$, then $f @ a : B$ (or $f(a) : B$)],
|
||||
[What is the result of the application? $(lambda(x : A) . b(x)) @ a defeq subst(a, x, b)$
|
||||
- Substitution $subst(a, x, b)$ is built-in],
|
||||
)
|
||||
|
||||
Questions:
|
||||
|
||||
- *What does the lambda symbol mean?* Lambda is just notation. It could also be written $tack.r "lambda"((x:A), b(x)) : A -> B$.
|
||||
|
||||
Another example: the singleton type
|
||||
|
||||
#let unit = $bb(1)$
|
||||
#let tt = $t t$
|
||||
#let rec = $sans("rec")$
|
||||
|
||||
#typeIntroTable(
|
||||
[$unit$ is a type],
|
||||
[$tt : unit$],
|
||||
[If $x : unit$ and $C$ is a type and $c : C$, then $rec_unit (C, c, x) : C$],
|
||||
[$rec_unit (C, c, t) defeq c$
|
||||
- Interpretation in sets: a one-element set],
|
||||
)
|
||||
|
||||
- Question: *How to construct this using lambda abstraction?*
|
||||
- (Structural rule: having $tack.r c : C$ means $x : unit tack.r c : C$, which by the lambda introduction rule gives us $lambda x.c : unit -> C$)
|
||||
|
||||
Booleans
|
||||
|
||||
#let Bool = $sans("Bool")$
|
||||
#let tru = $sans("true")$
|
||||
#let fls = $sans("false")$
|
||||
|
||||
#typeIntroTable(
|
||||
[$Bool$ is a type],
|
||||
[$tru : Bool$ and $fls : Bool$],
|
||||
[If $x : Bool$ and $C$ is a type and $c, c' : C$, then $rec_Bool (C, c, c', x) : C$],
|
||||
[$rec_Bool (C, c, c', tru) defeq c$ and $rec_Bool (C, c, c', fls) defeq c'$],
|
||||
)
|
||||
|
||||
Empty type
|
||||
|
||||
#let empty = $bb(0)$
|
||||
|
||||
#typeIntroTable(
|
||||
[$empty$ is a type],
|
||||
[_(no introduction rule)_],
|
||||
[If $x : empty$ and $C$ is a type, then $rec_empty (C, x) : C$],
|
||||
[_(no computation rule)_],
|
||||
)
|
||||
|
||||
Natural numbers
|
||||
|
||||
#let zero = $sans("zero")$
|
||||
#let suc = $sans("suc")$
|
||||
|
||||
#typeIntroTable(
|
||||
[$Nat$ is a type],
|
||||
[- $zero : Nat$
|
||||
- If $n : Nat$, then $suc(n) : Nat$],
|
||||
[If $C$ is a type and $c_0:C$ and $c_s:C->C$ and $x: Nat$, then $rec_Nat (C,c_0,c_s,x) : C$],
|
||||
[- $rec_Nat (C, c_0, c_s, zero) defeq c_0$
|
||||
- $rec_Nat (C, c_0, c_s, suc(n)) defeq c_s @ (rec_Nat (C, c_0, c_s, n))$
|
||||
We can define computation rule on naturals using a universal property],
|
||||
)
|
||||
|
1
Test.v
1
Test.v
|
@ -4,4 +4,3 @@ Lemma myfirstlemma : 2 + 2 = 4.
|
|||
Proof.
|
||||
apply idpath.
|
||||
Defined.
|
||||
|
||||
|
|
405
prooftree.typ
Normal file
405
prooftree.typ
Normal file
|
@ -0,0 +1,405 @@
|
|||
#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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue