@webwriter/automaton
Version:
Build, visualize, and interact with different kinds of automata (DFA, NFA, PDA).
496 lines (436 loc) • 18.6 kB
text/typescript
import { TemplateResult, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import '@shoelace-style/shoelace/dist/themes/light.css';
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.component.js';
import SlBadge from '@shoelace-style/shoelace/dist/components/badge/badge.component.js';
import SlTooltip from '@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js';
import SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js';
import SlButtonGroup from '@shoelace-style/shoelace/dist/components/button-group/button-group.component.js';
import {
biSkipStart,
biPlay,
biSkipEnd,
biAlphabet,
biSkipForward,
biArrowCounterclockwise,
biPause,
biStop,
biArrowRight,
biHandIndexThumb,
} from '../styles/icons';
import { Automaton, SimulationFeedback, SimulationResult, SimulationStatus } from '../automata';
import { classMap } from 'lit/directives/class-map.js';
import { SlChangeEvent } from '@shoelace-style/shoelace';
import { styleMap } from 'lit/directives/style-map.js';
import { simulationMenuStyles } from '../styles/simulationMenu';
import { LitElementWw } from '@webwriter/lit';
import { Graph } from '../graph';
import { ManualAutoSimulator } from 'automata/manual-auto';
import { localized, msg } from '@lit/localize';
import { Logger } from '@u/logger';
export class SimulatorMenu extends LitElementWw {
private accessor _automaton!: Automaton;
public set automaton(automaton: Automaton) {
this._automaton = automaton;
}
public accessor graph!: Graph;
private accessor _result!: {
status: SimulationStatus;
wordPosition: number;
step: number;
} | null;
private set result(result: { status: SimulationStatus; wordPosition: number; step?: number }) {
this._result = {
status: result.status,
wordPosition: result.wordPosition,
step: result.step || result.wordPosition,
};
}
private accessor _simulationResult: SimulationResult | null = null;
private accessor _mode: 'idle' | 'step' | 'run' | 'animate' = 'idle';
private accessor _animationRunning: boolean = false;
public static get styles() {
return simulationMenuStyles;
}
private accessor _backButton!: SlButton;
private accessor _nextButton!: SlButton;
private accessor _toggleButton!: SlButton;
private accessor _stopButton!: SlButton;
private accessor _wordInput!: SlInput;
public static get scopedElements() {
return {
'sl-button': SlButton,
'sl-tooltip': SlTooltip,
'sl-badge': SlBadge,
'sl-input': SlInput,
'sl-button-group': SlButtonGroup,
};
}
render() {
return html`<div class=${classMap({
simulator: true,
'simulator--pda': this._automaton.type === 'pda',
})}>${this.renderLabel()} ${this.renderInput()} ${this.renderButtonGroup()}</div>`;
}
private renderLabel() {
if (!this._simulationResult) {
return html`<div
class="simulator__label">
</div>`;
}
else if (this._simulationResult.errors && this._simulationResult.errors.length > 0) {
return html`<div
class="simulator__label">
<span>
${msg(html`Please fix the following <sl-badge variant="danger">errors</sl-badge> to run the simulation:`)}
<br/>
<div class="simulator__label__errors">${ this._simulationResult.errors.map(e => {
if (e.node) {
return html`${e.node?.label}: ${e.message}<br/>`;
}
return html`${e.message}<br/>`;
}) }</div>
</span>
</div>`;
}
else if (this._result?.status === SimulationStatus.NO_PATH || !this._simulationResult.path || this._simulationResult.path.nodes.length === 0) {
return html`<div
class="simulator__label">
<span>
${msg(html`No valid path found. The automaton <sl-badge variant="danger">rejects</sl-badge> the word <b>${this._automaton.simulator.word}</b>.`)}
</span>
</div>`;
}
else {
let pathHtml: TemplateResult<1>[] = [];
const pathLengthShown = (this._result?.step || 0) + 1;
for (let i = 0; i < pathLengthShown; i++) {
const nodeLabel = this._simulationResult?.path.nodes[i].label || '';
pathHtml.push(html`<sl-button
=${() => {
this.goToStep(i);
}}
class="simulator__label__path__node"
?circle=${nodeLabel.length <= 3}
?pill=${nodeLabel.length > 3}
size="medium"
>${nodeLabel}</sl-button>`);
if (i < pathLengthShown - 1) {
pathHtml.push(html`
<div class="simulator__label__path__transition">
<span>${this._simulationResult?.path.transitions[i].symbol || 'ε'}</span>
${biArrowRight}
</div>`);
}
}
return html`<div
class="simulator__label">
${!!this._result?.status && this._result.status === SimulationStatus.ACCEPTED
? msg(html`<span>The automaton <sl-badge variant="success">accepts</sl-badge> the word <b>${this._automaton.simulator.word || "ε"}</b>.</span>`)
: !!this._result?.status && this._result.status === SimulationStatus.REJECTED
? msg(html`<span>The automaton <sl-badge variant="danger">rejects</sl-badge> the word <b>${this._automaton.simulator.word || "ε"}</b>.</span>`)
: !!this._result?.status && this._result.status === SimulationStatus.NO_MOVES
? html`<span>${msg("No further moves possible. Try a different path.")}</span>`
: !!this._result?.status && this._result.status === SimulationStatus.PAUSED
? html`<span>${msg("Simulation paused.")}</span>`
: ""}
<div class="simulator__label__path">
${pathHtml}
</div>
</div>`;
}
}
private renderInput() {
return html` <sl-input
class='simulator__input'
style=${styleMap({
display: this._mode === 'idle' ? 'block' : 'none',
})}
-input=${(e: SlChangeEvent) => {
this._automaton.simulator.word = (e.target as SlInput).value;
this.reset();
this.requestUpdate();
}}
value=${this._automaton.simulator.word}
id="wordInput"
placeholder=${msg("Input Word e.g. aaabbb, step;step;stop")}
clearable
>
<span slot="prefix" class="simulator__input__prefix">${biAlphabet}</span>
</sl-input>
<div
class='simulator__input-display'
style=${styleMap({
display: this._mode !== 'idle' ? 'flex' : 'none',
})}
=${() => {
this.reset();
this._wordInput.focus();
}}
>
<span slot="prefix" class="simulator__input-display__prefix">${biAlphabet}</span>
<div class="simulator__input-display__input">
${this._automaton.simulator.wordArray.map((e, i) => {
return i === this._result?.wordPosition ? html`|${e}` : e;
})}${this._result?.wordPosition === this._automaton.simulator.wordArray.length ? '|' : ''}
</div>
</div>`;
}
private renderButtonGroup() {
return html`<sl-button-group
class="simulator_buttons"
style=${styleMap({
display: this._mode === 'idle' ? 'initial' : 'none',
})}
>
${this._automaton.type === 'pda' || this._automaton.type === 'nfa' ? html`
<sl-tooltip content=${msg("Manual")} placement="top">
<sl-button
=${() => {
this.startStepByStep(true);
}}
>${biHandIndexThumb}</sl-button
>
</sl-tooltip>
` : ''}
<sl-tooltip content=${msg("Animate")} placement="top">
<sl-button
=${() => {
this.startAnimation();
}}
>${biPlay}</sl-button
>
</sl-tooltip>
<sl-tooltip content=${msg("Step by Step")} placement="top">
<sl-button
=${() => {
this.startStepByStep();
}}
>${biSkipEnd}</sl-button
>
</sl-tooltip>
<sl-tooltip content=${msg("Check")} placement="top">
<sl-button
=${() => {
this.run();
}}
>${biSkipForward}</sl-button
>
</sl-tooltip> </sl-button-group
><sl-button-group
class="simulator_buttons"
style=${styleMap({
display: this._mode === 'step' ? 'flex' : 'none',
})}
>
<sl-tooltip content=${msg("Back")} placement="top">
<sl-button
id="simulator_back"
=${() => {
this.stepBackward();
}}
disabled
>${biSkipStart}</sl-button
>
</sl-tooltip>
<sl-tooltip content=${msg("Next")} placement="top">
<sl-button
id="simulator_next"
=${() => {
this.stepForward();
}}
>${biSkipEnd}</sl-button
>
</sl-tooltip>
<sl-tooltip content=${msg("Reset")} placement="top">
<sl-button
=${() => {
this.reset();
}}
>${biArrowCounterclockwise}</sl-button
>
</sl-tooltip> </sl-button-group
><sl-button-group
class="simulator_buttons"
style=${styleMap({
display: this._mode === 'run' ? 'flex' : 'none',
})}
>
<sl-tooltip content=${msg("Reset")} placement="top">
<sl-button
=${() => {
this.reset();
}}
>${biArrowCounterclockwise}</sl-button
>
</sl-tooltip>
</sl-button-group>
<sl-button-group
class="simulator_buttons"
style=${styleMap({ display: this._mode === 'animate' ? 'flex' : 'none' })}
>
<sl-tooltip content=${this._animationRunning ? msg('Pause') : msg('Play')} placement="top">
<sl-button
=${() => {
this.toggleAnimation();
}}
id="simulator_toggle"
>${this._animationRunning ? biPause : biPlay}</sl-button
>
</sl-tooltip>
<sl-tooltip content=${msg("Stop")} placement="top">
<sl-button
=${() => {
this.stopAnimation();
}}
id="simulator_stop"
>${biStop}</sl-button
>
</sl-tooltip>
<sl-tooltip content=${msg("Reset")} placement="top">
<sl-button
=${() => {
this.reset();
}}
>${biArrowCounterclockwise}</sl-button
>
</sl-tooltip>
</sl-button-group> `;
}
private run() {
this.reset();
this._mode = 'run';
Logger.time('simulation');
const result = this._automaton.simulator.simulate();
Logger.timeEnd('simulation');
this.result = {
status: result.status,
wordPosition: this._automaton.simulator.word.length,
step: (result.simulationResult?.path?.nodes.length || 1) - 1,
};
this._simulationResult = result.simulationResult || null;
Logger.log('Simulation Result:', this._simulationResult);
this.requestUpdate();
}
private startAnimation() {
this.reset();
this._mode = 'animate';
if (!!this._automaton.getInitialNode())
this._automaton.highlightNode(this._automaton.getInitialNode());
this._automaton.simulator.startAnimation((result: SimulationFeedback) => {
Logger.log('Animation Result:', result);
this.result = result;
this._simulationResult = result.simulationResult || null;
if (result.status !== SimulationStatus.RUNNING) {
this._toggleButton.disabled = true;
this._stopButton.disabled = true;
}
this.requestUpdate();
});
this._animationRunning = true;
this.requestUpdate();
}
private stopAnimation() {
this._automaton.simulator.stopAnimation((result) => {
this.result = result;
this._simulationResult = result.simulationResult || null;
this._toggleButton.disabled = true;
this._stopButton.disabled = true;
this.requestUpdate();
});
this._animationRunning = false;
this.requestUpdate();
}
private toggleAnimation() {
if (this._animationRunning) {
this._automaton.simulator.pauseAnimation((result) => {
this.result = result;
this._simulationResult = result.simulationResult || null;
this.requestUpdate();
});
this._animationRunning = false;
} else {
this._automaton.simulator.startAnimation((result) => {
this.result = result;
this._simulationResult = result.simulationResult || null;
if (result.status !== SimulationStatus.RUNNING) {
this._toggleButton.disabled = true;
this._stopButton.disabled = true;
}
this.requestUpdate();
});
this._animationRunning = true;
}
this.requestUpdate();
}
public reset() {
this._automaton.simulator.reset();
this._result = {
status: SimulationStatus.IDLE,
wordPosition: 0,
step: 0,
};
this._simulationResult = null;
this._mode = 'idle';
this._nextButton.disabled = false;
this._backButton.disabled = true;
this._toggleButton.disabled = false;
this._stopButton.disabled = false;
this.requestUpdate();
}
public init() {
this._automaton.simulator.init();
}
private startStepByStep(manual = false) {
this.reset();
this._mode = 'step';
if (manual && (this._automaton.type === 'nfa' || this._automaton.type === 'pda')) {
(this._automaton.simulator as ManualAutoSimulator).setManualMode(true);
}
this._automaton.simulator.initStepByStep(this.graph, (res: any) => {
Logger.log(res);
this.result = res;
this._simulationResult = res.simulationResult || null;
this._backButton.disabled = res.step <= 0;
if (
res.status === SimulationStatus.NO_PATH
|| res.status === SimulationStatus.ACCEPTED
|| res.status === SimulationStatus.REJECTED
|| res.status === SimulationStatus.ERROR
) {
this._nextButton.disabled = true;
}
this.requestUpdate();
});
if (!!this._automaton.getInitialNode())
this._automaton.highlightNode(this._automaton.getInitialNode());
this.requestUpdate();
}
private goToStep(step: number) {
if (this._mode !== 'step') {
this.startStepByStep();
}
const result = this._automaton.simulator.goToStep(step);
this.result = result;
if (result.simulationResult) {
this._simulationResult = result.simulationResult;
}
this._nextButton.disabled = result.finalStep || false;
this._backButton.disabled = result.firstStep || false;
this.requestUpdate();
}
private stepForward() {
this.goToStep(this._result?.step !== undefined ? this._result.step + 1 : 0);
}
private stepBackward() {
this.goToStep(this._result?.step !== undefined ? this._result.step - 1 : 0);
}
}