├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .typstignore
├── CHANGELOG.md
├── Justfile
├── LICENSE
├── README.md
├── docs
├── assets
│ ├── example-dark.svg
│ ├── example.svg
│ ├── example.typ
│ ├── finite-logo-dark.svg
│ ├── finite-logo.svg
│ ├── finite-logo.typ
│ └── flaci-export.json
├── manual.pdf
└── manual.typ
├── scripts
├── link
├── package
├── setup
└── uninstall
├── src
├── cmd.typ
├── draw.typ
├── finite.typ
├── flaci.typ
├── layout.typ
└── util.typ
├── tbump.toml
├── tests
├── .gitignore
├── .ignore
├── accepts
│ ├── .gitignore
│ ├── diff
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ ├── out
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ ├── ref
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ └── test.typ
├── flaci-load
│ ├── .gitignore
│ ├── DEA-1.json
│ ├── DEA-2.json
│ ├── DEA-3.json
│ ├── NEA-1.json
│ ├── NKA-1.json
│ ├── diff
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ ├── out
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ ├── ref
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ └── test.typ
├── large-automaton
│ ├── diff
│ │ └── 1.png
│ ├── out
│ │ └── 1.png
│ ├── ref
│ │ └── 1.png
│ └── test.typ
├── layout-circular
│ ├── .gitignore
│ ├── ref
│ │ └── 1.png
│ └── test.typ
├── layouts
│ ├── .gitignore
│ ├── diff
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ └── 6.png
│ ├── out
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ └── 6.png
│ ├── ref
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ └── 6.png
│ └── test.typ
├── spec
│ ├── .gitignore
│ └── test.typ
├── state-anchors
│ ├── .gitignore
│ ├── diff
│ │ ├── 1.png
│ │ └── 2.png
│ ├── out
│ │ ├── 1.png
│ │ └── 2.png
│ ├── ref
│ │ ├── 1.png
│ │ └── 2.png
│ └── test.typ
├── state-final
│ ├── .gitignore
│ ├── ref
│ │ └── 1.png
│ └── test.typ
├── state-initial
│ ├── .gitignore
│ ├── ref
│ │ ├── 1.png
│ │ └── 2.png
│ └── test.typ
├── state-labels
│ ├── diff
│ │ ├── 1.png
│ │ └── 2.png
│ ├── out
│ │ ├── 1.png
│ │ └── 2.png
│ ├── ref
│ │ ├── 1.png
│ │ └── 2.png
│ └── test.typ
├── state
│ ├── .gitignore
│ ├── ref
│ │ ├── 1.png
│ │ └── 2.png
│ └── test.typ
├── test-utils.typ
├── transition-labels
│ ├── .gitignore
│ ├── diff
│ │ ├── 1.png
│ │ └── 2.png
│ ├── out
│ │ ├── 1.png
│ │ └── 2.png
│ ├── ref
│ │ ├── 1.png
│ │ └── 2.png
│ └── test.typ
├── transition
│ ├── .gitignore
│ ├── diff
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ ├── out
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ ├── ref
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ └── test.typ
└── ttable
│ ├── .gitignore
│ ├── ref
│ ├── 1.png
│ ├── 2.png
│ └── 3.png
│ └── test.typ
└── typst.toml
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 |
8 | jobs:
9 | tests:
10 | strategy:
11 | # run tests on all versions, even if one fails
12 | fail-fast: false
13 | matrix:
14 | # add any other Typst versions that your package should support
15 | typst-version:
16 | #- typst: 0.12
17 | # tytanic: 0.1
18 | - typst: 0.13
19 | tytanic: 0.2
20 | # the docs don't need to build with all versions supported by the package;
21 | # the latest one is enough
22 | doc: false
23 |
24 | name: Test for ${{ matrix.typst-version.typst }} (Tytanic ${{ matrix.typst-version.tytanic }}${{ matrix.typst-version.doc && ', with docs' }})
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 |
31 | - name: Install just and tytanic
32 | uses: taiki-e/install-action@v2
33 | with:
34 | tool: just,tytanic@${{ matrix.typst-version.tytanic }}
35 |
36 | - name: Setup typst
37 | id: setup-typst
38 | uses: typst-community/setup-typst@v3
39 | with:
40 | typst-version: ${{ matrix.typst-version.typst }}
41 |
42 | - name: Run test suite
43 | run: just test
44 |
45 | - name: Archive test results
46 | uses: actions/upload-artifact@v4
47 | if: always()
48 | with:
49 | name: typst-${{ steps.setup-typst.outputs.typst-version }}-test-results
50 | path: |
51 | tests/**/diff/*.png
52 | tests/**/out/*.png
53 | tests/**/ref/*.png
54 | retention-days: 5
55 |
56 | - name: Build docs
57 | if: ${{ matrix.typst-version.doc }}
58 | run: just doc
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Thumbs.db
3 |
--------------------------------------------------------------------------------
/.typstignore:
--------------------------------------------------------------------------------
1 | # this is not a "standard" ignore file, it's specific to this template's `scripts/package` script
2 | # list any files here that should not be uploaded to Universe when releasing this package
3 |
4 | # if you are used to ignore files, be aware that .typstignore is a bit more limited:
5 | # - only this file is used; .typstignore files in subdirectories are not considered
6 | # - patterns must match file/directory names from the beginning: `x.typ` will not match `src/x.typ`
7 | # - `*` in patterns works, but also matches directory separators: `*.typ` _will_ match `src/x.typ`
8 | # .git and .typstignore are excluded automatically
9 |
10 | .github
11 | scripts
12 | tests
13 | Justfile
14 | # PDF manuals should be included so that they can be linked, but not their sources
15 | docs/*
16 | !docs/*.pdf
17 | !docs/assets
18 | docs/assets/*
19 | !docs/assets/*.png
20 | !docs/assets/*.svg
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### Version 0.5.0
4 |
5 | - :warning: New layout system:
6 | - Layouts are no longer CeTZ groups and only are used as a parameter to `#automaton`.
7 | - :warning: Breaking style changes:
8 | - Changed label color attribute from `color` to `fill` for consistency.
9 | - The default for `state.label.fill` and `transition.label.fill` is now `none` and sets the color of the label to `stroke.paint`.
10 | - The default value for `transition.curve` is now `1.0` for an easier understanding of the influence on the curvature.
11 | - Added `flaci` module to load and display files exported from [FLACI](http://flaci.com):
12 | - `flaci.load` loads a FLACI file as a `finite` automaton specification.
13 | - `flaci.automaton` displays a FLACI file as a `finite` automaton.
14 |
15 | ### Version 0.4.2
16 |
17 | - Fixed label rotation for transitions.
18 | - Bumped dependencies:
19 | - CeTZ 0.3.0 :arrow upper right: 0.3.1
20 | - t4t 0.3.1 :arrow upper right: 0.4.0
21 |
22 | ### Version 0.4.1
23 |
24 | - Fixes error in `#powerset` function (#8).
25 |
26 | ### Version 0.4.0
27 |
28 | :warning: Version 0.4.0 is a major rewrite of finite to make it compatible with Typst 0.12 and CeTZ 0.3.0. This includes some minor breaking changes, mostly in how layouts work, but overall functionality should be the same.
29 |
30 | ### Version 0.3.2
31 |
32 | - Fixed an issue with final states not being recognized properly (#5)
33 |
34 | ### Version 0.3.1
35 |
36 | - Added styling options for initial states:
37 | - `stroke` sets a stroke for the marking.
38 | - `scale` scales the marking by a factor.
39 | - Updated manual.
40 |
41 | ### Version 0.3.0
42 |
43 | - Bumped `tools4typst` to v0.3.2.
44 | - Introducing automaton specs as a data structure.
45 | - Changes to `automaton` command:
46 | - Changed `label-format` argument to `state-format` and `input-format`.
47 | - `layout` can now take a dictionary with (`state`: `coordinate`) pairs to position states.
48 | - Added `#powerset` command, to transform a NFA into a DFA.
49 | - Added `#add-trap` command, to complete a partial DFA.
50 | - Added `#accepts` command, to test a word against an NFA or DFA.
51 | - Added `transpose-table` and `get-inputs` utilities.
52 | - Added "Start" label to the mark for initial states.
53 | - Added option to modify the mark label for initial states.
54 | - Added anchor option for loops, to position the loop at one of the eight default anchors.
55 | - Changed `curve` option to be the height of the arc of the transition.
56 | - This makes styling more consistent over longer distances.
57 | - Added `rest` key to custom layouts.
58 |
59 | ### Version 0.2.0
60 |
61 | - Bumped CeTZ to v0.1.1.
62 |
63 | ### Version 0.1.0
64 |
65 | - Initial release submitted to [typst/packages](https://github.com/typst/packages).
66 |
--------------------------------------------------------------------------------
/Justfile:
--------------------------------------------------------------------------------
1 | root := justfile_directory()
2 | package-fork := x'${TYPST_PKG_FORK:-}'
3 | export TYPST_ROOT := root
4 |
5 | [private]
6 | default:
7 | @just --list --unsorted
8 |
9 | # build assets
10 | assets:
11 | typst compile docs/assets/example.typ docs/assets/example.svg
12 | typst compile --input theme=dark docs/assets/example.typ docs/assets/example-dark.svg
13 | typst compile docs/assets/finite-logo.typ docs/assets/finite-logo.svg
14 | typst compile --input theme=dark docs/assets/finite-logo.typ docs/assets/finite-logo-dark.svg
15 |
16 | # generate manual
17 | doc: assets
18 | typst compile docs/manual.typ docs/manual.pdf
19 |
20 | # run test suite
21 | test *args:
22 | tt run {{ args }}
23 |
24 | # update test cases
25 | update *args:
26 | tt update {{ args }}
27 |
28 | # package the library into the specified destination folder
29 | package target:
30 | ./scripts/package "{{ target }}"
31 |
32 | # install the library with the "@local" prefix
33 | install: (package "@local")
34 |
35 | # install the library with the "@preview" prefix (for pre-release testing)
36 | install-preview: (package "@preview")
37 |
38 | [private]
39 | [working-directory(x'${TYPST_PKG_FORK:-.}')]
40 | prepare-fork:
41 | git checkout main
42 | git pull typst main
43 | git push origin --force
44 |
45 | prepare: prepare-fork (package package-fork)
46 |
47 | # create a symbolic link to this library in the target repository
48 | link target="@local":
49 | ./scripts/link "{{ target }}"
50 |
51 | link-preview: (link "@preview")
52 |
53 | [private]
54 | remove target:
55 | ./scripts/uninstall "{{ target }}"
56 |
57 | # uninstalls the library from the "@local" prefix
58 | uninstall: (remove "@local")
59 |
60 | # uninstalls the library from the "@preview" prefix (for pre-release testing)
61 | uninstall-preview: (remove "@preview")
62 |
63 | # unlinks the library from the "@local" prefix
64 | unlink: (remove "@local")
65 |
66 | # unlinks the library from the "@preview" prefix
67 | unlink-preview: (remove "@preview")
68 |
69 | # run ci suite
70 | ci: test doc
71 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-2025 Jonas Neugebauer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | [](https://typst.app/universe/package/finite)
11 | [](https://github.com/lilaq-project/lilaq/blob/main/LICENSE)
12 | [](https://github.com/jneug/typst-finite/actions/workflows/tests.yml)
13 |
14 |
15 |
16 | **finite** is a [Typst](https://github.com/typst/typst) package for rendering finite automata.
17 |
18 | ---
19 |
20 | ## Usage
21 |
22 | Import the package from the Typst preview repository:
23 |
24 | ```typst
25 | #import "@preview/finite:0.5.0": automaton
26 | ```
27 |
28 | After importing the package, simply call `#automaton()` with a dictionary holding a transition table:
29 | ```typst
30 | #import "@preview/finite:0.5.0": automaton
31 |
32 | #automaton(
33 | (
34 | q0: (q1: 0, q0: "0,1"),
35 | q1: (q0: (0, 1), q2: "0"),
36 | q2: none,
37 | ),
38 | initial: "q1",
39 | final: ("q0",),
40 | )
41 | ```
42 |
43 | The output should look like this:
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ## Further documentation
54 |
55 | See [manual.pdf](docs/manual.pdf) for a full manual of the package.
56 |
57 | See the [changelog](CHANGELOG.md) for recent changes.
58 |
--------------------------------------------------------------------------------
/docs/assets/example.svg:
--------------------------------------------------------------------------------
1 |
259 |
--------------------------------------------------------------------------------
/docs/assets/example.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ": automaton
2 |
3 | #set page(width: auto, height: auto, margin: 1cm, fill: none)
4 |
5 | #let theme = (
6 | text: black,
7 | bg: white,
8 | )
9 | #if sys.inputs.at("theme", default: "light") == "dark" {
10 | theme.text = rgb("#d6d1cd")
11 | theme.bg = rgb("#343534")
12 | }
13 |
14 | #set text(theme.text) if theme == "dark"
15 |
16 | #automaton(
17 | // @typstyle off
18 | (
19 | q0: (q1: 0, q0: "0,1"),
20 | q1: (q0: (0, 1), q2: "0"),
21 | q2: none,
22 | ),
23 | initial: "q1",
24 | final: ("q0",),
25 | style: (
26 | transition: (stroke: theme.text),
27 | state: (fill: theme.bg, stroke: theme.text),
28 | ),
29 | )
30 |
--------------------------------------------------------------------------------
/docs/assets/finite-logo-dark.svg:
--------------------------------------------------------------------------------
1 |
219 |
--------------------------------------------------------------------------------
/docs/assets/finite-logo.svg:
--------------------------------------------------------------------------------
1 |
219 |
--------------------------------------------------------------------------------
/docs/assets/finite-logo.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #let theme = (
4 | text: black,
5 | bg: white,
6 | )
7 | #if sys.inputs.at("theme", default: "light") == "dark" {
8 | theme.text = rgb("#d6d1cd")
9 | theme.bg = rgb("#343534")
10 | }
11 |
12 | #set page(width: auto, height: auto, margin: 5mm, fill: none)
13 | #set text(font: "Liberation Sans")
14 |
15 | #finite.automaton(
16 | (
17 | q0: (q1: none),
18 | q1: (q2: none),
19 | q2: (q3: none),
20 | q3: (q4: none),
21 | q4: (q5: none),
22 | q5: none,
23 | ),
24 | labels: range(6).fold(
25 | (:),
26 | (d, i) => {
27 | d.insert("q" + str(i), "FINITE".at(i))
28 | d
29 | },
30 | ),
31 | style: (
32 | state: (label: (size: 20pt), fill: theme.bg, stroke: theme.text),
33 | transition: (curve: .8, stroke: theme.text),
34 | q0: (initial: ""),
35 | ),
36 | layout: finite.layout.linear.with(spacing: .4),
37 | )
38 |
--------------------------------------------------------------------------------
/docs/assets/flaci-export.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TEIL2: Wort mit gerader Anzahl an Nullen",
3 | "description": "",
4 | "type": "DEA",
5 | "automaton": {
6 | "acceptCache": [],
7 | "simulationInput": [],
8 | "Alphabet": [
9 | "0",
10 | "1",
11 | "2",
12 | "3",
13 | "4",
14 | "5",
15 | "6",
16 | "7",
17 | "8",
18 | "9"
19 | ],
20 | "StackAlphabet": [
21 | "|"
22 | ],
23 | "States": [
24 | {
25 | "ID": 1,
26 | "Name": "q0",
27 | "x": 220,
28 | "y": 130,
29 | "Final": false,
30 | "Radius": 30,
31 | "Transitions": [
32 | {
33 | "Source": 1,
34 | "Target": 1,
35 | "x": 0,
36 | "y": -150,
37 | "Labels": [
38 | "1",
39 | "2",
40 | "3",
41 | "4",
42 | "5",
43 | "6",
44 | "7",
45 | "8",
46 | "9"
47 | ]
48 | },
49 | {
50 | "Source": 1,
51 | "Target": 2,
52 | "x": 0,
53 | "y": -80,
54 | "Labels": [
55 | "0"
56 | ]
57 | }
58 | ],
59 | "Start": true
60 | },
61 | {
62 | "ID": 2,
63 | "Name": "q1",
64 | "x": 510,
65 | "y": 130,
66 | "Final": true,
67 | "Radius": 30,
68 | "Transitions": [
69 | {
70 | "Source": 2,
71 | "Target": 2,
72 | "x": 0,
73 | "y": -150,
74 | "Labels": [
75 | "1",
76 | "2",
77 | "3",
78 | "4",
79 | "5",
80 | "6",
81 | "7",
82 | "8",
83 | "9"
84 | ]
85 | },
86 | {
87 | "Source": 2,
88 | "Target": 1,
89 | "x": 0,
90 | "y": -50,
91 | "Labels": [
92 | "0"
93 | ]
94 | }
95 | ],
96 | "Start": false
97 | }
98 | ],
99 | "lastInputs": []
100 | },
101 | "GUID": "hiicmv2p1"
102 | }
103 |
--------------------------------------------------------------------------------
/docs/manual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/docs/manual.pdf
--------------------------------------------------------------------------------
/docs/manual.typ:
--------------------------------------------------------------------------------
1 |
2 | #import "@preview/mantys:1.0.2": *
3 |
4 | #import "../src/finite.typ" as finite: cetz
5 |
6 | #import "../src/util.typ"
7 |
8 | // Customize the show-module function
9 | #let show-module(
10 | name,
11 | scope: (:),
12 | outlined: false,
13 | ..tidy-args,
14 | ) = tidy-module(
15 | name,
16 | read("../src/" + name + ".typ"),
17 | show-outline: outlined,
18 | // scope: scope,
19 | // include-examples-scope: true,
20 | // extract-headings: 3,
21 | ..tidy-args,
22 | )
23 |
24 | // Nice display of CeTZ commands
25 | #show "CETZ": _ => package[CeTZ]
26 | #let cetz-cmd = cmd.with(module: "cetz")
27 | #let cetz-cmd- = cmd-.with(module: "cetz")
28 | #let cetz-draw = cmd.with(module: "cetz.draw")
29 | #let cetz-draw- = cmd-.with(module: "cetz.draw")
30 |
31 | #show: mantys(
32 | ..toml("../typst.toml"),
33 |
34 | title: align(
35 | center,
36 | image("assets/finite-logo.svg", width: 100%),
37 | ),
38 |
39 | date: datetime.today(),
40 | abstract: [
41 | FINITE is a Typst package to draw transition diagrams for finite automata (finite state machines) with the power of CETZ.
42 |
43 | The package provides commands to quickly draw automata from a transition table but also lets you manually create and customize transition diagrams on any CETZ canvas.
44 | ],
45 |
46 | examples-scope: (
47 | scope: (
48 | cetz: cetz,
49 | finite: finite,
50 | util: util,
51 | automaton: finite.automaton,
52 | ),
53 | ),
54 |
55 | theme: themes.modern,
56 |
57 | git: git-info(file => read(file)),
58 |
59 | assets: (
60 | "assets/finite-logo.svg": "assets/finite-logo.typ",
61 | ),
62 | )
63 |
64 | = Usage
65 |
66 | == Importing the package
67 |
68 | Import the package in your Typst file:
69 |
70 | #show-import(imports: "automaton")
71 |
72 | == Manual installation
73 |
74 | The package can be downloaded and saved into the system dependent local package repository.
75 |
76 | Either download the current release from #github("jneug/typst-finite") and unpack the archive into your system dependent local repository folder#footnote[#std.link("https://github.com/typst/packages#local-packages")] or clone it directly:
77 |
78 | #show-git-clone()
79 |
80 | In either case, make sure the files are placed in a subfolder with the correct version number: #context document.use-value("package", p => raw(block: false, p.name + "/" + str(p.version)))
81 |
82 | After installing the package, just import it inside your `typ` file:
83 |
84 | #show-import(repository: "@local", imports: "automaton")
85 |
86 | == Dependencies
87 |
88 | FINITE loads #link("https://github.com/johannes-wolf/typst-canvas")[CETZ] and the utility package #link("https://github.com/jneug/typst-tools4typst")[#package[t4t]] from the `preview` package repository. The dependencies will be downloaded by Typst automatically on first compilation.
89 |
90 | #custom-type("coordinate", color: silver)Whenever a #dtype("coordinate") type is referenced, a CETZ coordinate can be used. Please refer to the CETZ manual for further information on coordinate systems.
91 |
92 | = Drawing automata
93 |
94 | FINITE helps you draw transition diagrams for finite automata in your Typst documents, using the power of CETZ.
95 |
96 | To draw an automaton, simply import #cmd[automaton] from FINITE and use it like this:
97 | #example[```typ
98 | #automaton((
99 | q0: (q1:0, q0:"0,1"),
100 | q1: (q0:(0,1), q2:"0"),
101 | q2: none,
102 | ))
103 | ```]
104 |
105 | As you can see, an automaton ist defined by a dictionary of dictionaries. The keys of the top-level dictionary are the names of states to draw. The second-level dictionaries use the names of connected states as keys and transition labels as values.
106 |
107 | In the example above, the states `q0`, `q1` and `q2` are defined. `q0` is connected to `q1` and has a loop to itself. `q1` transitions to `q2` and back to `q0`. #cmd-[automaton] selected the first state in the dictionary (in this case `q0`) to be the initial state and the last (`q2`) to be a final state.
108 |
109 | See @aut-specs for more details on how to specify automata.
110 |
111 | To modify the layout and style of the transition diagram, #cmd-[automaton] accepts a set of options:
112 | #example(breakable: true)[```typ
113 | #automaton(
114 | (
115 | q0: (q1:0, q0:"0,1"),
116 | q1: (q0:(0,1), q2:"0"),
117 | q2: (),
118 | ),
119 | initial: "q1",
120 | final: ("q0", "q2"),
121 | labels:(
122 | q2: "FIN"
123 | ),
124 | style:(
125 | state: (fill: luma(248), stroke:luma(120)),
126 | transition: (stroke: (dash:"dashed")),
127 | q0-q0: (anchor:top+left),
128 | q1: (initial:top),
129 | q1-q2: (stroke: 2pt + red)
130 | )
131 | )
132 | ```]
133 |
134 | For larger automatons, the states can be arranged in different ways:
135 | #example(breakable: true)[```typ
136 | #let aut = (:)
137 | #for i in range(10) {
138 | let name = "q"+str(i)
139 | aut.insert(name, (:))
140 | if i < 9 {
141 | aut.at(name).insert("q" + str(i + 1), none)
142 | }
143 | }
144 | #automaton(
145 | aut,
146 | layout: finite.layout.circular.with(offset: 45deg),
147 | style: (
148 | transition: (curve: 0),
149 | q0: (initial: top+left)
150 | )
151 | )
152 | ```]
153 |
154 | See @using-layout for more on layouts.
155 |
156 | == Specifing finite automata
157 |
158 | Most of FINITEs commands expect a finite automaton specification ("@type:spec" in short) the first argument. These specifications are dictionaries defining the elements of the automaton.
159 |
160 | If an automaton has only one final state, the spec can simply be a @type:transition-table. In other cases, the specification can explicitly define the various elements.
161 |
162 | #custom-type("transition-table", color: rgb("#57a0be"))
163 | A transition table is a #typ.t.dict with state names as keys and dictionaries as values. The nested dictionaries have state names as keys and the transition inputs / labels as values.
164 |
165 | #codesnippet[```typc
166 | (
167 | q0: (q1: (0, 1), q2: (0, 1)),
168 | q1: (q1: (0, 1), q0: 0, q2: 1),
169 | q2: (q0: 0, q1: (1, 0)),
170 | )
171 | ```]
172 |
173 | #custom-type("spec", color: rgb("#76d6ff"))
174 | A specification (@type:spec) is composed of these keys:
175 | ```typc
176 | (
177 | transitions: (...),
178 | states: (...),
179 | inputs: (...),
180 | initial: "...",
181 | final: (...)
182 | )
183 | ```
184 |
185 | - `transitions` is a dictionary of dictionaries in the format:
186 | ```typc
187 | (
188 | state1: (input1, input2, ...),
189 | state2: (input1, input2, ...),
190 | ...
191 | )
192 | ```
193 | - `states` is an optional array with the names of all states. The keys of `transitions` are used by default.
194 | - `inputs` is an optional array with all input values. The inputs found in `transitions` are used by default.
195 | - `initial` is an optional name of the initial state. The first value in `states` is used by default.
196 | - `final` is an optional array of final states. The last value in `states` is used by default.
197 |
198 | The utility function #cmd(module: "util")[to-spec] can be used to create a full spec from a partial dictionary by filling in the missing values with the defaults.
199 |
200 | == Command reference
201 | #show-module("cmd", sort-functions: false)
202 |
203 | == Styling the output
204 |
205 | As common in CETZ, you can pass general styles for states and transitions to the #cetz-cmd-[set-style] function within a call to #cetz-cmd-[canvas]. The elements functions #cmd-[state] and #cmd-[transition] (see below) can take their respective styling options as arguments to style individual elements.
206 |
207 | #cmd[automaton] takes a #arg[style] argument that passes the given style to the above functions. The example below sets a background and stroke color for all states and draws transitions with a dashed style. Additionally, the state `q1` has the arrow indicating an initial state drawn from above instead from the left. The transition from `q1` to `q2` is highlighted in red.
208 | #example(breakable: true)[```typ
209 | #automaton(
210 | (
211 | q0: (q1:0, q0:"0,1"),
212 | q1: (q0:(0,1), q2:"0"),
213 | q2: (),
214 | ),
215 | initial: "q1",
216 | final: ("q0", "q2"),
217 | style:(
218 | state: (fill: luma(248), stroke:luma(120)),
219 | transition: (stroke: (dash:"dashed")),
220 | q1: (initial:top),
221 | q1-q2: (stroke: 2pt + red)
222 | )
223 | )
224 | ```]
225 |
226 | Every state can be accessed by its name and every transition is named with its initial and end state joined with a dash (`-`), for example `q1-q2`.
227 |
228 | The supported styling options (and their defaults) are as follows:
229 | - states:
230 | / #arg(fill: auto): Background fill for states.
231 | / #arg(stroke: auto): Stroke for state borders.
232 | / #arg(radius: .6): Radius of the states circle.
233 | / #arg(extrude: .88):
234 | - `label`:
235 | / #arg(text: auto): State label.
236 | / #arg(size: 1em): Initial text size for the labels (will be modified to fit the label into the states circle).
237 | / #arg(fill: none): Color of the label text.
238 | / #arg(padding: auto): Padding around the label.
239 | - `initial`:
240 | / #arg(anchor: auto): Anchorpoint to point the initial arrow to.
241 | / #arg(label: auto): Text above the arrow.
242 | / #arg(stroke: auto): Stroke for the arrow.
243 | / #arg(scale: auto): Scale of the arrow.
244 | - transitions
245 | / #arg(curve: 1.0): "Curviness" of transitions. Set to #value(0) to get straight lines.
246 | / #arg(stroke: auto): Stroke for transition lines.
247 | - `label`:
248 | / #arg(text: ""): Transition label.
249 | / #arg(size: 1em): Size for label text.
250 | / #arg(fill: auto): Color for label text.
251 | / #arg(
252 | pos: .5,
253 | ): Position on the transition, between #value(0) and #value(1). #value(0) sets the text at the start, #value(1) at the end of the transition.
254 | / #arg(dist: .33): Distance of the label from the transition.
255 | / #arg(angle: auto): Angle of the label text. #value(auto) will set the angle based on the transitions direction.
256 |
257 | == Using #cmd-(module: "cetz")[canvas]
258 |
259 | The above commands use custom CETZ elements to draw states and transitions. For complex automata, the functions in the #module[draw] module can be used inside a call to #cetz-cmd-[canvas].
260 | #example(breakable: true)[```typ
261 | #cetz.canvas({
262 | import cetz.draw: set-style
263 | import finite.draw: state, transition
264 |
265 | state((0,0), "q0", initial:true)
266 | state((2,1), "q1")
267 | state((4,-1), "q2", final:true)
268 | state((rel:(0, -3), to:"q1.south"), "trap", label:"TRAP", anchor:"north-west")
269 |
270 | transition("q0", "q1", inputs:(0,1))
271 | transition("q1", "q2", inputs:(0))
272 | transition("q1", "trap", inputs:(1), curve:-1)
273 | transition("q2", "trap", inputs:(0,1))
274 | transition("trap", "trap", inputs:(0,1))
275 | })
276 | ```]
277 |
278 | === Element functions
279 | #show-module("draw", sort-functions: false)
280 |
281 | === Anchors
282 |
283 | States and transitions are created in a #cetz-draw[group]. States are drawn with a circle named `state` that can be referenced in the group. Additionally they have a content element named `label` and optionally a line named `initial`. These elements can be referenced inside the group and used as anchors for other CETZ elements. The anchors of `state` are also copied to the state group and are directly accessible.
284 |
285 | #info-alert[
286 | That means setting #arg(anchor: "west") for a state will anchor the state at the `west` anchor of the states circle, not of the bounding box of the group.
287 | ]
288 |
289 | Transitions have an `arrow` (#cetz-draw[line]) and `label` (#cetz-draw[content]) element. The anchors of `arrow` are copied to the group.
290 |
291 | #example(breakable: true)[```typ
292 | #cetz.canvas({
293 | import cetz.draw: circle, line, content
294 | import finite.draw: state, transition
295 |
296 | let magenta = rgb("#dc41f1")
297 |
298 | state((0, 0), "q0")
299 | state((4, 0), "q1", final: true, stroke: magenta)
300 |
301 | transition("q0", "q1", label: $epsilon$)
302 |
303 | circle("q0.north-west", radius: .4em, stroke: none, fill: black)
304 |
305 | let magenta-stroke = 2pt + magenta
306 | circle("q0-q1.label", radius: .5em, stroke: magenta-stroke)
307 | line(
308 | name: "q0-arrow",
309 | (rel: (.6, .6), to: "q1.state.north-east"),
310 | (rel: (.1, .1), to: "q1.state.north-east"),
311 | stroke: magenta-stroke,
312 | mark: (end: ">"),
313 | )
314 | content(
315 | (rel: (0, .25), to: "q0-arrow.start"),
316 | text(fill: magenta, [*very important state*]),
317 | )
318 | })
319 | ``` ]
320 |
321 | == Layouts
322 |
323 | #error-alert[
324 | Layouts changed in FINITE version 0.5 and are no longer compatible with FINITE 0.4 and before.
325 | ]
326 |
327 | Layouts can be passed to @cmd:automaton to position states on the canvas without the need to give specific coordinates for each state. FINITE ships with a bunch of layouts, to accomodate different scenarios.
328 |
329 | === Available layouts
330 | #show-module("layout", sort-functions: false)
331 |
332 | == Utility functions
333 | #show-module("util", outlined: true)
334 |
335 | = Simulating input
336 |
337 | FINITE has a set of functions to simulate, test and view finite automata.
338 |
339 | = FLACI support
340 |
341 | FINITE was heavily inspired by the online app #link("https://flaci.org", "FLACI"). FLACI lets you build automata in a visual online app and export your creations as JSON files. FINITE can import theses files and render the result in your document.
342 |
343 | #warning-alert[FINITE currently only supports DEA and NEA automata.]
344 |
345 | #example[```typ
346 | #finite.flaci.automaton(read("flaci-export.json"))
347 | ```][
348 | #finite.flaci.automaton(read("assets/flaci-export.json"))
349 | ]
350 |
351 | #warning-alert[
352 | *Important* \
353 | Read the FLACI json-file with the #typ.read function, not
354 | the #typ.json function. FLACI exports automatons with a wrong encoding
355 | that prevents Typst from properly loading the file as JSON.
356 | ]
357 |
358 | === FLACI functions
359 | #show-module("flaci", module: "flaci", sort-functions: false)
360 |
361 | // = Working with grammars
362 |
363 | = Doing other stuff with FINITE
364 |
365 | Since transition diagrams are effectively graphs, FINITE could also be used to draw graph structures:
366 | #example[```typ
367 | #cetz.canvas({
368 | import cetz.draw: set-style
369 | import finite.draw: state, transitions
370 |
371 | state((0,0), "A")
372 | state((3,1), "B")
373 | state((4,-2), "C")
374 | state((1,-3), "D")
375 | state((6,1), "E")
376 |
377 | transitions((
378 | A: (B: 1.2),
379 | B: (C: .5, E: 2.3),
380 | C: (B: .8, D: 1.4, E: 4.5),
381 | D: (A: 1.8),
382 | E: (:)
383 | ),
384 | C-E: (curve: -1.2))
385 | })
386 | ```]
387 |
388 | = Showcase
389 |
390 | #example(breakable: true)[```typ
391 | #scale(80%, automaton((
392 | q0: (q1: 0, q2: 0),
393 | q2: (q3: 1, q4: 0),
394 | q4: (q2: 0, q5: 0, q6: 0),
395 | q6: (q7: 1),
396 | q1: (q3: 1, q4: 0),
397 | q3: (q1: 1, q5: 1, q6: 1),
398 | q5: (q7: 1),
399 | q7: ()
400 | ),
401 | layout: finite.layout.group.with(grouping: (
402 | ("q0",),
403 | ("q1", "q2", "q3", "q4", "q5", "q6"),
404 | ("q7",)
405 | ),
406 | spacing: 2,
407 | layout: (
408 | finite.layout.custom.with(positions: (q0: (0, -2))),
409 | finite.layout.grid.with(columns:3, spacing:2.6, position: (2, 1)),
410 | finite.layout.custom.with(positions: (q7: (8, 6)))
411 | )
412 | ),
413 | style: (
414 | transition: (curve: 0),
415 | q1-q3: (curve:1),
416 | q3-q1: (curve:1),
417 | q2-q4: (curve:1),
418 | q4-q2: (curve:1),
419 | q1-q4: (label: (pos:.75)),
420 | q2-q3: (label: (pos:.75, dist:-.33)),
421 | q3-q6: (label: (pos:.75)),
422 | q4-q5: (label: (pos:.75, dist:-.33)),
423 | q4-q6: (curve: 1)
424 | )
425 | ))
426 | ```]
427 |
--------------------------------------------------------------------------------
/scripts/link:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | # licensed under Apache License 2.0
5 |
6 | . "$(dirname "${BASH_SOURCE[0]}")/setup"
7 |
8 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then
9 | echo "link TARGET"
10 | echo ""
11 | echo "Creates a symbolic link from '/' at TARGET "
12 | echo "to this project directory. If TARGET is set to @local or @preview,"
13 | echo "the local Typst package directory will be used so that the package"
14 | echo "gets installed for local use."
15 | echo "The name and version are read from 'typst.toml' in the project root."
16 | echo ""
17 | echo "Local package prefix: $DATA_DIR/typst/package/local"
18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview"
19 | exit 1
20 | fi
21 |
22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")"
23 | echo "Install dir: $TARGET"
24 |
25 | PKGDIR="${TARGET:?}/${PKG_PREFIX:?}"
26 | TARGET="${PKGDIR}/${VERSION:?}"
27 | if [ ! -e "$TARGET" ]; then
28 | mkdir -p "$PKGDIR"
29 |
30 | echo "Linked from: $TARGET"
31 | ln -s "$PWD" "$TARGET"
32 | else
33 | echo "Version already exists: $TARGET"
34 | fi
35 |
--------------------------------------------------------------------------------
/scripts/package:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package
5 | # licensed under Apache License 2.0
6 |
7 | . "$(dirname "${BASH_SOURCE[0]}")/setup"
8 |
9 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then
10 | echo "package TARGET"
11 | echo ""
12 | echo "Packages all relevant files into a directory named '/'"
13 | echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package"
14 | echo "directory will be used so that the package gets installed for local use."
15 | echo "The name and version are read from 'typst.toml' in the project root."
16 | echo ""
17 | echo "Local package prefix: $DATA_DIR/typst/package/local"
18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview"
19 | exit 1
20 | fi
21 |
22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")"
23 | echo "Install dir: $TARGET"
24 |
25 | # ignore rules
26 | readarray -t ignores < <(grep -v '^#' .typstignore | grep '[^[:blank:]]')
27 |
28 | # recursively print all files that are not excluded via .typstignore
29 | function enumerate {
30 | local root="$1"
31 | if [[ -f "$root" ]]; then
32 | echo "$root"
33 | else
34 | local files
35 | readarray -t files < <(find "$root" \
36 | -mindepth 1 -maxdepth 1 \
37 | -not -name .git \
38 | -not -name .typstignore)
39 | # declare -p files >&2
40 |
41 | local f
42 | for f in "${files[@]}"; do
43 | local include
44 | include=1
45 |
46 | local ignore
47 | for ignore in "${ignores[@]}"; do
48 | if [[ "$ignore" =~ ^! ]]; then
49 | ignore="${ignore:1}"
50 | if [[ "$f" == ./$ignore ]]; then
51 | # echo "\"$f\" matched \"!$ignore\"" >&2
52 | include=1
53 | fi
54 | elif [[ "$f" == ./$ignore ]]; then
55 | # echo "\"$f\" matched \"$ignore\"" >&2
56 | include=0
57 | fi
58 | done
59 | if [[ "$include" == 1 ]]; then
60 | enumerate "$f"
61 | fi
62 | done
63 | fi
64 | }
65 |
66 | # List of all files that get packaged
67 | readarray -t files < <(enumerate ".")
68 | # declare -p files >&2
69 |
70 | TMP="$(mktemp -d)"
71 |
72 | for f in "${files[@]}"; do
73 | mkdir -p "$TMP/$(dirname "$f")" 2>/dev/null
74 | cp -r "$ROOT/$f" "$TMP/$f"
75 | done
76 |
77 | TARGET="${TARGET:?}/${PKG_PREFIX:?}/${VERSION:?}"
78 | echo "Packaged to: $TARGET"
79 | if rm -r "${TARGET:?}" 2>/dev/null; then
80 | echo "Overwriting existing version."
81 | fi
82 | mkdir -p "$TARGET"
83 |
84 | # include hidden files by setting dotglob
85 | shopt -s dotglob
86 | mv "$TMP"/* "$TARGET"
87 |
--------------------------------------------------------------------------------
/scripts/setup:
--------------------------------------------------------------------------------
1 | # source this script to prepare some common environment variables
2 |
3 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package
4 | # licensed under Apache License 2.0
5 |
6 | # Local package directories per platform
7 | if [[ "$OSTYPE" == "linux"* ]]; then
8 | DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}"
9 | elif [[ "$OSTYPE" == "darwin"* ]]; then
10 | DATA_DIR="$HOME/Library/Application Support"
11 | else
12 | DATA_DIR="${APPDATA}"
13 | fi
14 |
15 | function read-toml() {
16 | local file="$1"
17 | local key="$2"
18 | # Read a key value pair in the format: = ""
19 | # stripping surrounding quotes.
20 | perl -lne "print \"\$1\" if /^${key}\\s*=\\s*\"(.*)\"/" < "$file"
21 | }
22 |
23 | ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P)/.." # macOS has no realpath
24 | PKG_PREFIX="$(read-toml "$ROOT/typst.toml" "name")"
25 | VERSION="$(read-toml "$ROOT/typst.toml" "version")"
26 |
27 | function resolve-target() {
28 | local target="$1"
29 |
30 | if [[ "$target" == "@local" ]]; then
31 | echo "${DATA_DIR}/typst/packages/local"
32 | elif [[ "$target" == "@preview" ]]; then
33 | echo "${DATA_DIR}/typst/packages/preview"
34 | else
35 | echo "$target"
36 | fi
37 | }
38 |
--------------------------------------------------------------------------------
/scripts/uninstall:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eu
3 |
4 | # adapted from https://github.com/johannes-wolf/cetz/blob/35c0868378cea5ad323cc0d9c2f76de8ed9ba5bd/scripts/package
5 | # licensed under Apache License 2.0
6 |
7 | . "$(dirname "${BASH_SOURCE[0]}")/setup"
8 |
9 | if (( $# < 1 )) || [[ "${1:-}" == "help" ]]; then
10 | echo "uninstall TARGET"
11 | echo ""
12 | echo "Removes the package installed into a directory named '/'"
13 | echo "at TARGET. If TARGET is set to @local or @preview, the local Typst package"
14 | echo "directory will be used so that the package gets installed for local use."
15 | echo "The name and version are read from 'typst.toml' in the project root."
16 | echo ""
17 | echo "Local package prefix: $DATA_DIR/typst/package/local"
18 | echo "Local preview package prefix: $DATA_DIR/typst/package/preview"
19 | exit 1
20 | fi
21 |
22 | TARGET="$(resolve-target "${1:?Missing target path, @local or @preview}")"
23 | echo "Install dir: $TARGET"
24 |
25 | TARGET="${TARGET:?}/${PKG_PREFIX:?}/${VERSION:?}"
26 | echo "Package to uninstall: $TARGET"
27 | if [[ ! -e "${TARGET:?}" ]]; then
28 | echo "Package was not found."
29 | elif rm -r "${TARGET:?}" 2>/dev/null; then
30 | echo "Successfully removed."
31 | else
32 | echo "Removal failed."
33 | fi
34 |
--------------------------------------------------------------------------------
/src/cmd.typ:
--------------------------------------------------------------------------------
1 | #import "./draw.typ"
2 |
3 | #import "./layout.typ" as _layout
4 | #import "./util.typ" as util: cetz
5 |
6 |
7 | /// Creates a full automaton specification (@type:spec) for a finite automaton.
8 | /// The function accepts either a partial specification and
9 | /// adds the missing keys by parsing the available information
10 | /// or takes a @type:transition-table and parses it into a full specification.
11 | ///
12 | /// ```example
13 | /// #finite.create-automaton((
14 | /// q0: (q1: 0, q0: (0,1)),
15 | /// q1: (q0: (0,1), q2: "0"),
16 | /// q2: none,
17 | /// ))
18 | /// ```
19 | ///
20 | /// If any of the keyword arguments are set, they will
21 | /// overwrite the information in #arg[spec].
22 | ///
23 | /// -> automaton
24 | #let create-automaton(
25 | /// Automaton specification.
26 | /// -> spec | transition-table
27 | spec,
28 | /// The list of state names in the automaton. #typ.v.auto uses
29 | /// the keys of #arg[spec].
30 | /// -> array
31 | states: auto,
32 | /// The name of the initial state. #typ.v.auto uses the first
33 | /// key in #arg[spec].
34 | /// -> str
35 | initial: auto,
36 | /// The list of final states. #typ.v.auto uses the last
37 | /// key in #arg[spec].
38 | /// -> array
39 | final: auto,
40 | /// The list of all inputs, the automaton uses. #typ.v.auto
41 | /// uses the inputs provided in #arg[spec].
42 | inputs: auto,
43 | ) = {
44 | // TODO: (jneug) add asserts to react to malicious specs
45 | util.assert.any-type(dictionary, spec)
46 |
47 | // TODO: (jneug) check for duplicate names
48 | if "transitions" not in spec {
49 | spec = (transitions: spec)
50 | }
51 |
52 | // Make sure transition inputs are string arrays
53 | for (state, trans) in spec.transitions {
54 | if util.is-empty(trans) {
55 | trans = (:)
56 | }
57 | for (s, inputs) in trans {
58 | if inputs == none {
59 | trans.at(s) = ()
60 | } else if type(inputs) != array {
61 | trans.at(s) = (str(inputs),)
62 | } else {
63 | trans.at(s) = inputs.map(str)
64 | }
65 | }
66 | spec.transitions.at(state) = trans
67 | }
68 |
69 | // TODO (jneug) validate given states with transitions
70 | if "states" not in spec {
71 | spec.insert(
72 | "states",
73 | util.def.if-auto(
74 | states,
75 | def: spec
76 | .transitions
77 | .pairs()
78 | .fold(
79 | (),
80 | (
81 | a,
82 | (s, t),
83 | ) => {
84 | a.push(s)
85 | return a + t.keys()
86 | },
87 | )
88 | .dedup(),
89 | ),
90 | )
91 | }
92 |
93 |
94 | if "initial" not in spec {
95 | spec.insert(
96 | "initial",
97 | util.def.if-auto(
98 | initial,
99 | def: spec.transitions.keys().first(),
100 | ),
101 | )
102 | }
103 |
104 | // Insert final state
105 | if "final" not in spec {
106 | if util.is-auto(final) {
107 | final = (spec.transitions.keys().last(),)
108 | } else if util.is-none(final) {
109 | final = ()
110 | }
111 | spec.insert("final", final)
112 | }
113 |
114 | if "inputs" not in spec {
115 | if util.is-auto(inputs) {
116 | inputs = util.get-inputs(spec.transitions)
117 | }
118 | spec.insert("inputs", inputs)
119 | } else {
120 | spec.inputs = spec.inputs.map(str).sorted()
121 | }
122 |
123 | if util.is-dea(spec.transitions) {
124 | spec.insert("type", "DEA")
125 | } else {
126 | spec.insert("type", "NEA")
127 | }
128 |
129 | return spec + (finite-spec: true)
130 | }
131 |
132 |
133 | /// Draw an automaton from a specification.
134 | ///
135 | /// #arg[spec] is a dictionary with a specification for a
136 | /// finite automaton. See above for a description of the
137 | /// specification dictionaries.
138 | ///
139 | /// The following example defines three states `q0`, `q1` and `q2`.
140 | /// For the input `0`, `q0` transitions to `q1` and for the inputs `0` and `1` to `q2`.
141 | /// `q1` transitions to `q0` for `0` and `1` and to `q2` for `0`. `q2` has no transitions.
142 | /// #codesnippet[```typ
143 | /// #automaton((
144 | /// q0: (q1:0, q0:(0, 1)),
145 | /// q1: (q0:(0, 1), q2:0),
146 | /// q2: none
147 | /// ))
148 | /// ```]
149 | ///
150 | /// #arg[inital] and #arg[final] can be used to customize the initial and final states.
151 | /// #info-alert[
152 | /// The #arg[inital] and #arg[final] will be removed in future
153 | /// versions in favor of automaton specs.
154 | /// ]
155 | ///
156 | /// -> content
157 | #let automaton(
158 | /// Automaton specification.
159 | /// -> spec
160 | spec,
161 | /// The name of the initial state. For #typ.v.auto, the first state in #arg[spec] is used.
162 | /// -> string | auto | none
163 | initial: auto,
164 | /// A list of final state names. For #typ.v.auto, the last state in #arg[spec] is used.
165 | /// -> string | auto | none
166 | final: auto,
167 | /// A dictionary with custom labels for states and transitions.
168 | /// #example[```
169 | /// #finite.automaton(
170 | /// (q0: (q1:none), q1: (q2:none), q2: none),
171 | /// labels: (
172 | /// q0: [START], q1: $lambda$, q2: [END],
173 | /// q0-q1: $delta$
174 | /// )
175 | /// )
176 | /// ```]
177 | /// -> dictionary
178 | labels: (:),
179 | /// A dictionary with styles for states and transitions.
180 | /// -> dictionary
181 | style: (:),
182 | /// A function #lambda("string", ret:"content") to format state labels.
183 | /// The function will get the states name as a string and should return the final label as #dtype("content").
184 | /// #example[```
185 | /// #finite.automaton(
186 | /// (q0: (q1:none), q1: none),
187 | /// state-format: (label) => upper(label)
188 | /// )
189 | /// ```]
190 | /// -> function
191 | state-format: label => {
192 | let m = label.match(regex(`^(\D+)(\d+)$`.text))
193 | if m != none {
194 | [#m.captures.at(0)#sub(m.captures.at(1))]
195 | } else {
196 | label
197 | }
198 | },
199 | /// A function #lambda("array", ret:"content")
200 | /// to generate transition labels from input values. The functions will be
201 | /// called with the array of inputs and should return the final label for
202 | /// the transition. This is only necessary, if no label is specified.
203 | /// #example[```
204 | /// #finite.automaton(
205 | /// (q0: (q1:(3,0,2,1,5)), q1: none),
206 | /// input-format: (inputs) => inputs.sorted().rev().map(str).join("|")
207 | /// )
208 | /// ```]
209 | /// -> function
210 | input-format: inputs => inputs.map(str).join(","),
211 | /// Either a dictionary with (`state`: `coordinate`)
212 | /// pairs, or a layout function. See below for more information on layouts.
213 | /// #example[```
214 | /// #finite.automaton(
215 | /// (q0: (q1:none), q1: none),
216 | /// layout: (q0: (0,0), q1: (rel:(-2,1)))
217 | /// )
218 | /// ```]
219 | /// -> dictionary | function
220 | layout: _layout.linear,
221 | /// Arguments for #cmd-(module:"cetz")[canvas].
222 | /// -> any
223 | ..canvas-styles,
224 | ) = {
225 | spec = create-automaton(spec, initial: initial, final: final)
226 |
227 | // use a dict with coordinates as custom layout
228 | if util.is-dict(layout) {
229 | layout = _layout.custom.with(positions: layout)
230 | }
231 |
232 | let (coordinates, anchors) = layout(spec, style: style)
233 |
234 | cetz.canvas(
235 | ..canvas-styles,
236 | {
237 | import cetz.draw: set-style
238 | import draw: state, transition
239 |
240 | set-style(..style)
241 |
242 | // Create states
243 | for name in spec.states {
244 | state(
245 | coordinates.at(name, default: ()),
246 | name,
247 | label: labels.at(name, default: state-format(name)),
248 | initial: (name == spec.initial),
249 | final: (name in spec.final),
250 | anchor: anchors.at(name, default: none),
251 | ..style.at(name, default: (:)),
252 | )
253 | }
254 |
255 | // Create transitions
256 | for (from, transitions) in spec.transitions {
257 | if util.is-dict(transitions) {
258 | for (to, inputs) in transitions {
259 | let name = from + "-" + to
260 |
261 | // prepare inputs (may be a string or int)
262 | if inputs == none {
263 | inputs = ()
264 | } else if not util.is-arr(inputs) {
265 | inputs = str(inputs).split(",")
266 | }
267 |
268 | // prepare label
269 | let label = labels.at(
270 | name,
271 | default: input-format(inputs),
272 | )
273 | if util.is-dict(label) and "text" not in label {
274 | label.text = input-format(inputs)
275 | }
276 |
277 | // create transition
278 | transition(
279 | from,
280 | to,
281 | inputs: inputs,
282 | label: label,
283 | ..style.at(name, default: (:)),
284 | )
285 | }
286 | }
287 | }
288 | },
289 | )
290 | }
291 |
292 | /// Displays a transition table for an automaton.
293 | ///
294 | /// #arg[spec] is a @type:spec for a
295 | /// finite automaton.
296 | ///
297 | /// The table will show states in rows and inputs in columns:
298 | /// #example(```
299 | /// #finite.transition-table((
300 | /// q0: (q1: 0, q0: (1,0)),
301 | /// q1: (q0: 1, q2: (1,0)),
302 | /// q2: (q0: 1, q2: 0),
303 | /// ))
304 | /// ```)
305 | ///
306 | /// #info-alert[The #arg[inital] and #arg[final] arguments will be removed
307 | /// in future versions in favor of automaton specs.
308 | /// ]
309 | ///
310 | /// -> content
311 | #let transition-table(
312 | /// Automaton specification.
313 | /// -> spec
314 | spec,
315 | /// The name of the initial state. For #value(auto), the first state in #arg[states] is used.
316 | /// -> str, auto, none
317 | initial: auto,
318 | /// A list of final state names. For #value(auto), the last state in #arg[states] is used.
319 | /// -> array, auto, none
320 | final: auto,
321 | /// A function to format the value in a table cell. The function takes a column and row index and the cell content
322 | /// as a #typ.t.str and generates content: #lambda("int", "int", "str", ret:"content").
323 | /// #example[```
324 | /// #finite.transition-table((
325 | /// q0: (q1: 0, q0: (1,0)),
326 | /// q1: (q0: 1, q2: (1,0)),
327 | /// q2: (q0: 1, q2: 0),
328 | /// ),
329 | /// format: (col, row, value) => if col == 0 and row == 0 {
330 | /// $delta$
331 | /// } else if col == 1 {
332 | /// strong(value)
333 | /// } else [#value]
334 | /// )
335 | /// ```]
336 | /// -> function
337 | format: (col, row, v) => raw(str(v)),
338 | /// Formats a list of states for display in a table cell. The function takes an array of state names and generates a string to be passed to @cmd:transition-table.format:
339 | /// #lambda("array", ret:"str")
340 | /// #example[```
341 | /// #finite.transition-table((
342 | /// q0: (q1: 0, q0: (1,0)),
343 | /// q1: (q0: 1, q2: (1,0)),
344 | /// q2: (q0: 1, q2: 0),
345 | /// ), format-list: (states) => "[" + states.join(" | ") + "]")
346 | /// ```]
347 | /// -> function
348 | format-list: states => states.join(", "),
349 | /// Arguments for #typ.table.
350 | /// -> any
351 | ..table-style,
352 | ) = {
353 | spec = create-automaton(spec, initial: initial, final: final)
354 |
355 | let table-cnt = (
356 | format(0, 0, ""),
357 | )
358 | for (col, input) in spec.inputs.enumerate() {
359 | table-cnt.push(format(col + 1, 0, input))
360 | }
361 |
362 | for (row, (state, transitions)) in spec.transitions.pairs().enumerate() {
363 | table-cnt.push(format(0, row + 1, state))
364 | if util.is-dict(transitions) {
365 | for (i, char) in spec.inputs.enumerate() {
366 | let to = ()
367 | for (name, label) in transitions {
368 | if util.is-str(label) {
369 | label = label.split(",")
370 | }
371 | label = util.def.as-arr(label).map(str)
372 |
373 | if char in label {
374 | to.push(name)
375 | }
376 | }
377 | table-cnt.push(if to == () { "" } else { format(i + 1, row + 1, format-list(to)) })
378 | }
379 | }
380 | }
381 |
382 | table(
383 | columns: 1 + spec.inputs.len(),
384 | fill: (c, r) => if r == 0 or c == 0 {
385 | luma(240)
386 | },
387 | align: center + horizon,
388 | ..table-style,
389 | ..table-cnt
390 | )
391 | }
392 |
393 |
394 | /// Creates a deterministic finite automaton from a nondeterministic one by using powerset construction.
395 | ///
396 | /// See #link("https://en.wikipedia.org/wiki/Powerset_construction")[the Wikipedia article on powerset construction] for further
397 | /// details on the algorithm.
398 | ///
399 | /// #arg[spec] is an automaton @type:spec.
400 | ///
401 | /// -> spec
402 | #let powerset(
403 | /// Automaton specification.
404 | /// -> spec
405 | spec,
406 | /// The name of the initial state. For #typ.v.auto, the first state in #arg[states] is used.
407 | /// -> string | auto | none
408 | initial: auto,
409 | /// A list of final state names. For #typ.v.auto, the last state in #arg[states] is used.
410 | /// -> string | auto | none
411 | final: auto,
412 | /// A function to generate the new state names from a list of states.
413 | /// The function takes an array of strings and returns a string: #lambda("array", ret:"string").
414 | /// -> function
415 | state-format: states => "{" + states.sorted().join(",") + "}",
416 | ) = {
417 | spec = create-automaton(spec, initial: initial, final: final)
418 |
419 | let table = util.transpose-table(spec.transitions)
420 |
421 | let (new-initial, new-final) = (
422 | state-format((spec.initial,)),
423 | (),
424 | )
425 |
426 | let powerset = (:)
427 | let queue = ((spec.initial,),)
428 | while queue.len() > 0 {
429 | let cur = queue.remove(0)
430 | let key = state-format(cur)
431 |
432 | if key not in powerset {
433 | powerset.insert(key, (:))
434 |
435 | if cur.any(s => s in spec.final) {
436 | new-final.push(key)
437 | }
438 |
439 | for inp in spec.inputs {
440 | let trans = ()
441 | for s in cur {
442 | let s-trans = table.at(s)
443 | if inp in s-trans {
444 | trans += s-trans.at(inp)
445 | }
446 | }
447 | trans = trans.dedup().sorted()
448 | powerset.at(key).insert(inp, trans)
449 | queue.push(trans)
450 | }
451 | }
452 | }
453 |
454 | for (s, t) in powerset {
455 | for (i, states) in t {
456 | powerset.at(s).at(i) = state-format(states)
457 | }
458 | }
459 |
460 | return create-automaton(
461 | util.transpose-table(powerset),
462 | initial: new-initial,
463 | final: new-final,
464 | inputs: spec.inputs,
465 | )
466 | }
467 |
468 | /// Adds a trap state to a partial DFA and completes it.
469 | ///
470 | /// Deterministic automata need to specify a transition for every
471 | /// possible input. If those inputs don't transition to another
472 | /// state, a trap-state is introduced that is not final
473 | /// and can't be left by any input. To simplify
474 | /// transition diagrams, these trap-states are usually
475 | /// not drawn. This function adds a trap-state to such a
476 | /// partial automaton and thus completes it.
477 | ///
478 | /// #example[```
479 | /// #finite.transition-table(finite.add-trap((
480 | /// q0: (q1: 0),
481 | /// q1: (q0: 1)
482 | /// )))
483 | /// ```]
484 | /// -> spec
485 | #let add-trap(
486 | /// Automaton specification.
487 | /// -> spec
488 | spec,
489 | /// Name for the new trap-state.
490 | /// -> str
491 | trap-name: "TRAP",
492 | ) = {
493 | spec = create-automaton(spec)
494 |
495 | let table = util.transpose-table(spec.transitions)
496 |
497 | let trap-added = false
498 | for (s, values) in table {
499 | for inp in spec.inputs {
500 | if inp not in values {
501 | values.insert(inp, (trap-name,))
502 | trap-added = true
503 | }
504 | }
505 | table.at(s) = values
506 | }
507 |
508 | if trap-added {
509 | table.insert(
510 | trap-name,
511 | spec.inputs.fold(
512 | (:),
513 | (d, i) => {
514 | d.insert(i, (trap-name,))
515 | return d
516 | },
517 | ),
518 | )
519 | spec.states.push(trap-name)
520 | }
521 |
522 | spec.at("transitions") = util.transpose-table(table)
523 | return spec
524 | }
525 |
526 |
527 | /// Tests if #arg[word] is accepted by a given automaton.
528 | ///
529 | /// The result if either #value(false) or an array of tuples
530 | /// with a state name and the input used to transition to the
531 | /// next state. The array is a possible path to an accepting
532 | /// final state. The last tuple always has #value(none) as
533 | /// an input.
534 | /// #example[```
535 | /// #let aut = (
536 | /// q0: (q1: 0),
537 | /// q1: (q0: 1)
538 | /// )
539 | /// #finite.accepts(aut, "01010")
540 | ///
541 | /// #finite.accepts(aut, "0101")
542 | /// ```]
543 | ///
544 | /// -> content
545 | #let accepts(
546 | /// Automaton specification.
547 | /// -> spec
548 | spec,
549 | /// A word to test.
550 | /// -> str
551 | word,
552 | /// A function to format the result.
553 | /// -> function
554 | format: (spec, states) => states
555 | .map(((s, i)) => if i != none [
556 | #s #box[#sym.arrow.r#place(top + center, dy: -88%)[#text(.88em, raw(i))]]
557 | ] else [#s])
558 | .join(),
559 | ) = {
560 | spec = create-automaton(spec)
561 |
562 | let (transitions, initial, final) = (
563 | spec.at("transitions", default: (:)),
564 | spec.at("initial", default: none),
565 | spec.at("final", default: ()),
566 | )
567 | transitions = util.transpose-table(transitions)
568 |
569 | util.assert.that(transitions != (:))
570 | util.assert.that(initial != none)
571 | util.assert.that(final != ())
572 |
573 | let next-symbol(word, inputs) = {
574 | for sym in inputs {
575 | if word.starts-with(sym) {
576 | return (word.slice(sym.len()), sym)
577 | }
578 | }
579 | return (word, none)
580 | }
581 | let traverse(word, state) = {
582 | if word.len() > 0 {
583 | let (word, symbol) = next-symbol(word, spec.inputs)
584 | if state in transitions {
585 | if symbol != none and symbol in transitions.at(state) {
586 | for next-state in transitions.at(state).at(symbol) {
587 | let states = traverse(word, next-state)
588 | if states != false {
589 | return ((state, symbol),) + states
590 | }
591 | }
592 | }
593 | }
594 | return false
595 | }
596 | // Word accepted?
597 | if state in final {
598 | return ((state, none),)
599 | } else {
600 | return false
601 | }
602 | }
603 |
604 | let result = traverse(word, initial)
605 | if result == false {
606 | return false
607 | } else {
608 | return format(spec, result)
609 | }
610 | }
611 |
--------------------------------------------------------------------------------
/src/draw.typ:
--------------------------------------------------------------------------------
1 |
2 | // imports cetz and t4t
3 | #import "./util.typ"
4 | #import util: cetz
5 |
6 | /// Draw a state at the given #arg[position].
7 | ///
8 | /// #example[```
9 | /// #cetz.canvas({
10 | /// import finite.draw: state
11 | /// state((0,0), "q1", label:"S1", initial:true)
12 | /// state("q1.east", "q2", label:"S2", final:true, anchor:"west")
13 | /// })
14 | /// ```]
15 | ///
16 | /// -> array
17 | #let state(
18 | /// Position of the states center.
19 | /// -> coordinate
20 | position,
21 | /// Name for the state.
22 | /// -> str
23 | name,
24 | /// Label for the state. If set to #value(auto), the #arg[name] is used.
25 | /// -> str | content | auto | none
26 | label: auto,
27 | /// Whether this is an initial state. This can be either
28 | /// - #value(true),
29 | /// - an #dtype("alignment") to specify an anchor for the inital marking,
30 | /// - a #dtype("string") to specify text for the initial marking,
31 | /// - an #dtype("dictionary") with the keys `anchor` and `label` to specifiy both an anchor and a text label for the marking. Additionally, the keys `stroke` and `scale` can be used to style the marking.
32 | /// -> boolean | alignment | dictionary
33 | initial: false,
34 | /// Whether this is a final state.
35 | /// -> boolean
36 | final: false,
37 | /// Anchor to use for drawing.
38 | /// -> str
39 | anchor: none,
40 | /// Styling options.
41 | /// -> any
42 | ..style,
43 | ) = {
44 | // No extra positional arguments from the style sink
45 | util.assert.no-pos(style)
46 |
47 | // Create element function
48 | cetz.draw.group(
49 | name: name,
50 | anchor: anchor,
51 | ctx => {
52 | let style = style.named()
53 |
54 | // Prepare label
55 | if not util.is-dict(label) {
56 | style.insert("label", (text: label))
57 | } else {
58 | style.insert("label", label)
59 | }
60 | if "text" not in style.label or util.is-auto(style.label.text) {
61 | style.label.insert("text", name)
62 | }
63 | // Prepare initial marking
64 | style.initial = (:)
65 | if util.is-align(initial) {
66 | style.initial.insert("anchor", initial)
67 | } else if util.is-str(initial) {
68 | style.initial.insert("label", (text: initial))
69 | } else if util.is-dict(initial) {
70 | style.initial = initial
71 | if "label" in initial and util.is-str(initial.label) {
72 | style.initial.label = (text: initial.label)
73 | }
74 | }
75 |
76 | // Prepare padding
77 | if "padding" not in style.label or util.is-auto(style.label.padding) {
78 | style.label.insert("padding", ctx.style.padding)
79 | }
80 |
81 | let style = cetz.styles.resolve(
82 | ctx.style,
83 | merge: style,
84 | base: util.default-style.state,
85 | root: "state",
86 | )
87 |
88 | // resolve coordinates
89 | let (_, pos) = cetz.coordinate.resolve(ctx, position)
90 |
91 | cetz.draw.circle(pos, name: "state", ..style)
92 | if final {
93 | cetz.draw.circle(pos, stroke: style.stroke, radius: style.radius * style.extrude)
94 | }
95 | if initial != false {
96 | let color = if style.initial.stroke == auto {
97 | stroke(style.stroke).paint
98 | } else {
99 | stroke(style.initial.stroke).paint
100 | }
101 |
102 | let initial-anchor = util.to-anchor(style.initial.anchor)
103 | let align-vec = util.align-to-vec(style.initial.anchor)
104 | let initial-start = (
105 | rel: cetz.vector.scale(
106 | align-vec,
107 | style.initial.scale,
108 | ),
109 | to: "state." + initial-anchor,
110 | )
111 |
112 | cetz.draw.line(
113 | name: "initial",
114 | initial-start,
115 | "state." + initial-anchor,
116 | mark: (end: "straight"),
117 | stroke: style.initial.stroke,
118 | )
119 | if style.initial.label != none {
120 | cetz.draw.content(
121 | name: "initial-label",
122 | (
123 | rel: util.vector-set-len(
124 | util.vector-rotate(
125 | initial-start.rel,
126 | if util.abs-angle-between(align-vec, (0, 0), -90deg, 90deg) {
127 | -90deg
128 | } else {
129 | 90deg
130 | },
131 | ),
132 | style.initial.label.dist,
133 | ),
134 | to: initial-start,
135 | ),
136 | anchor: "south",
137 | angle: util.label-angle(align-vec, style.initial.anchor),
138 | text(style.initial.label.size, color, style.initial.label.text),
139 | )
140 | }
141 | }
142 | if label not in (none, (:)) {
143 | if style.label.fill in (auto, none) {
144 | style.label.fill = stroke(style.stroke).paint
145 | }
146 | if style.label.fill == auto {
147 | style.label.fill = black
148 | }
149 |
150 | cetz.draw.content(
151 | "state.center",
152 | name: "label",
153 | anchor: "center",
154 | padding: style.label.padding,
155 | text(
156 | size: style.label.size,
157 | fill: style.label.fill,
158 | style.label.text,
159 | ),
160 | )
161 | }
162 | cetz.draw.copy-anchors("state")
163 | cetz.draw.anchor("default", "state.center")
164 | },
165 | )
166 | // Update prev coordinate
167 | cetz.draw.set-ctx(ctx => {
168 | let (ctx, _) = cetz.coordinate.resolve(ctx, position)
169 | ctx
170 | })
171 | }
172 |
173 | /// Draw a transition between two states.
174 | ///
175 | /// The two states #arg[from] and #arg[to] have to be existing names of states.
176 | /// #example[```
177 | /// #cetz.canvas({
178 | /// import finite.draw: state, transition
179 | /// state((0,0), "q1")
180 | /// state((2,0), "q2")
181 | /// transition("q1", "q2", label:"a")
182 | /// transition("q2", "q1", label:"b")
183 | /// })
184 | /// ```]
185 | ///
186 | /// -> array
187 | #let transition(
188 | /// Name of the starting state.
189 | /// -> str
190 | from,
191 | /// Name of the ending state.
192 | /// -> str
193 | to,
194 | /// A list of input symbols for the transition.
195 | /// If provided as a #typ.t.str, it is split at commas to get the list of
196 | /// input symbols.
197 | /// -> str | array | none
198 | inputs: none,
199 | /// A label for the transition. For #typ.v.auto
200 | /// the #arg[input] symbols are joined with commas (`,`). Can be a #typ.t.dictionary with
201 | /// a `text` key and additional styling keys.
202 | /// -> str | content | auto | dictionary
203 | label: auto,
204 | /// Anchor for loops. Has no effect on normal transitions.
205 | /// -> alignment
206 | anchor: top,
207 | ///Styling options.
208 | /// -> any
209 | ..style,
210 | ) = {
211 | // No extra positional arguments from the style sink
212 | util.assert.no-pos(style)
213 |
214 | // TODO: (jneug) allow labels with math or content
215 |
216 | // Name of two states required
217 | util.assert.all-of-type(str, from, to)
218 | let name = from.split(".").last() + "-" + to.split(".").last()
219 |
220 | cetz.draw.group(
221 | name: name,
222 | ctx => {
223 | let style = style.named()
224 |
225 | // Prepare inputs
226 | let inputs = if util.not-empty(inputs) {
227 | if util.is-str(inputs) {
228 | inputs.split(",")
229 | } else if not util.is-arr(inputs) {
230 | (inputs,)
231 | } else {
232 | inputs
233 | }
234 | } else {
235 | none
236 | }
237 | // Prepare label
238 | if util.is-auto(label) {
239 | if util.not-none(inputs) {
240 | style.label = (text: inputs.map(str).join(","))
241 | } else {
242 | style.label = (text: none)
243 | }
244 | } else if not util.is-dict(label) {
245 | style.label = (text: label)
246 | } else {
247 | style.label = label
248 | }
249 | if not "text" in style.label and util.not-none(inputs) {
250 | // TODO: (jneug) add input-label-format function
251 | style.label.insert("text", inputs.map(str).join(","))
252 | }
253 |
254 | let style = cetz.styles.resolve(
255 | ctx.style,
256 | merge: style,
257 | base: util.default-style.transition,
258 | root: "transition",
259 | )
260 | // resolve loop styles on top
261 | if from == to {
262 | style = cetz.styles.resolve(
263 | ctx.style,
264 | merge: ctx.style.at("loop", default: (:)),
265 | base: style,
266 | root: "transition",
267 | )
268 | }
269 |
270 | let (_, start, f-center, f-right, end, t-center, t-right) = cetz.coordinate.resolve(
271 | ctx,
272 | from + ".state",
273 | from + ".state.center",
274 | from + ".state.east",
275 | to + ".state",
276 | to + ".state.center",
277 | to + ".state.east",
278 | )
279 | let (start-rad, end-rad) = (
280 | f-right.at(0) - f-center.at(0),
281 | t-right.at(0) - t-center.at(0),
282 | )
283 | let (start, end, ctrl1, ctrl2) = util.transition-pts(
284 | start,
285 | end,
286 | start-rad,
287 | end-rad,
288 | curve: style.curve * .75,
289 | anchor: anchor,
290 | )
291 | cetz.draw.bezier(
292 | name: "arrow",
293 | start,
294 | end,
295 | ctrl1,
296 | ctrl2,
297 | mark: (
298 | end: ">",
299 | stroke: style.stroke,
300 | fill: stroke(style.stroke).paint,
301 | ),
302 | ..style,
303 | )
304 | cetz.draw.copy-anchors("arrow")
305 |
306 | if not util.is-empty(style.label.text) {
307 | style.label.size = cetz.util.resolve-number(ctx, style.label.size) * ctx.length
308 |
309 | if style.label.fill in (auto, none) {
310 | style.label.fill = stroke(style.stroke).paint
311 | }
312 | if style.label.fill == auto {
313 | style.label.fill = black
314 | }
315 |
316 | let label-pt = util.label-pt(start, end, ctrl1, ctrl2, style, loop: start == end)
317 | cetz.draw.content(
318 | name: "label",
319 | label-pt,
320 | angle: if util.is-angle(style.label.angle) {
321 | style.label.angle
322 | } else if start == end {
323 | 0deg
324 | } else {
325 | let d = util.cubic-derivative(start, end, ctrl1, ctrl2, style.label.pos)
326 | let a = cetz.vector.angle2((0, 0), d)
327 | if a < 0deg { a += 360deg }
328 | if a > 90deg and a < 270deg {
329 | a = cetz.vector.angle2((0, 0), cetz.vector.scale(d, -1))
330 | }
331 | a
332 | },
333 | {
334 | let label-style = (size: style.label.size)
335 | if style.label.fill != none {
336 | label-style.insert("fill", style.label.fill)
337 | }
338 | set text(
339 | size: style.label.size,
340 | fill: style.label.fill,
341 | )
342 | style.label.text
343 | },
344 | )
345 | }
346 | },
347 | )
348 | // Update prev coordinate
349 | cetz.draw.set-ctx(ctx => {
350 | let (ctx, _) = cetz.coordinate.resolve(ctx, to)
351 | ctx
352 | })
353 | }
354 |
355 |
356 | /// Create a transition loop on a state.
357 | /// #example[```
358 | /// #cetz.canvas({
359 | /// import finite.draw: state, loop
360 | /// state((0,0), "q1")
361 | /// loop("q1", label:"a")
362 | /// loop("q1", anchor: bottom+right, label:"b")
363 | /// })
364 | /// ```]
365 | ///
366 | /// This is a shortcut for @cmd:transition that takes only one
367 | /// state name instead of two.
368 | #let loop(
369 | /// Name of the state to draw the loop on.
370 | /// -> str
371 | state,
372 | /// A list of input symbols for the loop.
373 | /// If provided as a #typ.t.str, it is split at commas to get the list of
374 | /// input symbols.
375 | /// -> str | array | none
376 | inputs: none,
377 | /// A label for the loop. For #typ.v.auto
378 | /// the #arg[input] symbols are joined with commas (`,`). Can be a #typ.t.dictionary with
379 | /// a `text` key and additional styling keys.
380 | /// -> str | content | auto | dictionary
381 | label: auto,
382 | /// Anchor for the loop.
383 | /// -> alignment
384 | anchor: top,
385 | ///Styling options.
386 | /// -> any
387 | ..style,
388 | ) = transition(
389 | state,
390 | state,
391 | inputs: inputs,
392 | label: label,
393 | anchor: anchor,
394 | ..style,
395 | )
396 |
397 |
398 | /// Draws multiple transitions from a transition table with a common style.
399 | /// #example[```
400 | /// #cetz.canvas({
401 | /// import finite.draw: state, transitions
402 | /// state((0,0), "q1")
403 | /// state((2,0), "q2")
404 | /// transitions(
405 | /// (
406 | /// q1: (q2: (0, 1)),
407 | /// q2: (q1: 0, q2: 1)
408 | /// ),
409 | /// transition: (stroke: green)
410 | /// )
411 | /// })
412 | /// ```]
413 | ///
414 | /// -> content
415 | #let transitions(
416 | /// A transition table given as a #typ.t.dictionary of dictionaries.
417 | /// -> transition-table
418 | states,
419 | /// Styling options.
420 | /// -> any
421 | ..style,
422 | ) = {
423 | util.assert.no-pos(style)
424 | style = style.named()
425 |
426 | for (from, transitions) in states {
427 | for (to, label) in transitions {
428 | let name = from + "-" + to
429 |
430 | transition(
431 | from,
432 | to,
433 | inputs: label,
434 | // label: label,
435 | ..style.at("transition", default: (:)),
436 | ..style.at(name, default: (:)),
437 | )
438 | }
439 | }
440 | }
441 |
--------------------------------------------------------------------------------
/src/finite.typ:
--------------------------------------------------------------------------------
1 | #import "util.typ": cetz
2 |
3 | #import "./draw.typ"
4 | #import "./layout.typ"
5 |
6 | #import "./cmd.typ": create-automaton, automaton, transition-table, powerset, add-trap, accepts
7 |
8 | #import "./flaci.typ"
9 |
--------------------------------------------------------------------------------
/src/flaci.typ:
--------------------------------------------------------------------------------
1 | #import "./cmd.typ"
2 | #import "./util.typ": get, vec-to-align, align-to-vec
3 |
4 | #let _bom = bytes((239, 187, 191))
5 | // Parse a string with BOM into a JSON dictionary.
6 | #let _load-json(data) = {
7 | data = bytes(data)
8 | if data.slice(0, 3) == _bom {
9 | data = data.slice(3)
10 | }
11 | return json(data)
12 | }
13 |
14 | /// Loads #arg[data] into an automaton @type:spec.
15 | /// #arg[data] needs to be a string and not a JSON dictionary.
16 | /// -> spec
17 | #let load(
18 | /// FLACI data read as a string via #typ.read.
19 | /// -> str
20 | data,
21 | ) = {
22 | data = _load-json(data)
23 | assert(
24 | data.at("type", default: "ERROR") in ("DEA", "NEA"),
25 | message: "Currently only FLACI automata of type DEA or NEA are supported. Got: " + data.at("type", default: "none"),
26 | )
27 | let automaton = data.at("automaton", default: (:))
28 |
29 | // Scaling factor for FLACI coordinates to CeTZ conversion
30 | let scaling = .02
31 |
32 | // Some FLACI default styles
33 | let style = (
34 | transition: (
35 | curve: 0,
36 | label: (angle: 0deg),
37 | ),
38 | )
39 |
40 | // Variable declarations
41 | let transitions = (:)
42 | let states = ()
43 | let final = ()
44 | let initial = none
45 | let inputs = automaton.at("Alphabet", default: ())
46 | let id-map = (:)
47 | let layout = (:)
48 |
49 | // Parse states
50 | for state in automaton.at("States", default: ()) {
51 | let name = state.Name
52 |
53 | // Prepare state
54 | states.push(name)
55 | transitions.insert(name, (:))
56 | id-map.insert(str(state.ID), name)
57 |
58 | // Final and initial states
59 | if state.Final {
60 | final.push(name)
61 | }
62 | if state.Start {
63 | initial = name
64 | }
65 |
66 | // Layout
67 | layout.insert(
68 | name,
69 | (state.x * scaling, state.y * -scaling),
70 | )
71 |
72 | // Style
73 | style.insert(
74 | name,
75 | (
76 | radius: state.Radius * scaling,
77 | ),
78 | )
79 | }
80 |
81 | // Second pass to parse transitions
82 | for state in automaton.at("States", default: ()) {
83 | let name = state.Name
84 | let id = state.ID
85 |
86 | let state-transitions = (:)
87 | for transition in state.Transitions {
88 | if transition.Source == id {
89 | state-transitions.insert(id-map.at(str(transition.Target)), transition.Labels)
90 | } else {
91 | state-transitions.insert(id-map.at(str(transition.Source)), transition.Labels)
92 | }
93 |
94 | if transition.Source == transition.Target {
95 | let vec = (transition.x, -transition.y)
96 | let align = vec-to-align(vec)
97 | style.insert(name + "-" + name, (anchor: align))
98 | }
99 | }
100 | transitions.insert(state.Name, state-transitions)
101 | }
102 |
103 | // Do some cleanup of style (eg curve for double transitions)
104 | for (state, state-transitions) in transitions {
105 | for target-state in state-transitions.keys() {
106 | if state in transitions.at(target-state) {
107 | let name = state + "-" + target-state
108 | let sty = style.at(name, default: (:))
109 | style.insert(name, get.dict-merge((curve: .6), sty))
110 | }
111 | }
112 | }
113 |
114 |
115 | return (
116 | cmd.create-automaton(transitions, states: states, inputs: inputs, initial: initial, final: final),
117 | layout,
118 | style,
119 | )
120 | }
121 |
122 |
123 | /// Show a FLACI file as an @cmd:automaton.
124 | ///
125 | /// #warning-alert[
126 | /// Read the FLACI json-file with the #typ.read function, not
127 | /// the #typ.json function. FLACI exports automatons with a wrong encoding
128 | /// that prevents Typst from properly loading the file as JSON.
129 | /// ]
130 | ///
131 | /// #info-alert[
132 | /// Currently only DEA and NEA automata are supported.
133 | /// ]
134 | /// -> content
135 | #let automaton(
136 | /// FLACI data read as a string via #typ.read.
137 | /// -> str
138 | data,
139 | /// Custom layout for the automaton. Will overwrite state positions from #arg[data].
140 | /// -> function
141 | layout: auto,
142 | /// Custom state positions to merge with the ones found in #arg[data].
143 | /// This allows the placement of some states while the
144 | /// rest keeps their positions.
145 | /// -> dictionary
146 | merge-layout: true,
147 | /// Custom styles to overwrite the defaults.
148 | /// -> dictionary
149 | style: auto,
150 | /// Custom styles to merge with the styles from #arg[data].
151 | merge-style: true,
152 | /// Further arguments for @cmd:automaton.
153 | /// -> any
154 | ..args,
155 | ) = {
156 | let (spec, lay, sty) = load(data)
157 |
158 | if layout == auto {
159 | layout = lay
160 | } else if merge-layout and type(layout) == dictionary {
161 | layout = get.dict-merge(lay, layout)
162 | }
163 | if style == auto {
164 | style = sty
165 | } else if merge-style and type(style) == dictionary {
166 | style = get.dict-merge(sty, style)
167 | }
168 |
169 | cmd.automaton(spec, layout: layout, style: style, ..args)
170 | }
171 |
--------------------------------------------------------------------------------
/src/layout.typ:
--------------------------------------------------------------------------------
1 | #import "util.typ"
2 | #import util: default-style, cetz, test, assert-dict, assert-spec
3 |
4 |
5 | /// Helper function to create a layout dictionary by providing
6 | /// #arg[positions] and/or #arg[anchors].
7 | /// -> array
8 | #let create-layout(positions: (:), anchors: (:)) = {
9 | (positions, anchors)
10 | }
11 |
12 |
13 | /// Create a custom layout from a #typ.t.dictionary with
14 | /// state #dtype("coordinate")s.
15 | ///
16 | /// The result may specify a `rest` key that is used as a default coordinate. This is useful
17 | /// sense in combination with a relative coordinate like `(rel:(2,0))`.
18 | ///
19 | /// #example(breakable:true)[```
20 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d})
21 | /// #finite.automaton(
22 | /// aut,
23 | /// initial: none, final: none,
24 | /// layout:finite.layout.custom.with(positions: (
25 | /// q0: (0,0), q1: (0,2), rest:(rel: (1.5,-.5))
26 | /// ))
27 | /// )
28 | /// ```]
29 | ///
30 | /// -> array
31 | #let custom(
32 | /// Automaton specification.
33 | /// -> spec
34 | spec,
35 | /// A dictionary with #dtype("coordinate")s for each state.
36 | ///
37 | /// The dictionary contains each states name as a
38 | /// key and the new coordinate as a value.
39 | ///
40 | /// -> dictionary
41 | positions: (:),
42 | /// Position of the anchor point.
43 | /// -> coordinate
44 | position: (0, 0),
45 | /// Styling options.
46 | /// -> dictionary
47 | style: (:),
48 | ) = {
49 | assert-spec(spec)
50 | assert-dict(positions)
51 |
52 | let rest = positions.at("rest", default: (rel: (4, 0)))
53 |
54 | for state in spec.states {
55 | positions.insert(
56 | state,
57 | (
58 | rel: position,
59 | to: positions.at(state, default: util.call-or-get(rest, state)),
60 | ),
61 | )
62 | }
63 |
64 | // return coordinates
65 | return create-layout(positions: positions)
66 | }
67 |
68 |
69 | /// Arrange states in a line.
70 | ///
71 | /// The direction of the line can be set via #arg[dir] either to an #typ.t.alignment
72 | /// or a direction vector with a x and y shift. Note that the length of the vector is set to #arg[spacing] and only the direction is used.
73 | ///
74 | /// #example(breakable:true)[```
75 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d})
76 | /// #finite.automaton(
77 | /// aut,
78 | /// initial: none, final: none,
79 | /// layout:finite.layout.linear.with(dir: right)
80 | /// )
81 | /// #finite.automaton(
82 | /// aut,
83 | /// initial: none, final: none,
84 | /// layout:finite.layout.linear.with(spacing: .5, dir:(2,-1))
85 | /// )
86 | /// ```]
87 | ///
88 | /// -> array
89 | #let linear(
90 | /// Automaton specification.
91 | /// -> spec
92 | spec,
93 | /// Direction of the line.
94 | /// -> vector | alignment | 2d alignment
95 | dir: right,
96 | /// Spacing between states on the line.
97 | /// -> float
98 | spacing: default-style.state.radius * 2,
99 | /// Position of the anchor point.
100 | /// -> coordinate
101 | position: (0, 0),
102 | /// Styling options.
103 | /// -> dictionary
104 | style: (:),
105 | ) = {
106 | assert-spec(spec)
107 |
108 | let dir = dir
109 | if test.is-type(alignment, dir) {
110 | dir = util.align-to-vec(dir)
111 | }
112 | dir = cetz.vector.norm(dir)
113 | let dir-angle = cetz.vector.angle2((0, 0), dir)
114 | let spacing-vec = cetz.vector.scale(dir, spacing)
115 |
116 | let positions = (:)
117 | let anchors = (:)
118 |
119 | let prev-name = none
120 | for name in spec.states {
121 | positions.insert(
122 | name,
123 | if prev-name == none {
124 | position
125 | } else {
126 | (
127 | rel: spacing-vec,
128 | to: prev-name + ".state." + repr(dir-angle),
129 | )
130 | },
131 | )
132 | anchors.insert(
133 | name,
134 | if prev-name == none {
135 | "center"
136 | } else {
137 | repr(dir-angle + 180deg)
138 | },
139 | )
140 | prev-name = name
141 | }
142 |
143 | // return coordinates
144 | return create-layout(positions: positions, anchors: anchors)
145 | }
146 |
147 |
148 | /// Arrange states in a circle.
149 | ///
150 | /// #example(breakable:true)[```
151 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d})
152 | /// #grid(columns: 2, gutter: 2em,
153 | /// finite.automaton(
154 | /// aut,
155 | /// initial: none, final: none,
156 | /// layout:finite.layout.circular,
157 | /// style: (q0: (fill: yellow.lighten(60%)))
158 | /// ),
159 | /// finite.automaton(
160 | /// aut,
161 | /// initial: none, final: none,
162 | /// layout:finite.layout.circular.with(offset:45deg),
163 | /// style: (q0: (fill: yellow.lighten(60%)))
164 | /// ),
165 | /// finite.automaton(
166 | /// aut,
167 | /// initial: none, final: none,
168 | /// layout:finite.layout.circular.with(dir:left),
169 | /// style: (q0: (fill: yellow.lighten(60%)))
170 | /// ),
171 | /// finite.automaton(
172 | /// aut,
173 | /// initial: none, final: none,
174 | /// layout:finite.layout.circular.with(dir:left, offset:45deg),
175 | /// style: (q0: (fill: yellow.lighten(60%)))
176 | /// )
177 | /// )
178 | /// ```]
179 | #let circular(
180 | /// Automaton specification.
181 | /// -> spec
182 | spec,
183 | /// Direction of the circle. Either #value(left) or #value(right).
184 | /// -> alignment
185 | dir: right,
186 | /// Spacing between states on the line.
187 | /// -> float
188 | spacing: default-style.state.radius * 2,
189 | /// Either a fixed radius or #typ.v.auto to calculate a suitable radius.
190 | /// -> float | auto
191 | radius: auto,
192 | /// An offset angle to place the first state at.
193 | /// -> angle
194 | offset: 0deg,
195 | /// Position of the anchor point.
196 | /// -> coordinate
197 | position: (0, 0),
198 | /// Styling options.
199 | /// -> dictionary
200 | style: (:),
201 | ) = {
202 | // TODO: (jneug) fix positioning
203 | let radii = util.get-radii(spec, style: style)
204 | let len = radii.values().fold(0, (s, r) => s + 2 * r + spacing)
205 |
206 | let radius = radius
207 | if util.is-auto(radius) {
208 | radius = len / (2 * calc.pi)
209 | } else {
210 | len = 2 * radius * calc.pi
211 | }
212 |
213 | let positions = (:)
214 | let anchors = (:)
215 | let at = 0.0
216 | for name in spec.states {
217 | let state-radius = radii.at(name)
218 | let ang = 0deg
219 | let ang = (
220 | offset
221 | + util.math.map(
222 | 0.0,
223 | len,
224 | 0deg,
225 | 360deg,
226 | at + state-radius,
227 | )
228 | )
229 |
230 | let pos = (
231 | rel: (
232 | -radius * calc.cos(ang),
233 | if dir == right {
234 | radius
235 | } else {
236 | -radius
237 | }
238 | * calc.sin(ang),
239 | ),
240 | to: position,
241 | )
242 |
243 | positions.insert(name, pos)
244 |
245 | // anchors.insert(name, "state." + repr(-ang))
246 |
247 | at += 2 * state-radius + spacing
248 | }
249 | return create-layout(positions: positions, anchors: anchors)
250 | }
251 |
252 |
253 | /// Arrange states in rows and columns.
254 | ///
255 | /// #example[```
256 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d})
257 | /// #finite.automaton(
258 | /// aut,
259 | /// initial: none, final: none,
260 | /// layout:finite.layout.grid.with(columns:3)
261 | /// )
262 | /// ```]
263 | ///
264 | /// -> array
265 | #let grid(
266 | /// Automaton specification.
267 | /// -> spec
268 | spec,
269 | /// Number of columns per row.
270 | /// -> int
271 | columns: 4,
272 | /// Spacing between states on the grid.
273 | /// -> float
274 | spacing: default-style.state.radius * 2,
275 | /// Position of the anchor point.
276 | /// -> coordinate
277 | position: (0, 0),
278 | /// Styling options.
279 | /// -> dictionary
280 | style: (:),
281 | ) = {
282 | let spacing = if not util.is-arr(spacing) {
283 | (x: spacing, y: spacing)
284 | } else {
285 | (x: spacing.first(), y: spacing.last())
286 | }
287 |
288 | let radii = util.get-radii(spec, style: style)
289 | let max-radius = calc.max(..radii.values())
290 |
291 | let positions = (:)
292 | for (i, name) in spec.states.enumerate() {
293 | let (row, col) = (
294 | calc.quo(i, columns),
295 | calc.rem(i, columns),
296 | )
297 | positions.insert(
298 | name,
299 | (
300 | rel: (
301 | col * (2 * max-radius + spacing.x),
302 | row * (2 * max-radius + spacing.y),
303 | ),
304 | to: position,
305 | ),
306 | )
307 | }
308 | return create-layout(positions: positions)
309 | }
310 |
311 |
312 | /// Arrange states in a grid, but alternate the direction in every even and odd row.
313 | ///
314 | /// #example(breakable:true)[```
315 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d})
316 | /// #finite.automaton(
317 | /// aut,
318 | /// initial: none, final: none,
319 | /// layout:finite.layout.snake.with(columns:3)
320 | /// )
321 | /// ```]
322 | ///
323 | /// -> array
324 | #let snake(
325 | /// Automaton specification.
326 | /// -> spec
327 | spec,
328 | /// Number of columns per row.
329 | /// -> int
330 | columns: 4,
331 | /// Spacing between states on the line.
332 | /// -> float
333 | spacing: default-style.state.radius * 2,
334 | /// Position of the anchor point.
335 | /// -> coordinate
336 | position: (0, 0),
337 | /// Styling options.
338 | /// -> dictionary
339 | style: (:),
340 | ) = {
341 | let spacing = if not util.is-arr(spacing) {
342 | (x: spacing, y: spacing)
343 | } else {
344 | (x: spacing.first(), y: spacing.last())
345 | }
346 |
347 | let radii = util.get-radii(spec, style: style)
348 | let max-radius = calc.max(..radii.values())
349 |
350 | let positions = (:)
351 | for (i, name) in spec.states.enumerate() {
352 | let (row, col) = (
353 | calc.quo(i, columns),
354 | calc.rem(i, columns),
355 | )
356 | positions.insert(
357 | name,
358 | if calc.odd(row) {
359 | (
360 | rel: (
361 | (columns - col - 1) * (2 * max-radius + spacing.x),
362 | row * (2 * max-radius + spacing.y),
363 | ),
364 | to: position,
365 | )
366 | } else {
367 | (
368 | rel: (
369 | col * (2 * max-radius + spacing.x),
370 | row * (2 * max-radius + spacing.y),
371 | ),
372 | to: position,
373 | )
374 | },
375 | )
376 | }
377 | return create-layout(positions: positions)
378 | }
379 |
380 |
381 | /// Creates a group layout that collects states into groups that are
382 | /// positioned by specific sub-layouts.
383 | ///
384 | /// #example(breakable:true)[```
385 | /// #let aut = range(6).fold((:), (d, s) => {d.insert("q"+str(s), none); d})
386 | /// #finite.automaton(
387 | /// aut,
388 | /// initial: none, final: none,
389 | /// layout: finite.layout.group.with(
390 | /// grouping: 3,
391 | /// spacing: 4,
392 | /// layout: (
393 | /// finite.layout.linear.with(dir: bottom),
394 | /// finite.layout.circular,
395 | /// )
396 | /// )
397 | /// )
398 | /// ```]
399 | ///
400 | /// See @sec:showcase for a more comprehensive example.
401 | ///
402 | /// -> array
403 | #let group(
404 | /// Automaton specification.
405 | /// -> spec
406 | spec,
407 | /// Either an integer to collect states into
408 | /// roughly equal sized groups or an array of arrays that specify which
409 | /// states (by name) are in each group.
410 | /// -> int | array
411 | grouping: auto,
412 | /// Spacing between states on the line.
413 | /// -> float
414 | spacing: default-style.state.radius * 2,
415 | /// An array of layouts to use for each group. The first group of
416 | /// states will be passed to the first layout and so on.
417 | /// -> array
418 | layout: linear.with(dir: bottom),
419 | /// Position of the anchor point.
420 | /// -> coordinate
421 | position: (0, 0),
422 | /// Styling options.
423 | /// -> dictionary
424 | style: (:),
425 | ) = {
426 | assert-spec(spec)
427 |
428 | let groups = ()
429 | let rest = ()
430 |
431 | let grouping = util.def.if-auto(grouping, def: spec.states.len())
432 |
433 | // collect state groups
434 | if util.is-int(grouping) {
435 | // by equal size
436 | groups = spec.states.chunks(grouping)
437 | } else if util.is-arr(grouping) {
438 | groups = grouping
439 | // collect remaining states into "rest" group
440 | rest = spec.states.filter(s => not groups.any(g => s in g))
441 | }
442 |
443 | let positions = (:)
444 | let anchors = (:)
445 | let last-name = none
446 | for (i, group) in groups.enumerate() {
447 | let group-layout
448 | if util.is-arr(layout) {
449 | if layout.len() > i {
450 | group-layout = layout.at(i)
451 | } else {
452 | group-layout = layout.at(-1)
453 | }
454 | } else {
455 | group-layout = layout
456 | }
457 |
458 | let (pos, anc) = group-layout(
459 | spec + (states: group),
460 | position: if i == 0 {
461 | position
462 | } else {
463 | // TODO: (jneug) fix spacing between layouts
464 | (rel: (i * spacing, 0), to: position)
465 | },
466 | style: style,
467 | )
468 |
469 | positions += pos
470 | anchors += anc
471 | }
472 |
473 | return create-layout(positions: positions, anchors: anchors)
474 | }
475 |
--------------------------------------------------------------------------------
/src/util.typ:
--------------------------------------------------------------------------------
1 |
2 | // Package dependencies
3 | #import "@preview/t4t:0.4.3": *
4 | #import "@preview/cetz:0.3.4"
5 |
6 | // TODO: (jneug) implement scheme validation with valkyrie
7 | // #import "@preview/valkyrie:0.2.1" as t
8 |
9 | // TODO: (jneug) don't import into global scope
10 | #import cetz.util.bezier: cubic-point, cubic-derivative, cubic-through-3points
11 |
12 |
13 | // TODO (jneug) refactor module and cleanup
14 |
15 | // =================================
16 | // Defaults
17 | // =================================
18 |
19 | #let default-style = (
20 | state: (
21 | fill: white,
22 | stroke: auto,
23 | radius: .6,
24 | extrude: .88,
25 | label: (
26 | text: auto,
27 | size: 1em,
28 | fill: none,
29 | padding: auto,
30 | ),
31 | initial: (
32 | anchor: left,
33 | label: (
34 | text: "Start",
35 | size: .88em,
36 | dist: .1,
37 | ),
38 | stroke: auto,
39 | scale: .8,
40 | ),
41 | ),
42 | transition: (
43 | curve: 1,
44 | stroke: auto,
45 | label: (
46 | text: "",
47 | size: 1em,
48 | fill: none,
49 | pos: .5,
50 | dist: .33,
51 | angle: auto,
52 | ),
53 | ),
54 | loop: (:),
55 | )
56 |
57 | // =================================
58 | // Helpers
59 | // =================================
60 |
61 | /// Calls #arg[value] with #sarg[args], if it is a #dtype("function") and returns the result or #arg[value] otherwise.
62 | #let call-or-get(value, ..args) = {
63 | if is-func(value) {
64 | return value(..args)
65 | } else {
66 | return value
67 | }
68 | }
69 |
70 | #let assert-dict = assert.new(
71 | is-dict,
72 | message: v => "dictionary expected. got " + repr(v),
73 | )
74 |
75 | #let assert-spec = assert.new(
76 | value => is-dict(value) and "finite-spec" in value,
77 | message: v => "automaton specification expected. got " + repr(v),
78 | )
79 |
80 | #let assert-full-spec = assert.new(
81 | value => is-dict(value) and ("finite-spec", "type", "transitions", "states", "initial", "final").all(k => k in value),
82 | message: v => "full automaton specification expected. got " + repr(v),
83 | )
84 |
85 |
86 | // =================================
87 | // Vectors
88 | // =================================
89 |
90 | /// Set the length of a cetz.vector.
91 | #let vector-set-len(v, len) = if cetz.vector.len(v) == 0 {
92 | return v
93 | } else {
94 | return cetz.vector.scale(cetz.vector.norm(v), len)
95 | }
96 |
97 | /// Compute a normal for a 2d cetz.vector. The normal will be pointing to the right
98 | /// of the original cetz.vector.
99 | #let vector-normal(v) = cetz.vector.norm((-v.at(1), v.at(0), 0))
100 |
101 | /// Rotates a vector by #arg[angle] degree around the origin.
102 | #let vector-rotate(vec, angle) = {
103 | let (x, y, ..) = vec
104 | return (
105 | calc.cos(angle) * x - calc.sin(angle) * y,
106 | calc.sin(angle) * x + calc.cos(angle) * y,
107 | )
108 | }
109 |
110 | /// Returns a vector for an alignment.
111 | #let align-to-vec(a) = {
112 | let v = (
113 | ("none": 0, "center": 0, "left": -1, "right": 1).at(repr(a.x)),
114 | ("none": 0, "horizon": 0, "top": 1, "bottom": -1).at(repr(a.y)),
115 | )
116 |
117 | return cetz.vector.norm(v)
118 | }
119 |
120 | #let vec-to-align(vec) = {
121 | let angle = cetz.vector.angle2((0, 0), vec) / 1deg
122 | if angle < 0 { angle = angle + 360 }
123 | let idx = calc.round((angle - 22.5) / 45 + .5)
124 |
125 | return (
126 | right,
127 | top + right,
128 | top,
129 | top + left,
130 | left,
131 | bottom + left,
132 | bottom,
133 | bottom + right,
134 | ).at(int(idx))
135 | }
136 |
137 |
138 | // =================================
139 | // Bezier
140 | // =================================
141 |
142 | /// Compute a normal vector for a point on a cubic bezier curve.
143 | #let cubic-normal(a, b, c, d, t) = {
144 | let qd = cubic-derivative(a, b, c, d, t)
145 | if cetz.vector.len(qd) == 0 {
146 | return (0, 1, 0)
147 | } else {
148 | return vector-normal(qd)
149 | }
150 | }
151 |
152 | /// Compute the mid point of a quadratic bezier curve.
153 | #let mid-point(a, b, c, d) = cubic-point(a, b, c, d, .5)
154 |
155 |
156 | // =================================
157 | // Helpers
158 | // =================================
159 |
160 | /// Calculate the control point for a transition.
161 | #let cubic-pts(a, b, curve: 1) = {
162 | if curve == 0 {
163 | return (a, b, b, a)
164 | }
165 | let ab = cetz.vector.sub(b, a)
166 | let X = cetz.vector.add(
167 | cetz.vector.add(
168 | a,
169 | cetz.vector.scale(ab, .5),
170 | ),
171 | cetz.vector.scale(
172 | vector-normal(ab),
173 | curve,
174 | ),
175 | )
176 | return cubic-through-3points(a, X, b)
177 | }
178 |
179 | /// Calculate the direction vector for a transition mark (arrowhead)
180 | #let mark-dir(a, b, c, d, scale: 1) = vector-set-len(cubic-derivative(a, b, c, d, 1), scale)
181 |
182 | /// Calculate the location for a transitions label, based
183 | /// on its bezier points.
184 | #let label-pt(a, b, c, d, style, loop: false) = {
185 | let pos = style.label.pos // style.label.at("pos", default: default-style.transition.label.pos)
186 | let dist = style.label.dist // style.label.at("dist", default: default-style.transition.label.dist)
187 | let curve = style.curve // style.at("curve", default: default-style.transition.curve)
188 |
189 | let pt = cubic-point(a, b, c, d, pos)
190 | let n = cubic-normal(a, b, c, d, pos)
191 |
192 | if loop and curve < 0 {
193 | dist *= -1
194 | }
195 |
196 | return cetz.vector.add(
197 | pt,
198 | cetz.vector.scale(n, dist),
199 | )
200 | }
201 |
202 |
203 | /// Calculate start, end and ctrl points for a transition loop.
204 | ///
205 | /// - start (vector): Center of the state.
206 | /// - start-radius (length): Radius of the state.
207 | /// - curve (float): Curvature of the transition.
208 | /// - anchor (alignment): Anchorpoint on the state
209 | #let loop-pts(start, start-radius, anchor: top, curve: 1) = {
210 | anchor = vector-set-len(align-to-vec(anchor), start-radius)
211 |
212 | let end = cetz.vector.add(
213 | start,
214 | vector-rotate(anchor, -22.5deg),
215 | )
216 | let start = cetz.vector.add(
217 | start,
218 | vector-rotate(anchor, 22.5deg),
219 | )
220 |
221 | if curve < 0 {
222 | (start, end) = (end, start)
223 | } else if curve == 0 {
224 | curve = start-radius
225 | }
226 |
227 | let (start, end, c1, c2) = cubic-pts(start, end, curve: curve)
228 |
229 | if curve < 0 {
230 | (c1, c2) = (c2, c1)
231 | }
232 |
233 | let d = cetz.vector.scale(cetz.vector.sub(c2, c1), curve * 4)
234 | c1 = cetz.vector.sub(c1, d)
235 | c2 = cetz.vector.add(c2, d)
236 |
237 | return (start, end, c1, c2)
238 | }
239 |
240 |
241 | /// Calculate start, end and ctrl points for a transition.
242 | ///
243 | /// - start (vector): Center of the start state.
244 | /// - end (vector): Center of the end state.
245 | /// - start-radius (length): Radius of the start state.
246 | /// - end-radius (length): Radius of the end state.
247 | /// - curve (float): Curvature of the transition.
248 | #let transition-pts(start, end, start-radius, end-radius, curve: 1, anchor: top) = {
249 | // Is it a loop?
250 | if start == end {
251 | return loop-pts(start, start-radius, curve: curve, anchor: anchor)
252 | } else {
253 | let (start, end, ctrl1, ctrl2) = cubic-pts(start, end, curve: curve)
254 |
255 | start = cetz.vector.add(
256 | start,
257 | vector-set-len(
258 | cetz.vector.sub(
259 | ctrl1,
260 | start,
261 | ),
262 | start-radius,
263 | ),
264 | )
265 | end = cetz.vector.add(
266 | end,
267 | vector-set-len(
268 | cetz.vector.sub(
269 | end,
270 | ctrl2,
271 | ),
272 | -end-radius,
273 | ),
274 | )
275 | return (
276 | start,
277 | end,
278 | ctrl1,
279 | ctrl2,
280 | )
281 | }
282 | }
283 |
284 | /// Fits (text) content inside the available space.
285 | ///
286 | /// - ctx (dictionary): The canvas context.
287 | /// - content (string, content): The content to fit.
288 | /// - size (length,auto): The initial text size.
289 | /// - min-size (length): The minimal text size to set.
290 | #let fit-content(ctx, width, height, content, size: auto, min-size: 6pt) = {
291 | let s = def.if-auto(ctx.length, size)
292 |
293 | let m = (width: 2 * width, height: 2 * height)
294 | while (m.height > height or m.width > height) and s > min-size {
295 | s = s * .88
296 | m = cetz.util.measure(
297 | ctx,
298 | {
299 | set text(s)
300 | content
301 | },
302 | )
303 | }
304 | s = calc.max(min-size, s)
305 | {
306 | set text(s)
307 | content
308 | }
309 | }
310 |
311 | /// Changes a @type:transition-table from the format (`state`: `inputs`) to (`input`: `states`) or vice versa.
312 | /// -> dict
313 | #let transpose-table(
314 | /// A transition table in any format.
315 | /// -> transition-table
316 | table,
317 | ) = {
318 | let ttable = (:)
319 | for (key, values) in table {
320 | let new-values = (:)
321 |
322 | if not-none(values) {
323 | for (kk, vv) in values {
324 | for i in def.as-arr(vv) {
325 | if not-none(i) {
326 | i = str(i)
327 | if i not in new-values {
328 | new-values.insert(i, (kk,))
329 | } else {
330 | new-values.at(i).push(kk)
331 | }
332 | }
333 | }
334 | }
335 | }
336 |
337 | ttable.insert(key, new-values)
338 | }
339 |
340 | return ttable
341 | }
342 |
343 | /// Gets a list of all inputs from a transition table.
344 | ///
345 | #let get-inputs(
346 | /// A transition table.
347 | /// -> transition-table
348 | table,
349 | /// If #arg[table] needs to be transposed first. Set this to #typ.v.false if the table already is in the format (`input`: `states`).
350 | /// -> bool
351 | transpose: true,
352 | ) = {
353 | if transpose {
354 | table = transpose-table(table)
355 | }
356 |
357 | let inputs = ()
358 | for (_, values) in table {
359 | for (inp, _) in values {
360 | if inp not in inputs {
361 | inputs.push(str(inp))
362 | }
363 | }
364 | }
365 |
366 | return inputs.sorted()
367 | }
368 |
369 |
370 | /// Checks if a given @type:spec represents
371 | /// a deterministic automaton.
372 | ///
373 | /// ```example
374 | /// #util.is-dea((
375 | /// q0: (q1: 1, q2: 1),
376 | /// ))
377 | /// #util.is-dea((
378 | /// q0: (q1: 1, q2: 0),
379 | /// ))
380 | /// ```
381 | ///
382 | /// -> bool
383 | #let is-dea(
384 | /// A transition table.
385 | /// -> transition-table
386 | table,
387 | ) = {
388 | for (_, transitions) in table {
389 | let inp = ()
390 | for (state, inputs) in transitions {
391 | for i in def.as-arr(inputs) {
392 | if i in inp {
393 | return false
394 | } else {
395 | inp.push(i)
396 | }
397 | }
398 | }
399 | }
400 |
401 | return true
402 | }
403 |
404 |
405 | // deprecated!!!
406 | #let to-spec(spec, states: auto, initial: auto, final: auto, inputs: auto, labels: auto) = {
407 | // TODO: (jneug) add asserts to react to malicious specs
408 | // TODO: (jneug) check for duplicate names
409 | if "transitions" not in spec {
410 | spec = (transitions: spec)
411 | }
412 | if "states" not in spec {
413 | if is-auto(states) {
414 | states = spec.transitions.keys()
415 | }
416 | spec.insert("states", states)
417 | }
418 | if "initial" not in spec {
419 | if is-auto(initial) {
420 | initial = spec.states.first()
421 | }
422 | spec.insert("initial", initial)
423 | }
424 | if "final" not in spec {
425 | if is-auto(final) {
426 | final = (spec.states.last(),)
427 | } else if is-none(final) {
428 | final = ()
429 | }
430 | spec.insert("final", final)
431 | }
432 | if "inputs" not in spec {
433 | if is-auto(inputs) {
434 | inputs = get-inputs(spec.transitions)
435 | }
436 | spec.insert("inputs", inputs)
437 | } else {
438 | spec.inputs = spec.inputs.map(str).sorted()
439 | }
440 |
441 | return spec + (finite-spec: true, type: "DEA")
442 | }
443 |
444 |
445 | /// Return anchor name for an #dtype(alignment).
446 | #let align-to-anchor(align) = {
447 | let anchor = ()
448 | if align.y == top {
449 | anchor.push("north")
450 | } else if align.y == bottom {
451 | anchor.push("south")
452 | }
453 | if align.x == left {
454 | anchor.push("west")
455 | } else if align.x == right {
456 | anchor.push("east")
457 | }
458 | if anchor == () {
459 | return "center"
460 | } else {
461 | return anchor.join("-")
462 | }
463 | }
464 |
465 | #let to-anchor(align) = {
466 | if type(align) == alignment {
467 | return align-to-anchor(align)
468 | } else {
469 | align
470 | }
471 | }
472 |
473 | #let label-angle(vec, a) = if a in (top, top + right, right, bottom + right) {
474 | cetz.vector.angle2((0, 0), vec)
475 | } else {
476 | cetz.vector.angle2(vec, (0, 0))
477 | }
478 |
479 | #let abs-angle-between(vec1, vec2, lower, upper) = {
480 | let ang = calc.abs(cetz.vector.angle2(vec1, vec2))
481 | return ang >= lower and ang <= upper
482 | }
483 |
484 |
485 | #let is-state(element) = {
486 | return "finite" in element and element.finite.state
487 | }
488 |
489 | #let is-transition(element) = {
490 | return "finite" in element and element.finite.transition
491 | }
492 |
493 | #let resolve-one(ctx, element) = {
494 | element = (element)(ctx)
495 | ctx = element.ctx
496 |
497 | if "name" in element and element.name != none {
498 | if "nodes" not in ctx {
499 | ctx.insert("nodes", (:))
500 | }
501 | ctx.nodes.insert(element.name, element)
502 | }
503 |
504 | return (ctx, element)
505 | }
506 |
507 | #let resolve-many(ctx, body) = {
508 | let elements = ()
509 | let (_ctx, element) = (ctx, none)
510 | for element in body {
511 | (_ctx, element) = resolve-one(_ctx, element)
512 | elements.push(element)
513 | }
514 | return (_ctx, elements)
515 | }
516 |
517 | #let resolve-zipped(ctx, body) = {
518 | let (_ctx, elements) = resolve-many(ctx, body)
519 | return (_ctx, body.zip(elements))
520 | }
521 |
522 | #let resolve-states(ctx, body) = {
523 | let (_, elements) = resolve-many(ctx, body)
524 | return elements.filter(is-state)
525 | }
526 |
527 | #let get-radii(elements) = {
528 | return elements
529 | .filter(is-state)
530 | .fold(
531 | (:),
532 | (radii, element) => {
533 | radii.insert(element.name, element.finite.radius)
534 | return radii
535 | },
536 | )
537 | }
538 |
539 | // Resolve radii for states by applying styles from other elements.
540 | #let resolve-radii(ctx, body) = {
541 | let (_, elements) = resolve-many(ctx, body)
542 | return get-radii(elements)
543 | }
544 |
545 | #let state-wrapper(group) = {
546 | return (
547 | ctx => {
548 | let g = (group.first())(ctx)
549 | g.insert(
550 | "finite",
551 | (
552 | state: true,
553 | transition: false,
554 | radius: (g.anchors)("state.east").at(0) - (g.anchors)("state.center").at(0),
555 | ),
556 | )
557 | return g
558 | },
559 | )
560 | }
561 |
562 | #let get-radii(spec, style: (:)) = spec.states.fold(
563 | (:),
564 | (d, name) => {
565 | let r = style.at(name, default: (:)).at("radius", default: none)
566 | d.insert(
567 | name,
568 | style
569 | .at(
570 | name,
571 | default: style.at(
572 | "state",
573 | default: (:),
574 | ),
575 | )
576 | .at(
577 | "radius",
578 | default: default-style.state.radius,
579 | ),
580 | )
581 | d
582 | },
583 | )
584 |
585 | #let transition-wrapper(from, to, group) = {
586 | return (
587 | ctx => {
588 | let g = (group.first())(ctx)
589 | g.insert(
590 | "finite",
591 | (
592 | state: false,
593 | transition: true,
594 | from: from,
595 | to: to,
596 | ),
597 | )
598 | return g
599 | },
600 | )
601 | }
602 |
--------------------------------------------------------------------------------
/tbump.toml:
--------------------------------------------------------------------------------
1 | # Uncomment this if your project is hosted on GitHub:
2 | github_url = "https://github.com/jneug/typst-finite/"
3 |
4 | [version]
5 | current = "0.5.0"
6 |
7 | # Example of a semver regexp.
8 | # Make sure this matches current_version before
9 | # using tbump
10 | regex = '''
11 | (?P\d+)
12 | \.
13 | (?P\d+)
14 | \.
15 | (?P\d+)
16 | '''
17 |
18 | [git]
19 | message_template = "Bump to {new_version}"
20 | tag_template = "v{new_version}"
21 |
22 | [[file]]
23 | src = "typst.toml"
24 | search = 'version = "{current_version}"'
25 |
26 | [[file]]
27 | src = "README.md"
28 | search = '\(v{current_version}\)'
29 | [[file]]
30 | src = "README.md"
31 | search = "finite:{current_version}"
32 |
33 | [[before_commit]]
34 | name = "compile manual"
35 | cmd = "just doc"
36 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 | out/**
3 | diff/**
4 |
5 |
--------------------------------------------------------------------------------
/tests/.ignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 | **.png
3 | **.svg
4 | **.pdf
5 |
--------------------------------------------------------------------------------
/tests/accepts/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 |
--------------------------------------------------------------------------------
/tests/accepts/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/1.png
--------------------------------------------------------------------------------
/tests/accepts/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/2.png
--------------------------------------------------------------------------------
/tests/accepts/diff/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/3.png
--------------------------------------------------------------------------------
/tests/accepts/diff/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/diff/4.png
--------------------------------------------------------------------------------
/tests/accepts/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/1.png
--------------------------------------------------------------------------------
/tests/accepts/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/2.png
--------------------------------------------------------------------------------
/tests/accepts/out/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/3.png
--------------------------------------------------------------------------------
/tests/accepts/out/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/out/4.png
--------------------------------------------------------------------------------
/tests/accepts/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/1.png
--------------------------------------------------------------------------------
/tests/accepts/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/2.png
--------------------------------------------------------------------------------
/tests/accepts/ref/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/3.png
--------------------------------------------------------------------------------
/tests/accepts/ref/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/accepts/ref/4.png
--------------------------------------------------------------------------------
/tests/accepts/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #let aut = finite.create-automaton((
6 | q0: (q1: "a"),
7 | q1: (q1: ("a", "b"), q2: "c"),
8 | q2: (q0: "b"),
9 | ))
10 |
11 | #finite.automaton(aut)
12 |
13 | #finite.accepts(aut, "abc")
14 |
15 | #pagebreak()
16 |
17 | #finite.accepts(
18 | aut,
19 | "abbaac",
20 | format: (spec, states) => {
21 | let style = (:)
22 | for i in range(states.len() - 1) {
23 | let key = str(states.at(i).first()) + "-" + str(states.at(i + 1).first())
24 | style.insert(key, (stroke: red))
25 | style.insert(states.at(i).first(), (stroke: red))
26 | style.insert(states.at(i + 1).first(), (stroke: red))
27 | }
28 | finite.automaton(spec, style: style)
29 | },
30 | )
31 |
32 | #pagebreak()
33 |
34 | #let aut = finite.create-automaton((
35 | q0: (q1: "la"),
36 | q1: (q1: ("la", "le"), q2: "lu"),
37 | q2: (q0: "lu"),
38 | ))
39 |
40 | #finite.automaton(aut)
41 |
42 | #finite.accepts(aut, "lalelu")
43 |
44 | #finite.accepts(aut, "lalalulu")
45 |
46 | #pagebreak()
47 |
48 | #let aut = finite.create-automaton((
49 | q0: (q1: "la"),
50 | q1: (q1: (1, "x"), q2: "foo"),
51 | q2: (q0: 0),
52 | ))
53 |
54 | #finite.automaton(aut)
55 |
56 | #finite.accepts(aut, "la1xfoo")
57 |
--------------------------------------------------------------------------------
/tests/flaci-load/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 |
--------------------------------------------------------------------------------
/tests/flaci-load/DEA-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TEIL2: Wort mit gerader Anzahl an Nullen",
3 | "description": "",
4 | "type": "DEA",
5 | "automaton": {
6 | "acceptCache": [],
7 | "simulationInput": [],
8 | "Alphabet": [
9 | "0",
10 | "1",
11 | "2",
12 | "3",
13 | "4",
14 | "5",
15 | "6",
16 | "7",
17 | "8",
18 | "9"
19 | ],
20 | "StackAlphabet": [
21 | "|"
22 | ],
23 | "States": [
24 | {
25 | "ID": 1,
26 | "Name": "q0",
27 | "x": 220,
28 | "y": 130,
29 | "Final": false,
30 | "Radius": 30,
31 | "Transitions": [
32 | {
33 | "Source": 1,
34 | "Target": 1,
35 | "x": 0,
36 | "y": -150,
37 | "Labels": [
38 | "1",
39 | "2",
40 | "3",
41 | "4",
42 | "5",
43 | "6",
44 | "7",
45 | "8",
46 | "9"
47 | ]
48 | },
49 | {
50 | "Source": 1,
51 | "Target": 2,
52 | "x": 0,
53 | "y": -80,
54 | "Labels": [
55 | "0"
56 | ]
57 | }
58 | ],
59 | "Start": true
60 | },
61 | {
62 | "ID": 2,
63 | "Name": "q1",
64 | "x": 510,
65 | "y": 130,
66 | "Final": true,
67 | "Radius": 30,
68 | "Transitions": [
69 | {
70 | "Source": 2,
71 | "Target": 2,
72 | "x": 0,
73 | "y": -150,
74 | "Labels": [
75 | "1",
76 | "2",
77 | "3",
78 | "4",
79 | "5",
80 | "6",
81 | "7",
82 | "8",
83 | "9"
84 | ]
85 | },
86 | {
87 | "Source": 2,
88 | "Target": 1,
89 | "x": 0,
90 | "y": -50,
91 | "Labels": [
92 | "0"
93 | ]
94 | }
95 | ],
96 | "Start": false
97 | }
98 | ],
99 | "lastInputs": []
100 | },
101 | "GUID": "hiicmv2p1"
102 | }
103 |
--------------------------------------------------------------------------------
/tests/flaci-load/DEA-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Schatzsuche",
3 | "description": "",
4 | "type": "DEA",
5 | "automaton": {
6 | "acceptCache": [],
7 | "simulationInput": [
8 | "I",
9 | "I",
10 | "I"
11 | ],
12 | "Alphabet": [
13 | "I",
14 | "V"
15 | ],
16 | "StackAlphabet": [
17 | "|"
18 | ],
19 | "States": [
20 | {
21 | "ID": 1,
22 | "Name": "q0",
23 | "x": 220,
24 | "y": 450,
25 | "Final": false,
26 | "Radius": 30,
27 | "Transitions": [
28 | {
29 | "Source": 1,
30 | "Target": 2,
31 | "x": -20,
32 | "y": 20,
33 | "Labels": [
34 | "I"
35 | ]
36 | },
37 | {
38 | "Source": 1,
39 | "Target": 5,
40 | "x": -20,
41 | "y": 30,
42 | "Labels": [
43 | "V"
44 | ]
45 | }
46 | ],
47 | "Start": true
48 | },
49 | {
50 | "ID": 2,
51 | "Name": "q1",
52 | "x": 360,
53 | "y": 550,
54 | "Final": true,
55 | "Radius": 30,
56 | "Transitions": [
57 | {
58 | "Source": 2,
59 | "Target": 3,
60 | "x": -10,
61 | "y": 10,
62 | "Labels": [
63 | "I"
64 | ]
65 | },
66 | {
67 | "Source": 2,
68 | "Target": 4,
69 | "x": -20,
70 | "y": 20,
71 | "Labels": [
72 | "V"
73 | ]
74 | }
75 | ],
76 | "Start": false
77 | },
78 | {
79 | "ID": 3,
80 | "Name": "q2",
81 | "x": 640,
82 | "y": 510,
83 | "Final": true,
84 | "Radius": 30,
85 | "Transitions": [
86 | {
87 | "Source": 3,
88 | "Target": 4,
89 | "x": 10,
90 | "y": -10,
91 | "Labels": [
92 | "I"
93 | ]
94 | },
95 | {
96 | "Source": 3,
97 | "Target": 9,
98 | "x": 0,
99 | "y": 0,
100 | "Labels": [
101 | "V"
102 | ]
103 | }
104 | ],
105 | "Start": false
106 | },
107 | {
108 | "ID": 4,
109 | "Name": "q3",
110 | "x": 770,
111 | "y": 650,
112 | "Final": true,
113 | "Radius": 30,
114 | "Transitions": [
115 | {
116 | "Source": 4,
117 | "Target": 9,
118 | "x": 0,
119 | "y": 0,
120 | "Labels": [
121 | "I",
122 | "V"
123 | ]
124 | }
125 | ],
126 | "Start": false
127 | },
128 | {
129 | "ID": 5,
130 | "Name": "q4",
131 | "x": 360,
132 | "y": 360,
133 | "Final": true,
134 | "Radius": 30,
135 | "Transitions": [
136 | {
137 | "Source": 5,
138 | "Target": 8,
139 | "x": 0,
140 | "y": 0,
141 | "Labels": [
142 | "I"
143 | ]
144 | },
145 | {
146 | "Source": 5,
147 | "Target": 9,
148 | "x": 0,
149 | "y": 0,
150 | "Labels": [
151 | "V"
152 | ]
153 | }
154 | ],
155 | "Start": false
156 | },
157 | {
158 | "ID": 8,
159 | "Name": "q7",
160 | "x": 510,
161 | "y": 400,
162 | "Final": true,
163 | "Radius": 30,
164 | "Transitions": [
165 | {
166 | "Source": 8,
167 | "Target": 3,
168 | "x": 0,
169 | "y": 0,
170 | "Labels": [
171 | "I"
172 | ]
173 | },
174 | {
175 | "Source": 8,
176 | "Target": 9,
177 | "x": 0,
178 | "y": 0,
179 | "Labels": [
180 | "V"
181 | ]
182 | }
183 | ],
184 | "Start": false
185 | },
186 | {
187 | "ID": 9,
188 | "Name": "TRAP",
189 | "x": 810,
190 | "y": 370,
191 | "Final": false,
192 | "Radius": 30,
193 | "Transitions": [
194 | {
195 | "Source": 9,
196 | "Target": 9,
197 | "x": 0,
198 | "y": -40,
199 | "Labels": [
200 | "I",
201 | "V"
202 | ]
203 | }
204 | ],
205 | "Start": false
206 | }
207 | ],
208 | "lastInputs": [
209 | [
210 | "I",
211 | "I",
212 | "I"
213 | ]
214 | ]
215 | },
216 | "GUID": "rz7hb617s"
217 | }
218 |
--------------------------------------------------------------------------------
/tests/flaci-load/DEA-3.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Plusterme",
3 | "description": "",
4 | "type": "DEA",
5 | "automaton": {
6 | "acceptCache": [],
7 | "simulationInput": [],
8 | "Alphabet": [
9 | "0",
10 | "1",
11 | "2",
12 | "3",
13 | "4",
14 | "5",
15 | "6",
16 | "7",
17 | "8",
18 | "9",
19 | "+",
20 | "OPERATOR",
21 | "OPERAND"
22 | ],
23 | "StackAlphabet": [
24 | "|"
25 | ],
26 | "States": [
27 | {
28 | "ID": 1,
29 | "Name": "q0",
30 | "x": 170,
31 | "y": 160,
32 | "Final": false,
33 | "Radius": 30,
34 | "Transitions": [
35 | {
36 | "Source": 1,
37 | "Target": 2,
38 | "x": 0,
39 | "y": 0,
40 | "Labels": [
41 | "1",
42 | "2",
43 | "3",
44 | "4",
45 | "5",
46 | "6",
47 | "7",
48 | "8",
49 | "9"
50 | ]
51 | },
52 | {
53 | "Source": 1,
54 | "Target": 3,
55 | "x": 0,
56 | "y": 0,
57 | "Labels": [
58 | "0"
59 | ]
60 | }
61 | ],
62 | "Start": false
63 | },
64 | {
65 | "ID": 2,
66 | "Name": "q1",
67 | "x": 390,
68 | "y": 160,
69 | "Final": false,
70 | "Radius": 30,
71 | "Transitions": [
72 | {
73 | "Source": 2,
74 | "Target": 4,
75 | "x": 0,
76 | "y": 0,
77 | "Labels": [
78 | "+"
79 | ]
80 | },
81 | {
82 | "Source": 2,
83 | "Target": 2,
84 | "x": 0,
85 | "y": -150,
86 | "Labels": [
87 | "0",
88 | "1",
89 | "2",
90 | "3",
91 | "4",
92 | "5",
93 | "6",
94 | "7",
95 | "8",
96 | "9"
97 | ]
98 | }
99 | ],
100 | "Start": false
101 | },
102 | {
103 | "ID": 3,
104 | "Name": "q2",
105 | "x": 420,
106 | "y": 330,
107 | "Final": false,
108 | "Radius": 30,
109 | "Transitions": [
110 | {
111 | "Source": 3,
112 | "Target": 4,
113 | "x": 0,
114 | "y": 0,
115 | "Labels": [
116 | "+"
117 | ]
118 | }
119 | ],
120 | "Start": false
121 | },
122 | {
123 | "ID": 4,
124 | "Name": "q3",
125 | "x": 630,
126 | "y": 170,
127 | "Final": false,
128 | "Radius": 30,
129 | "Transitions": [
130 | {
131 | "Source": 4,
132 | "Target": 5,
133 | "x": 0,
134 | "y": 0,
135 | "Labels": [
136 | "1",
137 | "2",
138 | "3",
139 | "4",
140 | "5",
141 | "6",
142 | "7",
143 | "8",
144 | "9"
145 | ]
146 | },
147 | {
148 | "Source": 4,
149 | "Target": 6,
150 | "x": 0,
151 | "y": 0,
152 | "Labels": [
153 | "0"
154 | ]
155 | }
156 | ],
157 | "Start": false
158 | },
159 | {
160 | "ID": 5,
161 | "Name": "q4",
162 | "x": 860,
163 | "y": 170,
164 | "Final": true,
165 | "Radius": 30,
166 | "Transitions": [
167 | {
168 | "Source": 5,
169 | "Target": 5,
170 | "x": 0,
171 | "y": -150,
172 | "Labels": [
173 | "0",
174 | "1",
175 | "2",
176 | "3",
177 | "4",
178 | "5",
179 | "6",
180 | "7",
181 | "8",
182 | "9"
183 | ]
184 | }
185 | ],
186 | "Start": false
187 | },
188 | {
189 | "ID": 6,
190 | "Name": "q5",
191 | "x": 860,
192 | "y": 310,
193 | "Final": true,
194 | "Radius": 30,
195 | "Transitions": [],
196 | "Start": false
197 | },
198 | {
199 | "ID": 7,
200 | "Name": "s0",
201 | "x": 230,
202 | "y": 430,
203 | "Final": false,
204 | "Radius": 30,
205 | "Transitions": [
206 | {
207 | "Source": 7,
208 | "Target": 8,
209 | "x": 0,
210 | "y": 0,
211 | "Labels": [
212 | "OPERAND"
213 | ]
214 | }
215 | ],
216 | "Start": true
217 | },
218 | {
219 | "ID": 8,
220 | "Name": "s1",
221 | "x": 400,
222 | "y": 430,
223 | "Final": false,
224 | "Radius": 30,
225 | "Transitions": [
226 | {
227 | "Source": 8,
228 | "Target": 9,
229 | "x": 0,
230 | "y": 0,
231 | "Labels": [
232 | "OPERATOR"
233 | ]
234 | }
235 | ],
236 | "Start": false
237 | },
238 | {
239 | "ID": 9,
240 | "Name": "s2",
241 | "x": 590,
242 | "y": 430,
243 | "Final": false,
244 | "Radius": 30,
245 | "Transitions": [
246 | {
247 | "Source": 9,
248 | "Target": 10,
249 | "x": 0,
250 | "y": 0,
251 | "Labels": [
252 | "OPERAND"
253 | ]
254 | }
255 | ],
256 | "Start": false
257 | },
258 | {
259 | "ID": 10,
260 | "Name": "s3",
261 | "x": 770,
262 | "y": 430,
263 | "Final": true,
264 | "Radius": 30,
265 | "Transitions": [],
266 | "Start": false
267 | }
268 | ],
269 | "lastInputs": []
270 | },
271 | "GUID": "f75paviy0"
272 | }
273 |
--------------------------------------------------------------------------------
/tests/flaci-load/NEA-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "[NEA] abacc",
3 | "description": "",
4 | "type": "NEA",
5 | "automaton": {
6 | "simulationInput": [],
7 | "Alphabet": [
8 | "a",
9 | "b",
10 | "c"
11 | ],
12 | "StackAlphabet": [
13 | "|"
14 | ],
15 | "States": [
16 | {
17 | "ID": 1,
18 | "Name": "q0",
19 | "x": 150,
20 | "y": 150,
21 | "Final": false,
22 | "Radius": 30,
23 | "Transitions": [
24 | {
25 | "Source": 1,
26 | "Target": 2,
27 | "x": 0,
28 | "y": 0,
29 | "Labels": [
30 | "a"
31 | ]
32 | }
33 | ],
34 | "Start": true
35 | },
36 | {
37 | "ID": 2,
38 | "Name": "q1",
39 | "x": 320,
40 | "y": 150,
41 | "Final": true,
42 | "Radius": 30,
43 | "Transitions": [
44 | {
45 | "Source": 2,
46 | "Target": 2,
47 | "x": 0,
48 | "y": -150,
49 | "Labels": [
50 | "a"
51 | ]
52 | },
53 | {
54 | "Source": 2,
55 | "Target": 3,
56 | "x": 20,
57 | "y": 0,
58 | "Labels": [
59 | "b",
60 | "c"
61 | ]
62 | },
63 | {
64 | "Source": 2,
65 | "Target": 4,
66 | "x": 50,
67 | "y": -20,
68 | "Labels": [
69 | "b",
70 | "c"
71 | ]
72 | }
73 | ],
74 | "Start": false
75 | },
76 | {
77 | "ID": 3,
78 | "Name": "q2",
79 | "x": 320,
80 | "y": 310,
81 | "Final": false,
82 | "Radius": 30,
83 | "Transitions": [
84 | {
85 | "Source": 3,
86 | "Target": 2,
87 | "x": 40,
88 | "y": 0,
89 | "Labels": [
90 | "a"
91 | ]
92 | }
93 | ],
94 | "Start": false
95 | },
96 | {
97 | "ID": 4,
98 | "Name": "q3",
99 | "x": 500,
100 | "y": 150,
101 | "Final": true,
102 | "Radius": 30,
103 | "Transitions": [
104 | {
105 | "Source": 4,
106 | "Target": 2,
107 | "x": 50,
108 | "y": -10,
109 | "Labels": [
110 | "c"
111 | ]
112 | }
113 | ],
114 | "Start": false
115 | }
116 | ],
117 | "lastInputs": []
118 | },
119 | "GUID": "ro3ujamz"
120 | }
121 |
--------------------------------------------------------------------------------
/tests/flaci-load/NKA-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Vorabi 24",
3 | "description": "",
4 | "type": "NKA",
5 | "automaton": {
6 | "Alphabet": [
7 | "X",
8 | "$",
9 | "1"
10 | ],
11 | "StackAlphabet": [
12 | "#",
13 | "A",
14 | "B",
15 | "C",
16 | "X"
17 | ],
18 | "States": [
19 | {
20 | "ID": 1,
21 | "Name": "q0",
22 | "x": 150,
23 | "y": 150,
24 | "Radius": 30,
25 | "Transitions": [
26 | {
27 | "Source": 1,
28 | "Target": 2,
29 | "x": 0,
30 | "y": 0,
31 | "Labels": [
32 | [
33 | "#",
34 | "",
35 | [
36 | "A",
37 | "C",
38 | "A",
39 | "B",
40 | "#"
41 | ]
42 | ]
43 | ]
44 | }
45 | ],
46 | "Start": true,
47 | "Final": false
48 | },
49 | {
50 | "ID": 3,
51 | "Name": "q2",
52 | "x": 590,
53 | "y": 150,
54 | "Radius": 30,
55 | "Transitions": [],
56 | "Start": false,
57 | "Final": true
58 | },
59 | {
60 | "ID": 2,
61 | "Name": "q1",
62 | "x": 370,
63 | "y": 150,
64 | "Radius": 30,
65 | "Transitions": [
66 | {
67 | "Source": 2,
68 | "Target": 3,
69 | "x": 0,
70 | "y": 0,
71 | "Labels": [
72 | [
73 | "#",
74 | "",
75 | []
76 | ]
77 | ]
78 | },
79 | {
80 | "Source": 2,
81 | "Target": 2,
82 | "x": 0,
83 | "y": 40,
84 | "Labels": [
85 | [
86 | "X",
87 | "X",
88 | []
89 | ],
90 | [
91 | "C",
92 | "1",
93 | []
94 | ],
95 | [
96 | "C",
97 | "$",
98 | []
99 | ],
100 | [
101 | "A",
102 | "X",
103 | []
104 | ],
105 | [
106 | "A",
107 | "X",
108 | [
109 | "C",
110 | "X"
111 | ]
112 | ],
113 | [
114 | "B",
115 | "",
116 | [
117 | "A"
118 | ]
119 | ],
120 | [
121 | "B",
122 | "",
123 | [
124 | "A",
125 | "B"
126 | ]
127 | ]
128 | ]
129 | }
130 | ],
131 | "Start": false,
132 | "Final": false
133 | }
134 | ],
135 | "acceptCache": [],
136 | "simulationInput": [
137 | "X",
138 | "$",
139 | "X",
140 | "$",
141 | "X",
142 | "X",
143 | "1",
144 | "X",
145 | "X",
146 | "X",
147 | "$",
148 | "X"
149 | ],
150 | "lastInputs": [
151 | [
152 | "X",
153 | "$",
154 | "X",
155 | "$",
156 | "X",
157 | "X",
158 | "1",
159 | "X",
160 | "X",
161 | "X",
162 | "$",
163 | "X"
164 | ],
165 | [
166 | "X",
167 | "X",
168 | "$",
169 | "X"
170 | ],
171 | [
172 | "$",
173 | "X",
174 | "X",
175 | "1",
176 | "X"
177 | ],
178 | [
179 | "X",
180 | "$",
181 | "X",
182 | "X",
183 | "1",
184 | "X"
185 | ],
186 | [
187 | "X",
188 | "$",
189 | "X",
190 | "X"
191 | ],
192 | [
193 | "X",
194 | "X",
195 | "X",
196 | "X"
197 | ]
198 | ]
199 | },
200 | "GUID": "ibbsud0or"
201 | }
202 |
--------------------------------------------------------------------------------
/tests/flaci-load/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/1.png
--------------------------------------------------------------------------------
/tests/flaci-load/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/2.png
--------------------------------------------------------------------------------
/tests/flaci-load/diff/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/3.png
--------------------------------------------------------------------------------
/tests/flaci-load/diff/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/diff/4.png
--------------------------------------------------------------------------------
/tests/flaci-load/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/1.png
--------------------------------------------------------------------------------
/tests/flaci-load/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/2.png
--------------------------------------------------------------------------------
/tests/flaci-load/out/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/3.png
--------------------------------------------------------------------------------
/tests/flaci-load/out/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/out/4.png
--------------------------------------------------------------------------------
/tests/flaci-load/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/1.png
--------------------------------------------------------------------------------
/tests/flaci-load/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/2.png
--------------------------------------------------------------------------------
/tests/flaci-load/ref/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/3.png
--------------------------------------------------------------------------------
/tests/flaci-load/ref/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/flaci-load/ref/4.png
--------------------------------------------------------------------------------
/tests/flaci-load/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ": automaton, flaci
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #let automatons = (
6 | "DEA-1",
7 | "DEA-2",
8 | "DEA-3", // With BOM
9 | "NEA-1",
10 | // "NKA-1", // Not yet supported
11 | )
12 |
13 | #for file in automatons {
14 | page[
15 | #let aut = read(file + ".json")
16 | #flaci.automaton(aut, style: (q1: (fill: green)))
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/large-automaton/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/large-automaton/diff/1.png
--------------------------------------------------------------------------------
/tests/large-automaton/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/large-automaton/out/1.png
--------------------------------------------------------------------------------
/tests/large-automaton/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/large-automaton/ref/1.png
--------------------------------------------------------------------------------
/tests/large-automaton/test.typ:
--------------------------------------------------------------------------------
1 | /// [skip]
2 |
3 | #import "../../src/finite.typ"
4 |
5 | #set page(width: auto, height: auto, margin: 1cm)
6 |
7 | #let spacing = 2
8 | #let splay = 5
9 | #finite.automaton(
10 | (
11 | q0: (q1: "λ", q5: "λ"),
12 | q1: (q2: 0),
13 | q2: (q3: 0),
14 | q3: (q4: 0),
15 | q4: (q1: "λ", q5: "λ"),
16 | q5: (q6: "λ"),
17 | q6: (q7: "λ", q9: "λ", q13: "λ"),
18 | q7: (q8: "λ"),
19 | q8: (q17: "λ"),
20 | q9: (q10: 0),
21 | q10: (q11: 1),
22 | q11: (q12: 1),
23 | q12: (q17: "λ"),
24 | q13: (q14: 0),
25 | q14: (q15: 0),
26 | q15: (q16: 1),
27 | q16: (q17: "λ"),
28 | q17: (q18: "λ"),
29 | q18: (q19: "λ", q23: "λ"),
30 | q19: (q20: 1),
31 | q20: (q21: 1),
32 | q21: (q22: 1),
33 | q22: (q19: "λ", q23: "λ"),
34 | q23: none,
35 | ),
36 | layout: (
37 | q0: (0.6, 0),
38 | q1: (spacing, 0),
39 | q2: (2 * spacing, 0),
40 | q3: (3 * spacing, 0),
41 | q4: (4 * spacing, 0),
42 | q5: (5 * spacing, 0),
43 | q6: (5 * spacing, -splay),
44 | q7: (3 * spacing, spacing - splay),
45 | q8: (2 * spacing, spacing - splay),
46 | q9: (4 * spacing, -splay),
47 | q10: (3 * spacing, -splay),
48 | q11: (2 * spacing, -splay),
49 | q12: (2 * spacing, -splay),
50 | q13: (4 * spacing, -spacing - splay),
51 | q14: (3 * spacing, -spacing - splay),
52 | q15: (2 * spacing, -spacing - splay),
53 | q16: (1 * spacing, -spacing - splay),
54 | q17: (0, -splay),
55 | q18: (0, -2 * splay),
56 | q19: (spacing, -2 * splay),
57 | q20: (2 * spacing, -2 * splay),
58 | q21: (3 * spacing, -2 * splay),
59 | q22: (4 * spacing, -2 * splay),
60 | q23: (5 * spacing, -2 * splay),
61 | ),
62 | style: (
63 | transition: (
64 | curve: 0,
65 | label: (angle: 0deg),
66 | ),
67 | q0-q5: (curve: 1.5),
68 | q4-q1: (curve: 1.5),
69 | q18-q23: (curve: 1.5),
70 | q22-q19: (curve: 1.5),
71 | ),
72 | )
73 |
74 |
--------------------------------------------------------------------------------
/tests/layout-circular/.gitignore:
--------------------------------------------------------------------------------
1 | # generated by tytanic, do not edit
2 |
3 | diff/**
4 | out/**
5 |
--------------------------------------------------------------------------------
/tests/layout-circular/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layout-circular/ref/1.png
--------------------------------------------------------------------------------
/tests/layout-circular/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #let aut = range(6).fold(
6 | (:),
7 | (d, i) => {
8 | d.insert("q" + str(i), (:))
9 | d
10 | },
11 | )
12 |
13 | #finite.automaton(
14 | aut,
15 | layout: finite.layout.circular.with(offset: 30deg),
16 | style: (
17 | state: (radius: .8),
18 | q1: (radius: 1),
19 | q4: (radius: .4),
20 | ),
21 | )
22 |
--------------------------------------------------------------------------------
/tests/layouts/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 |
--------------------------------------------------------------------------------
/tests/layouts/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/1.png
--------------------------------------------------------------------------------
/tests/layouts/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/2.png
--------------------------------------------------------------------------------
/tests/layouts/diff/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/3.png
--------------------------------------------------------------------------------
/tests/layouts/diff/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/4.png
--------------------------------------------------------------------------------
/tests/layouts/diff/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/5.png
--------------------------------------------------------------------------------
/tests/layouts/diff/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/diff/6.png
--------------------------------------------------------------------------------
/tests/layouts/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/1.png
--------------------------------------------------------------------------------
/tests/layouts/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/2.png
--------------------------------------------------------------------------------
/tests/layouts/out/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/3.png
--------------------------------------------------------------------------------
/tests/layouts/out/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/4.png
--------------------------------------------------------------------------------
/tests/layouts/out/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/5.png
--------------------------------------------------------------------------------
/tests/layouts/out/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/out/6.png
--------------------------------------------------------------------------------
/tests/layouts/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/1.png
--------------------------------------------------------------------------------
/tests/layouts/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/2.png
--------------------------------------------------------------------------------
/tests/layouts/ref/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/3.png
--------------------------------------------------------------------------------
/tests/layouts/ref/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/4.png
--------------------------------------------------------------------------------
/tests/layouts/ref/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/5.png
--------------------------------------------------------------------------------
/tests/layouts/ref/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/layouts/ref/6.png
--------------------------------------------------------------------------------
/tests/layouts/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #let aut = range(6).fold(
6 | (:),
7 | (d, i) => {
8 | d.insert("q" + str(i), ())
9 | d
10 | },
11 | )
12 | #let spec = finite.create-automaton(aut)
13 |
14 | === layout.custom
15 | #finite.automaton(
16 | aut,
17 | layout: (
18 | q0: (0, 0),
19 | q1: (4, 0),
20 | q2: (rel: (0, -3)),
21 | q3: (rel: (-1, -3), to: "q0"),
22 | rest: (rel: (2, -2)),
23 | ),
24 | )
25 |
26 | #pagebreak()
27 |
28 | === layout.linear
29 | #finite.automaton(
30 | aut,
31 | layout: finite.layout.linear,
32 | )
33 |
34 | #pagebreak()
35 | === layout.circular
36 | #finite.automaton(
37 | aut,
38 | layout: finite.layout.circular.with(offset: 30deg),
39 | style: (state: (radius: .8), q1: (radius: 1)),
40 | )
41 |
42 | #pagebreak()
43 | === layout.grid
44 | #finite.automaton(
45 | aut,
46 | layout: finite.layout.grid,
47 | )
48 |
49 | #pagebreak()
50 | === layout.snake
51 | #finite.automaton(
52 | aut,
53 | layout: finite.layout.snake,
54 | )
55 |
56 | #pagebreak()
57 | === layout.group
58 | #finite.automaton(
59 | aut,
60 | layout: finite.layout.group.with(
61 | grouping: 4,
62 | spacing: 2,
63 | layout: (
64 | finite.layout.circular.with(radius: 8),
65 | finite.layout.linear.with(dir: top),
66 | ),
67 | ),
68 | )
69 |
--------------------------------------------------------------------------------
/tests/spec/.gitignore:
--------------------------------------------------------------------------------
1 | # generated by tytanic, do not edit
2 |
3 | diff/**
4 | out/**
5 |
--------------------------------------------------------------------------------
/tests/spec/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #let aut = (
4 | q0: (q1: "λ", q5: "λ"),
5 | q1: (q2: 0),
6 | q2: (q3: 0),
7 | q3: (q4: 0),
8 | q4: (q1: "λ", q5: "λ"),
9 | q5: (q6: "λ"),
10 | q6: (q7: "λ", q9: "λ", q13: "λ"),
11 | q7: (q8: "λ"),
12 | q8: (q17: "λ"),
13 | q9: (q10: 0),
14 | q10: (q11: 1),
15 | q11: (q12: 1),
16 | q12: (q17: "λ"),
17 | q13: (q14: 0),
18 | q14: (q15: 0),
19 | q15: (q16: 1),
20 | q16: (q17: "λ"),
21 | q17: (q18: "λ"),
22 | q18: (q19: "λ", q23: "λ"),
23 | q19: (q20: 1),
24 | q20: (q21: 1),
25 | q21: (q22: 1),
26 | q22: (q19: "λ", q23: "λ"),
27 | q23: none,
28 | )
29 |
30 | #let spec = finite.create-automaton(aut)
31 |
32 | #assert.eq(spec.type, "NEA")
33 | #assert.eq(spec.initial, "q0")
34 | #assert.eq(spec.final, ("q23",))
35 | #assert.eq(spec.states.sorted(), aut.keys().sorted())
36 | #assert.eq(spec.inputs.sorted(), ("0", "1", "λ"))
37 |
--------------------------------------------------------------------------------
/tests/state-anchors/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 |
--------------------------------------------------------------------------------
/tests/state-anchors/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/diff/1.png
--------------------------------------------------------------------------------
/tests/state-anchors/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/diff/2.png
--------------------------------------------------------------------------------
/tests/state-anchors/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/out/1.png
--------------------------------------------------------------------------------
/tests/state-anchors/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/out/2.png
--------------------------------------------------------------------------------
/tests/state-anchors/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/ref/1.png
--------------------------------------------------------------------------------
/tests/state-anchors/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-anchors/ref/2.png
--------------------------------------------------------------------------------
/tests/state-anchors/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.cetz.canvas({
6 | import finite.cetz.draw: *
7 | import finite.draw: *
8 | import "../test-utils.typ": dot
9 |
10 | let name = "q0"
11 | let anchor = "west"
12 |
13 | state((0, 0), name, initial: true, anchor: anchor)
14 |
15 | for-each-anchor(
16 | name,
17 | exclude: ("initial",),
18 | anchor => {
19 | dot(name + "." + anchor)
20 | },
21 | )
22 | dot(name + ".initial.start")
23 | dot(name + ".initial.end")
24 | })
25 |
26 | #pagebreak()
27 |
28 | #finite.cetz.canvas({
29 | import finite.cetz.draw: *
30 | import finite.draw: *
31 | import "../test-utils.typ": dot
32 |
33 | let name = "q0"
34 | let anchor = "west"
35 |
36 | state((0, 0), name, final: true, anchor: anchor)
37 |
38 | for-each-anchor(
39 | name,
40 | exclude: ("initial",),
41 | anchor => {
42 | dot(name + "." + anchor)
43 | },
44 | )
45 | })
46 |
--------------------------------------------------------------------------------
/tests/state-final/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | out
3 |
4 | diff
5 |
--------------------------------------------------------------------------------
/tests/state-final/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-final/ref/1.png
--------------------------------------------------------------------------------
/tests/state-final/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.cetz.canvas({
6 | import finite.draw: state
7 |
8 | state((0, 0), "q0", final: true)
9 | state((2, 2), "q1", final: true, stroke: red)
10 | })
11 |
--------------------------------------------------------------------------------
/tests/state-initial/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | out
3 |
4 | diff
5 |
--------------------------------------------------------------------------------
/tests/state-initial/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-initial/ref/1.png
--------------------------------------------------------------------------------
/tests/state-initial/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-initial/ref/2.png
--------------------------------------------------------------------------------
/tests/state-initial/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.cetz.canvas({
6 | import finite.draw: state
7 |
8 | state((0, 0), "q0", initial: true)
9 | state((2, 2), "q1", initial: "Initial")
10 | state((4, 0), "q2", initial: bottom + right)
11 | state((2, -2), "q2", initial: (label: "Foo", anchor: bottom + left))
12 | })
13 |
14 | #pagebreak()
15 |
16 |
17 | #finite.cetz.canvas({
18 | import finite.draw: state
19 |
20 | state(
21 | (0, 0),
22 | "A",
23 | initial: (
24 | label: (
25 | text: "Init",
26 | size: 16pt,
27 | ),
28 | anchor: bottom,
29 | ),
30 | stroke: red,
31 | )
32 | })
33 |
--------------------------------------------------------------------------------
/tests/state-labels/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/diff/1.png
--------------------------------------------------------------------------------
/tests/state-labels/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/diff/2.png
--------------------------------------------------------------------------------
/tests/state-labels/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/out/1.png
--------------------------------------------------------------------------------
/tests/state-labels/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/out/2.png
--------------------------------------------------------------------------------
/tests/state-labels/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/ref/1.png
--------------------------------------------------------------------------------
/tests/state-labels/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state-labels/ref/2.png
--------------------------------------------------------------------------------
/tests/state-labels/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.automaton(
6 | (q0: (q1: 1), q1: (q0: 0)),
7 | style: (
8 | q0-q1: (label: (pos: 0.8)),
9 | q1-q0: (label: (pos: 0.2)),
10 | ),
11 | )
12 |
13 | #pagebreak()
14 |
15 | #finite.automaton(
16 | (q0: (q1: 1), q1: (q0: 0)),
17 | style: (
18 | q0-q1: (label: (pos: 0.8)),
19 | transition: (label: (pos: 0.2)),
20 | ),
21 | )
22 |
--------------------------------------------------------------------------------
/tests/state/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | out
3 |
4 | diff
5 |
--------------------------------------------------------------------------------
/tests/state/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state/ref/1.png
--------------------------------------------------------------------------------
/tests/state/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/state/ref/2.png
--------------------------------------------------------------------------------
/tests/state/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.cetz.canvas({
6 | import finite.draw: state
7 |
8 | state((0, 0), "q0")
9 | state((2, 2), "q1", stroke: red)
10 | state((4, 0), "q2", stroke: red, fill: red.lighten(80%))
11 | state((2, -2), "q3", stroke: red, fill: red.lighten(80%), label: "T")
12 | })
13 |
14 | #pagebreak()
15 |
16 |
--------------------------------------------------------------------------------
/tests/test-utils.typ:
--------------------------------------------------------------------------------
1 | #import "../src/util.typ": cetz
2 |
3 | #let dot = cetz.draw.circle.with(fill: red, stroke: none, radius: .1)
4 |
--------------------------------------------------------------------------------
/tests/transition-labels/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 |
--------------------------------------------------------------------------------
/tests/transition-labels/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/diff/1.png
--------------------------------------------------------------------------------
/tests/transition-labels/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/diff/2.png
--------------------------------------------------------------------------------
/tests/transition-labels/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/out/1.png
--------------------------------------------------------------------------------
/tests/transition-labels/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/out/2.png
--------------------------------------------------------------------------------
/tests/transition-labels/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/ref/1.png
--------------------------------------------------------------------------------
/tests/transition-labels/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition-labels/ref/2.png
--------------------------------------------------------------------------------
/tests/transition-labels/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.cetz.canvas({
6 | import finite.draw: state, transition
7 |
8 | state((0, 0), "q0")
9 | state((4, 0), "q1")
10 |
11 | transition("q0", "q1", inputs: (1, 2, 3, 4))
12 | transition("q1", "q0", inputs: (1, 2, 3, 4), label: "A")
13 | transition("q1", "q1", label: "1,2,3")
14 | })
15 |
16 | #pagebreak()
17 |
18 | #finite.cetz.canvas({
19 | import finite.draw: state, transition
20 |
21 | state((0, 0), "q0")
22 | state((4, 0), "q1")
23 | state((4, -4), "q2")
24 | state((0, -4), "q3")
25 |
26 | transition("q0", "q1", label: "A")
27 | transition("q1", "q2", label: "A", stroke: blue)
28 | transition("q2", "q3", label: (text: "A"))
29 | transition("q3", "q0", label: (text: "A", size: 2em))
30 | transition("q2", "q1", label: (text: "A", fill: green, pos: .8, dist: .88), stroke: blue)
31 | transition("q0", "q3", label: (text: "A", size: 2em, pos: 0.2, dist: -.33, angle: 0deg), stroke: blue)
32 | })
33 |
--------------------------------------------------------------------------------
/tests/transition/.gitignore:
--------------------------------------------------------------------------------
1 | # added by typst-test
2 |
--------------------------------------------------------------------------------
/tests/transition/diff/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/diff/1.png
--------------------------------------------------------------------------------
/tests/transition/diff/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/diff/2.png
--------------------------------------------------------------------------------
/tests/transition/diff/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/diff/3.png
--------------------------------------------------------------------------------
/tests/transition/out/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/out/1.png
--------------------------------------------------------------------------------
/tests/transition/out/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/out/2.png
--------------------------------------------------------------------------------
/tests/transition/out/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/out/3.png
--------------------------------------------------------------------------------
/tests/transition/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/ref/1.png
--------------------------------------------------------------------------------
/tests/transition/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/ref/2.png
--------------------------------------------------------------------------------
/tests/transition/ref/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/transition/ref/3.png
--------------------------------------------------------------------------------
/tests/transition/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #finite.cetz.canvas({
6 | import finite.draw: state, transition
7 |
8 | state((0, 0), "q0")
9 | state((4, 0), "q1")
10 |
11 | transition("q0", "q1")
12 | transition("q1", "q1")
13 | })
14 |
15 | #pagebreak()
16 |
17 | #finite.cetz.canvas({
18 | import finite.draw: state, transition
19 |
20 | state((0, 0), "q0")
21 | state((4, 0), "q1")
22 |
23 | transition("q0", "q1", label: "x", curve: -1, stroke: .5pt + green)
24 | transition("q1", "q1", stroke: 2pt + red, mark: (end: "x"))
25 | })
26 |
27 | #pagebreak()
28 |
29 | #finite.cetz.canvas({
30 | import finite.draw: state, transition
31 |
32 | state((0, 0), "q0")
33 | state((4, 2), "q1")
34 |
35 | transition("q0", "q1")
36 | transition("q1", "q1", anchor: right)
37 | })
38 |
39 |
--------------------------------------------------------------------------------
/tests/ttable/.gitignore:
--------------------------------------------------------------------------------
1 | # generated by tytanic, do not edit
2 |
3 | diff/**
4 | out/**
5 |
--------------------------------------------------------------------------------
/tests/ttable/ref/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/ttable/ref/1.png
--------------------------------------------------------------------------------
/tests/ttable/ref/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/ttable/ref/2.png
--------------------------------------------------------------------------------
/tests/ttable/ref/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jneug/typst-finite/d13a578477c448a391cc85b02017f4ba7dc74059/tests/ttable/ref/3.png
--------------------------------------------------------------------------------
/tests/ttable/test.typ:
--------------------------------------------------------------------------------
1 | #import "../../src/finite.typ"
2 |
3 | #set page(width: auto, height: auto, margin: 1cm)
4 |
5 | #let aut = (
6 | q0: (q1: "a"),
7 | q1: (q1: ("a", "b"), q2: ("c", "b")),
8 | q2: (q0: "b", q2: "c"),
9 | )
10 | #finite.transition-table(aut)
11 |
12 | #pagebreak()
13 |
14 | #finite.transition-table(
15 | aut,
16 | format: (c, r, v) => if c == 0 and r == 0 [
17 | $delta$
18 | ] else if c == 0 [
19 | #strong(v)
20 | ] else {
21 | raw(str(v))
22 | },
23 | )
24 |
25 | #pagebreak()
26 |
27 | #finite.transition-table(
28 | aut,
29 | fill: (c, r) => (orange, yellow).at(calc.rem(c + r, 2)),
30 | format: (c, r, v) => text((yellow, orange).at(calc.rem(c + r, 2)), weight: "bold", v),
31 | )
32 |
--------------------------------------------------------------------------------
/typst.toml:
--------------------------------------------------------------------------------
1 |
2 | [package]
3 | name = "finite"
4 | version = "0.5.0"
5 | entrypoint = "src/finite.typ"
6 | authors = ["Jonas Neugebauer"]
7 | license = "MIT"
8 | description = "Typst-setting finite automata with CeTZ"
9 | repository = "https://github.com/jneug/typst-finite"
10 | keywords = []
11 | categories = []
12 | disciplines = []
13 | compiler = "0.13.0"
14 | exclude = [
15 | ".github",
16 | "docs",
17 | "scripts",
18 | "tests",
19 | "assets",
20 | ".typstignore",
21 | "Justfile",
22 | ]
23 |
--------------------------------------------------------------------------------