mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-06 21:24:06 -04:00
some form of highlighting
This commit is contained in:
parent
58fb1b956c
commit
7f519cd7f3
12 changed files with 209 additions and 114 deletions
|
|
@ -176,8 +176,8 @@ impl<'a, 'b> FaCompiler<'a, 'b> {
|
||||||
self.ctx.emit_error(format!("unknown item {name:?}, expected states | alphabet | final states | initial state"), dest_s);
|
self.ctx.emit_error(format!("unknown item {name:?}, expected states | alphabet | final states | initial state"), dest_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => {
|
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => {
|
||||||
self.compile_transition_function(args, list)
|
self.compile_transition_function(args, func, list)
|
||||||
}
|
}
|
||||||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||||
self.ctx.emit_error(
|
self.ctx.emit_error(
|
||||||
|
|
@ -313,6 +313,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> {
|
||||||
fn compile_transition_function(
|
fn compile_transition_function(
|
||||||
&mut self,
|
&mut self,
|
||||||
args: Spanned<ast::Tuple<'a>>,
|
args: Spanned<ast::Tuple<'a>>,
|
||||||
|
function: Span,
|
||||||
list: Spanned<ast::Item<'a>>,
|
list: Spanned<ast::Item<'a>>,
|
||||||
) {
|
) {
|
||||||
let list = list.set_weak();
|
let list = list.set_weak();
|
||||||
|
|
@ -368,8 +369,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> {
|
||||||
}
|
}
|
||||||
if let Some(previous) = entry.replace(TransitionTo {
|
if let Some(previous) = entry.replace(TransitionTo {
|
||||||
state: State(next_state.0),
|
state: State(next_state.0),
|
||||||
|
function,
|
||||||
function: args.1,
|
|
||||||
transition: item.1,
|
transition: item.1,
|
||||||
}) {
|
}) {
|
||||||
self.ctx
|
self.ctx
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,8 @@ impl<'a, 'b> PdaCompiler<'a, 'b> {
|
||||||
self.ctx.emit_error(format!("unknown item {name:?}, expected states | stack symbols | alphabet | accept by | final states | initial state | initial stack"), dest_s);
|
self.ctx.emit_error(format!("unknown item {name:?}, expected states | stack symbols | alphabet | accept by | final states | initial state | initial stack"), dest_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => {
|
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => {
|
||||||
self.compile_transition_function(args, list)
|
self.compile_transition_function(args, func, list)
|
||||||
}
|
}
|
||||||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||||
self.ctx.emit_error(
|
self.ctx.emit_error(
|
||||||
|
|
@ -476,6 +476,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> {
|
||||||
fn compile_transition_function(
|
fn compile_transition_function(
|
||||||
&mut self,
|
&mut self,
|
||||||
args: Spanned<ast::Tuple<'a>>,
|
args: Spanned<ast::Tuple<'a>>,
|
||||||
|
function: Span,
|
||||||
list: Spanned<ast::Item<'a>>,
|
list: Spanned<ast::Item<'a>>,
|
||||||
) {
|
) {
|
||||||
let list = list.set_weak();
|
let list = list.set_weak();
|
||||||
|
|
@ -559,8 +560,8 @@ impl<'a, 'b> PdaCompiler<'a, 'b> {
|
||||||
if !entry.insert(TransitionTo {
|
if !entry.insert(TransitionTo {
|
||||||
state: State(next_state.0),
|
state: State(next_state.0),
|
||||||
stack,
|
stack,
|
||||||
|
|
||||||
function: args.1,
|
function,
|
||||||
transition: item.1,
|
transition: item.1,
|
||||||
}) {
|
}) {
|
||||||
self.ctx.emit_warning("duplicate transition", item.1);
|
self.ctx.emit_warning("duplicate transition", item.1);
|
||||||
|
|
|
||||||
|
|
@ -193,8 +193,8 @@ impl<'a, 'b> TmCompiler<'a, 'b> {
|
||||||
self.ctx.emit_error(format!("unknown item {name:?}, expected states | symbols | final states | initial state | blank symbol"), dest_s);
|
self.ctx.emit_error(format!("unknown item {name:?}, expected states | symbols | final states | initial state | blank symbol"), dest_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => {
|
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => {
|
||||||
self.compile_transition_function(args, list)
|
self.compile_transition_function(args, func, list)
|
||||||
}
|
}
|
||||||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||||
self.ctx.emit_error(
|
self.ctx.emit_error(
|
||||||
|
|
@ -349,6 +349,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> {
|
||||||
fn compile_transition_function(
|
fn compile_transition_function(
|
||||||
&mut self,
|
&mut self,
|
||||||
args: Spanned<ast::Tuple<'a>>,
|
args: Spanned<ast::Tuple<'a>>,
|
||||||
|
function: Span,
|
||||||
list: Spanned<ast::Item<'a>>,
|
list: Spanned<ast::Item<'a>>,
|
||||||
) {
|
) {
|
||||||
let list = list.set_weak();
|
let list = list.set_weak();
|
||||||
|
|
@ -398,7 +399,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> {
|
||||||
symbol: Symbol(to_tape.0),
|
symbol: Symbol(to_tape.0),
|
||||||
direction: direction.0,
|
direction: direction.0,
|
||||||
|
|
||||||
function: args.1,
|
function,
|
||||||
transition: item.1,
|
transition: item.1,
|
||||||
}) {
|
}) {
|
||||||
self.ctx.emit_warning("duplicate transition", item.1);
|
self.ctx.emit_warning("duplicate transition", item.1);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
// deno-lint-ignore-file
|
// deno-lint-ignore-file
|
||||||
|
|
||||||
import type { Machine } from "./automata.ts";
|
import type { Machine, Span } from "./automata.ts";
|
||||||
import type { Example } from "./examples.ts";
|
import type { Example } from "./examples.ts";
|
||||||
import type { Sim, SimStepResult } from "./simulation.ts";
|
import type { Sim, SimStepResult } from "./simulation.ts";
|
||||||
import type wasm from "./wasm.ts";
|
import type wasm from "./wasm.ts";
|
||||||
import type { Text } from "npm:@codemirror/state";
|
import type { Text } from "npm:@codemirror/state";
|
||||||
|
import type { Highlight } from "./highlight.ts";
|
||||||
|
|
||||||
type Unsubscribe = () => void;
|
type Unsubscribe = () => void;
|
||||||
|
|
||||||
|
|
@ -69,14 +70,14 @@ type AppEvents = {
|
||||||
"editor/change": {text: string, doc: Text};
|
"editor/change": {text: string, doc: Text};
|
||||||
"compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined};
|
"compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined};
|
||||||
|
|
||||||
"automata/sim/update": { simulation: Sim|null };
|
"automata/sim/update": Sim|null;
|
||||||
"automata/sim/before_step": { simulation: Sim };
|
"automata/sim/before_step": { simulation: Sim };
|
||||||
"automata/sim/after_step": { simulation: Sim, result: SimStepResult };
|
"automata/sim/after_step": { simulation: Sim, result: SimStepResult };
|
||||||
"automata/update": { automaton: Machine };
|
"automata/update": Machine;
|
||||||
|
|
||||||
"example/selected": {example: Example};
|
"example/selected": Example;
|
||||||
|
|
||||||
"controls/editor/set_text": {text: string};
|
"controls/editor/set_text": string;
|
||||||
|
|
||||||
"controls/vis/physics": {enabled: boolean};
|
"controls/vis/physics": {enabled: boolean};
|
||||||
"controls/vis/reset_network": void;
|
"controls/vis/reset_network": void;
|
||||||
|
|
@ -85,6 +86,12 @@ type AppEvents = {
|
||||||
"controls/sim/reload": void;
|
"controls/sim/reload": void;
|
||||||
"controls/sim/clear": void;
|
"controls/sim/clear": void;
|
||||||
|
|
||||||
|
"highlight/one/add": Highlight;
|
||||||
|
"highlight/one/remove": Highlight;
|
||||||
|
"highlight/all/remove": void;
|
||||||
|
|
||||||
|
"highlight/update": void;
|
||||||
|
|
||||||
"theme/update": void;
|
"theme/update": void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ class Controls {
|
||||||
if (Controls.running) Controls.setRunning(false);
|
if (Controls.running) Controls.setRunning(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
bus.on("automata/sim/update", ({ simulation }) => {
|
bus.on("automata/sim/update", simulation => {
|
||||||
Controls.simulation_active = !!simulation;
|
Controls.simulation_active = !!simulation;
|
||||||
if (!simulation) Controls.stop();
|
if (!simulation) Controls.stop();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import wasm from "./wasm.ts";
|
||||||
import { Share } from "./share.ts";
|
import { Share } from "./share.ts";
|
||||||
import { examples } from "./examples.ts";
|
import { examples } from "./examples.ts";
|
||||||
import { bus } from "./bus.ts";
|
import { bus } from "./bus.ts";
|
||||||
|
import { current, Highlight, HighlightKind } from "./highlight.ts";
|
||||||
|
|
||||||
function tokenize(text: string): wasm.Tok[] {
|
function tokenize(text: string): wasm.Tok[] {
|
||||||
try {
|
try {
|
||||||
|
|
@ -47,25 +48,16 @@ function compile(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type HighlightKind = "focus" | "success" | "warning" | "error";
|
|
||||||
|
|
||||||
export type HighlightSpan = {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
kind: HighlightKind;
|
|
||||||
};
|
|
||||||
|
|
||||||
function decoForKind(kind: HighlightKind) {
|
function decoForKind(kind: HighlightKind) {
|
||||||
// Use a class per kind so each gets a distinct color via CSS
|
// Use a class per kind so each gets a distinct color via CSS
|
||||||
return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` });
|
return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyHighlights(view: EditorView, spans: HighlightSpan[]) {
|
bus.on("highlight/update", _ => {
|
||||||
view.dispatch({ effects: setHighlights.of(spans) });
|
const arr = current.values().toArray().sort((a, b) => a.span[0]-b.span[0]);
|
||||||
}
|
editor.dispatch({ effects: setHighlights.of(arr) });
|
||||||
|
});
|
||||||
|
export const setHighlights = StateEffect.define<Highlight[]>();
|
||||||
|
|
||||||
export const highlightsField = StateField.define<DecorationSet>({
|
export const highlightsField = StateField.define<DecorationSet>({
|
||||||
create() {
|
create() {
|
||||||
return Decoration.none;
|
return Decoration.none;
|
||||||
|
|
@ -73,7 +65,7 @@ export const highlightsField = StateField.define<DecorationSet>({
|
||||||
|
|
||||||
update(highlights, tr) {
|
update(highlights, tr) {
|
||||||
// Keep highlights aligned with document edits
|
// Keep highlights aligned with document edits
|
||||||
// highlights = highlights.map(tr.changes);
|
highlights = highlights.map(tr.changes);
|
||||||
|
|
||||||
for (const e of tr.effects) {
|
for (const e of tr.effects) {
|
||||||
if (e.is(setHighlights)) {
|
if (e.is(setHighlights)) {
|
||||||
|
|
@ -81,8 +73,9 @@ export const highlightsField = StateField.define<DecorationSet>({
|
||||||
|
|
||||||
const builder = new RangeSetBuilder<Decoration>();
|
const builder = new RangeSetBuilder<Decoration>();
|
||||||
for (const s of spans) {
|
for (const s of spans) {
|
||||||
const from = Math.max(0, Math.min(s.from, tr.state.doc.length));
|
|
||||||
const to = Math.max(0, Math.min(s.to, tr.state.doc.length));
|
const from = Math.max(0, Math.min(s.span[0], tr.state.doc.length));
|
||||||
|
const to = Math.max(0, Math.min(s.span[1], tr.state.doc.length));
|
||||||
if (to > from) builder.add(from, to, decoForKind(s.kind));
|
if (to > from) builder.add(from, to, decoForKind(s.kind));
|
||||||
}
|
}
|
||||||
highlights = builder.finish();
|
highlights = builder.finish();
|
||||||
|
|
@ -95,7 +88,6 @@ export const highlightsField = StateField.define<DecorationSet>({
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setHighlights = StateEffect.define<HighlightSpan[]>();
|
|
||||||
|
|
||||||
const eventBusConnection = StateField.define({
|
const eventBusConnection = StateField.define({
|
||||||
create(state) {
|
create(state) {
|
||||||
|
|
@ -264,23 +256,15 @@ const editor = new EditorView({
|
||||||
|
|
||||||
bus.on(
|
bus.on(
|
||||||
"begin",
|
"begin",
|
||||||
(_) => bus.emit("controls/editor/set_text", { text: defaultText() }),
|
(_) => bus.emit("controls/editor/set_text", defaultText()),
|
||||||
);
|
);
|
||||||
|
|
||||||
bus.on("controls/editor/set_text", ({ text }) => {
|
bus.on("controls/editor/set_text", text => {
|
||||||
editor.dispatch({
|
editor.dispatch({
|
||||||
changes: { from: 0, to: editor.state.doc.length, insert: text },
|
changes: { from: 0, to: editor.state.doc.length, insert: text },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
bus.on("example/selected", ({ example }) => {
|
bus.on("example/selected", example => {
|
||||||
bus.emit("controls/editor/set_text", { text: example.machine });
|
bus.emit("controls/editor/set_text", example.machine);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
applyHighlights(editor, [
|
|
||||||
{ from: 0, to: 10, kind: "focus" },
|
|
||||||
{ from: 10, to: 20, kind: "success" },
|
|
||||||
{ from: 20, to: 30, kind: "warning" },
|
|
||||||
{ from: 30, to: 40, kind: "error" },
|
|
||||||
])
|
|
||||||
|
|
@ -245,5 +245,5 @@ function buildExamplesDropdown(
|
||||||
|
|
||||||
const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement;
|
const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement;
|
||||||
buildExamplesDropdown(selectEl, examples, (example) => {
|
buildExamplesDropdown(selectEl, examples, (example) => {
|
||||||
bus.emit("example/selected", {example});
|
bus.emit("example/selected", example);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
85
web/root/src/highlight.ts
Normal file
85
web/root/src/highlight.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import type { Span } from "./automata.ts";
|
||||||
|
import { bus } from "./bus.ts";
|
||||||
|
import { automaton } from "./simulation.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export type HighlightKind = "focus" | "error" | "warning" | "success";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type Highlight = {
|
||||||
|
span: Span,
|
||||||
|
kind: HighlightKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
type HighlightEntry = {
|
||||||
|
span: Span,
|
||||||
|
kind: HighlightKind,
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const current: Map<string, HighlightEntry> = new Map();
|
||||||
|
|
||||||
|
|
||||||
|
function asKey(highlight: Highlight): string {
|
||||||
|
return `${highlight.span[0]}:${highlight.span[1]}:${highlight.kind}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlight_from_node_id(node_id: string) {
|
||||||
|
const state = automaton.states.get(node_id);
|
||||||
|
if (state) {
|
||||||
|
bus.emit("highlight/one/add", { kind: "success", span: state.definition })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dehighlight_from_node_id(node_id: string) {
|
||||||
|
const state = automaton.states.get(node_id);
|
||||||
|
if (state) {
|
||||||
|
bus.emit("highlight/one/remove", { kind: "success", span: state.definition })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlight_from_edge_id(node_id: string) {
|
||||||
|
for (const edge_value of automaton.edges.get(node_id)!) {
|
||||||
|
bus.emit("highlight/one/add", { kind: "focus", span: edge_value.function })
|
||||||
|
bus.emit("highlight/one/add", { kind: "warning", span: edge_value.transition })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dehighlight_from_edge_id(node_id: string) {
|
||||||
|
for (const edge_value of automaton.edges.get(node_id)!) {
|
||||||
|
bus.emit("highlight/one/remove", { kind: "focus", span: edge_value.function })
|
||||||
|
bus.emit("highlight/one/remove", { kind: "warning", span: edge_value.transition })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.on("automata/update", _ => {
|
||||||
|
bus.emit("highlight/all/remove", undefined);
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.on("highlight/one/add", (highlight) => {
|
||||||
|
const key = asKey(highlight);
|
||||||
|
if (current.has(key)) {
|
||||||
|
current.get(key)!.count += 1;
|
||||||
|
} else {
|
||||||
|
current.set(key, { count: 1, ...highlight });
|
||||||
|
bus.emit("highlight/update", undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bus.on("highlight/one/remove", (highlight) => {
|
||||||
|
const key = asKey(highlight);
|
||||||
|
if (current.has(key)) {
|
||||||
|
const value = current.get(key)!
|
||||||
|
value.count -= 1;
|
||||||
|
if (value.count === 0) {
|
||||||
|
current.delete(key);
|
||||||
|
bus.emit("highlight/update", undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bus.on("highlight/all/remove", (_) => {
|
||||||
|
if (current.size !== 0) {
|
||||||
|
current.clear();
|
||||||
|
bus.emit("highlight/update", undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -11,8 +11,9 @@ import {parse_machine_from_json} from "./automata.ts";
|
||||||
|
|
||||||
export type SimStepResult = "pending" | "accept" | "reject";
|
export type SimStepResult = "pending" | "accept" | "reject";
|
||||||
export type Sim = FaSim | PdaSim | TmSim;
|
export type Sim = FaSim | PdaSim | TmSim;
|
||||||
let simulation: Sim | null = null;
|
|
||||||
let automaton: Machine = {
|
export let simulation: Sim | null = null;
|
||||||
|
export let automaton: Machine = {
|
||||||
type: "fa",
|
type: "fa",
|
||||||
alphabet: new Map(),
|
alphabet: new Map(),
|
||||||
final_states: new Map(),
|
final_states: new Map(),
|
||||||
|
|
@ -28,7 +29,7 @@ bus.on("compiled", ({ machine }) => {
|
||||||
try {
|
try {
|
||||||
bus.emit("controls/sim/clear", undefined);
|
bus.emit("controls/sim/clear", undefined);
|
||||||
automaton = parse_machine_from_json(machine);
|
automaton = parse_machine_from_json(machine);
|
||||||
bus.emit("automata/update", { automaton });
|
bus.emit("automata/update", automaton);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +37,7 @@ bus.on("compiled", ({ machine }) => {
|
||||||
});
|
});
|
||||||
bus.on("controls/sim/clear", (_) => {
|
bus.on("controls/sim/clear", (_) => {
|
||||||
simulation = null;
|
simulation = null;
|
||||||
bus.emit("automata/sim/update", { simulation: null });
|
bus.emit("automata/sim/update", null);
|
||||||
});
|
});
|
||||||
bus.on("controls/sim/step", (_) => {
|
bus.on("controls/sim/step", (_) => {
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
|
|
@ -48,7 +49,7 @@ bus.on("controls/sim/step", (_) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const machineInput = document.getElementById("machineInput") as HTMLInputElement;
|
const machineInput = document.getElementById("machineInput") as HTMLInputElement;
|
||||||
machineInput.addEventListener("input", () => bus.emit("automata/sim/update", {simulation: null}));
|
machineInput.addEventListener("input", () => bus.emit("controls/sim/clear", undefined));
|
||||||
machineInput.addEventListener("keydown", (e) => {
|
machineInput.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
bus.emit("controls/sim/reload", undefined)
|
bus.emit("controls/sim/reload", undefined)
|
||||||
|
|
@ -67,10 +68,10 @@ bus.on("controls/sim/reload", (_) => {
|
||||||
simulation = new TmSim(automaton as Tm, input);
|
simulation = new TmSim(automaton as Tm, input);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
bus.emit("automata/sim/update", { simulation });
|
bus.emit("automata/sim/update", simulation);
|
||||||
});
|
});
|
||||||
const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement;
|
const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement;
|
||||||
bus.on("automata/sim/update", ({simulation}) => {
|
bus.on("automata/sim/update", simulation => {
|
||||||
if (!simulation){
|
if (!simulation){
|
||||||
simulationStatus.innerText = "N/A"
|
simulationStatus.innerText = "N/A"
|
||||||
simulationStatus.style.color = "var(--fg-2)";
|
simulationStatus.style.color = "var(--fg-2)";
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,42 @@
|
||||||
import * as vis from "npm:vis-network/standalone";
|
import * as vis from "npm:vis-network/standalone";
|
||||||
|
|
||||||
import { bus } from "./bus.ts";
|
import { bus } from "./bus.ts";
|
||||||
import type { Sim } from "./simulation.ts";
|
import { automaton, simulation } from "./simulation.ts";
|
||||||
import type { Machine } from "./automata.ts";
|
import { dehighlight_from_edge_id, dehighlight_from_node_id, highlight_from_edge_id, highlight_from_node_id } from "./highlight.ts";
|
||||||
|
|
||||||
|
|
||||||
bus.on("controls/vis/physics", ({enabled}) => {
|
bus.on("controls/vis/physics", ({ enabled }) => {
|
||||||
network.setOptions({ physics: { enabled } });
|
network.setOptions({ physics: { enabled } });
|
||||||
network.setOptions({edges: {smooth: enabled}});
|
network.setOptions({ edges: { smooth: enabled } });
|
||||||
});
|
});
|
||||||
bus.on("controls/vis/reset_network", _ => {
|
bus.on("controls/vis/reset_network", _ => {
|
||||||
try {
|
try {
|
||||||
nodes.forEach((n) => {
|
nodes.forEach((n) => {
|
||||||
n.physics = true;
|
n.physics = true;
|
||||||
n.x = undefined;
|
n.x = undefined;
|
||||||
n.y = undefined;
|
n.y = undefined;
|
||||||
});
|
});
|
||||||
network.setData({ nodes, edges });
|
network.setData({ nodes, edges });
|
||||||
} catch {
|
} catch {
|
||||||
// Last resort
|
// Last resort
|
||||||
network.setData({ nodes, edges });
|
network.setData({ nodes, edges });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bus.on("automata/sim/after_step", _ => {
|
bus.on("automata/sim/after_step", _ => {
|
||||||
network.redraw();
|
network.redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
let simulation: Sim | null = null;
|
bus.on("automata/sim/update", _ => {
|
||||||
bus.on("automata/sim/update", ({simulation: sim}) => {
|
|
||||||
simulation = sim;
|
|
||||||
network.redraw();
|
network.redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
let automaton: Machine
|
bus.on("automata/update", automaton => {
|
||||||
|
|
||||||
bus.on("automata/update", ({automaton: auto}) => {
|
|
||||||
automaton = auto;
|
|
||||||
// Populate nodes
|
// Populate nodes
|
||||||
for (const state of automaton.states.keys()) {
|
for (const state of automaton.states.keys()) {
|
||||||
|
|
||||||
const size = measureTextWidth(state, getGraphTheme().node_font)/2+10
|
const size = measureTextWidth(state, getGraphTheme().node_font) / 2 + 10
|
||||||
if (nodes.get(state)) {
|
if (nodes.get(state)) {
|
||||||
nodes.update({
|
nodes.update({
|
||||||
id: state,
|
id: state,
|
||||||
|
|
@ -62,20 +58,20 @@ bus.on("automata/update", ({automaton: auto}) => {
|
||||||
for (const [edge_id, transitions] of automaton.edges) {
|
for (const [edge_id, transitions] of automaton.edges) {
|
||||||
const to_from = edge_id.split("#");
|
const to_from = edge_id.split("#");
|
||||||
const vadjust = -getGraphTheme().edge_font_size *
|
const vadjust = -getGraphTheme().edge_font_size *
|
||||||
Math.floor(transitions.length / 2);
|
Math.floor(transitions.length / 2);
|
||||||
const font = {
|
const font = {
|
||||||
vadjust,
|
vadjust,
|
||||||
bold: {
|
bold: {
|
||||||
vadjust
|
vadjust
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (edges.get(edge_id)) {
|
if (edges.get(edge_id)) {
|
||||||
edges.update({
|
edges.update({
|
||||||
id: edge_id,
|
id: edge_id,
|
||||||
font,
|
font,
|
||||||
from: to_from[0],
|
from: to_from[0],
|
||||||
to: to_from[1],
|
to: to_from[1],
|
||||||
label: transitions.map(i => i.repr).join(automaton.type=="fa"?",":"\n"),
|
label: transitions.map(i => i.repr).join(automaton.type == "fa" ? "," : "\n"),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
edges.add({
|
edges.add({
|
||||||
|
|
@ -83,7 +79,7 @@ bus.on("automata/update", ({automaton: auto}) => {
|
||||||
font,
|
font,
|
||||||
from: to_from[0],
|
from: to_from[0],
|
||||||
to: to_from[1],
|
to: to_from[1],
|
||||||
label: transitions.map(i => i.repr).join(automaton.type=="fa"?",":"\n"),
|
label: transitions.map(i => i.repr).join(automaton.type == "fa" ? "," : "\n"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,22 +234,6 @@ function measureTextWidth(text: string, font: string): number {
|
||||||
return ctx.measureText(text).width;
|
return ctx.measureText(text).width;
|
||||||
}
|
}
|
||||||
|
|
||||||
function chosen_edge(
|
|
||||||
_: vis.ChosenNodeValues,
|
|
||||||
id: vis.IdType,
|
|
||||||
selected: boolean,
|
|
||||||
hovered: boolean,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
function chosen_node(
|
|
||||||
_: vis.ChosenNodeValues,
|
|
||||||
id: vis.IdType,
|
|
||||||
selected: boolean,
|
|
||||||
hovered: boolean,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
const network: vis.Network = createGraph();
|
const network: vis.Network = createGraph();
|
||||||
|
|
||||||
function createGraph(): vis.Network {
|
function createGraph(): vis.Network {
|
||||||
|
|
@ -286,16 +266,9 @@ function createGraph(): vis.Network {
|
||||||
shape: "custom",
|
shape: "custom",
|
||||||
size: 18,
|
size: 18,
|
||||||
// @ts-expect-error bad library
|
// @ts-expect-error bad library
|
||||||
chosen: {
|
|
||||||
node: chosen_node,
|
|
||||||
},
|
|
||||||
ctxRenderer: renderNode,
|
ctxRenderer: renderNode,
|
||||||
},
|
},
|
||||||
edges: {
|
edges: {
|
||||||
chosen: {
|
|
||||||
// @ts-expect-error bad library
|
|
||||||
edge: chosen_edge,
|
|
||||||
},
|
|
||||||
arrowStrikethrough: false,
|
arrowStrikethrough: false,
|
||||||
arrows: "to",
|
arrows: "to",
|
||||||
},
|
},
|
||||||
|
|
@ -303,7 +276,7 @@ function createGraph(): vis.Network {
|
||||||
);
|
);
|
||||||
vis.DataSet;
|
vis.DataSet;
|
||||||
|
|
||||||
network.on("doubleClick", (params: {nodes: string[]}) => {
|
network.on("doubleClick", (params: { nodes: string[] }) => {
|
||||||
for (const node_id of params.nodes) {
|
for (const node_id of params.nodes) {
|
||||||
const node: vis.Node = nodes.get(node_id)!;
|
const node: vis.Node = nodes.get(node_id)!;
|
||||||
node.physics = !node.physics;
|
node.physics = !node.physics;
|
||||||
|
|
@ -311,6 +284,49 @@ function createGraph(): vis.Network {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
network.on("hoverEdge", ({ edge }: { edge: string }) => {
|
||||||
|
highlight_from_edge_id(edge)
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on('blurEdge', ({edge}: {edge: string}) => {
|
||||||
|
dehighlight_from_edge_id(edge)
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on("hoverNode", ({ node }: { node: string }) => {
|
||||||
|
highlight_from_node_id(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on('blurNode', ({ node }: { node: string }) => {
|
||||||
|
dehighlight_from_node_id(node)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
network.on("selectEdge", item => {
|
||||||
|
const id = network.getEdgeAt(item.pointer.DOM);
|
||||||
|
if(id)highlight_from_edge_id(id as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on('deselectEdge', item => {
|
||||||
|
console.log(item);
|
||||||
|
for (const edge of item.previousSelection.edges){
|
||||||
|
console.log(edge);
|
||||||
|
dehighlight_from_edge_id(edge.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on("selectNode", item => {
|
||||||
|
const id = network.getNodeAt(item.pointer.DOM);
|
||||||
|
if(id)highlight_from_node_id(id as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on('deselectNode', item => {
|
||||||
|
console.log(item);
|
||||||
|
for (const node of item.previousSelection.nodes){
|
||||||
|
console.log(node);
|
||||||
|
dehighlight_from_node_id(node.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,7 +339,7 @@ function renderNode({
|
||||||
state: { selected, hover },
|
state: { selected, hover },
|
||||||
style,
|
style,
|
||||||
label,
|
label,
|
||||||
}: {ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: {selected: boolean, hover: boolean}, style: vis.NodeOptions, label: string}) {
|
}: { ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: { selected: boolean, hover: boolean }, style: vis.NodeOptions, label: string }) {
|
||||||
return {
|
return {
|
||||||
drawNode() {
|
drawNode() {
|
||||||
const t = getGraphTheme();
|
const t = getGraphTheme();
|
||||||
|
|
@ -333,7 +349,7 @@ function renderNode({
|
||||||
const isFinal = automaton.final_states
|
const isFinal = automaton.final_states
|
||||||
? automaton.final_states.has(id)
|
? automaton.final_states.has(id)
|
||||||
: false;
|
: false;
|
||||||
const isActive = simulation?simulation.current_states.has(id):false;
|
const isActive = simulation ? simulation.current_states.has(id) : false;
|
||||||
|
|
||||||
const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0;
|
const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0;
|
||||||
const stroke = isActive ? t.current_node_border : t.node_border;
|
const stroke = isActive ? t.current_node_border : t.node_border;
|
||||||
|
|
@ -345,7 +361,7 @@ function renderNode({
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
ctx.font = hover||selected?t.node_font_bold:t.node_font;
|
ctx.font = hover || selected ? t.node_font_bold : t.node_font;
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
|
|
||||||
|
|
@ -398,7 +414,7 @@ function renderNode({
|
||||||
|
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
for (let i = 0; i < paths.length; i++) {
|
for (let i = 0; i < paths.length; i++) {
|
||||||
ctx.fillStyle = paths[i].accepted?t.current_node_border:t.fg_0;
|
ctx.fillStyle = paths[i].accepted ? t.current_node_border : t.fg_0;
|
||||||
ctx.fillText(paths[i].toString(), x, by + padY + i * lineH);
|
ctx.fillText(paths[i].toString(), x, by + padY + i * lineH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
|
|
||||||
.cm-highlight {
|
.cm-highlight {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 1px;
|
// padding: 0 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-highlight-warning { background: color-mix(in srgb, var(--warning) 40%, var(--bg-0)); }
|
.cm-highlight-warning { background: color-mix(in srgb, var(--warning) 40%, var(--bg-0)); }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue