Skip to content

Commit 8db2e9d

Browse files
Web 0.14.0: neue Welt VisuFlow zum PAP-Zeichnen (#208)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ugz28WKeXDbck9voLEKLtT
1 parent a075818 commit 8db2e9d

25 files changed

Lines changed: 3069 additions & 1307 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is loosely inspired by Keep a Changelog.
1515

1616
### Added
1717

18+
- **Web 0.14.0, new world VisuFlow: draw flowcharts:** fourth world next to VisuStruct, VisuBot and VisuObject (…/app/?visuflow, colour violet, chosen at the start or in the gear menu; the world chooser now has two rows). Only drawing, no simulation and no code: drag the DIN 66001 symbols (Start, Anweisung, Ein-/Ausgabe, Verzweigung, Unterprogramm, Ende) from the bar onto the canvas, or tap a symbol to add it below the selected one, joined by an arrow; type the text right in the symbol (tap again or double click to change it). Drag an arrow from the dot at an exit to the next symbol; the diamond has the exits ja and nein (button „ja/nein tauschen“ swaps their sides), arrows run at right angles, loops go around on the right. Move with the mouse, finger or arrow keys on a 20 px grid, delete, undo/redo, title above the canvas, example „Tee kochen“ from lesson 1. Save and open as `.visuflow` file (the last plan stays in the browser), export as PNG, SVG or PDF (A4). Texts de/en/pt.
1819
- **Web 0.13.38, comments in Java:** lesson 6 has a new step „Kommentare im Code“ after „Jetzt in Java“: single-line `//` and multi-line `/* … */` comments, Java skips them and the structure chart stays the same; Javadoc `/** … */` is only mentioned. The IPO example (with „Im Editor öffnen“) shows the task in a multi-line comment, `// Eingabe`, `// Verarbeitung`, `// Ausgabe`, an explanation after a line, the expected output and a note for next time; plus a fill-in-the-gaps exercise, an Angeberwissen (PL/I 1964, BCPL 1967), a competence, a checklist line and two quiz questions. Lesson 14 marks closing brackets with `} // Ende if` / `} // Ende for`, lesson 20 the class frame with `} // Ende main` / `} // Ende Klasse`. New glossary entry Kommentar and Nachschlagen section „Java › Kommentare“ (de/en/pt).
1920
- **Web 0.13.36, sequence diagram in lesson 5:** step „Schritt für Schritt“ now shows a sequence as a structure chart: four boxes one below the other (`laenge`, `breite`, `flaeche = laenge * breite`, output 15) with a short caption that each box runs exactly once from top to bottom (de/en/pt).
2021
- **Web 0.13.35, open Java examples in the editor:** a Java example in a lesson can now carry the button „Im Editor öffnen“. It opens the example in a new tab with Programmieren set to Java, so students can run it, change values and try again; the structure chart builds itself from the code. Lesson 4 step „Text in Java“ (Mia, 14, 1.62) uses it and says how to try it (de/en/pt). For authors: write ```` ```java editor ```` instead of ```` ```java ````; a test checks that every such example parses without errors.

‎web/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎web/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "visustruct-web",
33
"private": true,
4-
"version": "0.13.38",
4+
"version": "0.14.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

‎web/src/core/flowModel.ts‎

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/**
2+
* VisuFlow: Programmablaufplan (PAP, englisch flowchart) nach DIN 66001 frei zeichnen.
3+
* Nur Zeichnen: keine Simulation, kein Code. Ein Plan besteht aus Symbolen (Mittelpunkt x/y)
4+
* und Pfeilen vom Ausgang eines Symbols zum Eingang (oben) eines anderen.
5+
*/
6+
7+
export type FlowShape = 'start' | 'ende' | 'anweisung' | 'ein' | 'verzweigung' | 'unterprogramm'
8+
9+
/** Ausgang eines Symbols: `out` unten, bei der Raute `ja` und `nein`. */
10+
export type FlowPort = 'out' | 'ja' | 'nein'
11+
12+
export interface FlowNode {
13+
id: string
14+
shape: FlowShape
15+
x: number
16+
y: number
17+
text: string
18+
}
19+
20+
export interface FlowEdge {
21+
id: string
22+
from: string
23+
port: FlowPort
24+
to: string
25+
}
26+
27+
export interface FlowDoc {
28+
title: string
29+
nodes: FlowNode[]
30+
edges: FlowEdge[]
31+
/** Raute: `true` = „ja“ geht nach rechts, „nein“ nach unten (Standard umgekehrt). */
32+
yesRight?: string[]
33+
}
34+
35+
export const FLOW_SHAPES: FlowShape[] = ['start', 'anweisung', 'ein', 'verzweigung', 'unterprogramm', 'ende']
36+
37+
export const FLOW_GRID = 20
38+
39+
/** Füllfarben wie im Lernbaustein „PAP bauen“ (papView.ts). */
40+
export const FLOW_FILL: Record<FlowShape, string> = {
41+
start: '#d1f2d9',
42+
ende: '#d1f2d9',
43+
anweisung: '#e5f0ff',
44+
ein: '#f2e6ff',
45+
verzweigung: '#fff4d6',
46+
unterprogramm: '#e5f0ff',
47+
}
48+
49+
export type Measure = (text: string) => number
50+
51+
export interface FlowBox {
52+
x: number
53+
y: number
54+
w: number
55+
h: number
56+
lines: string[]
57+
}
58+
59+
export const FLOW_LINE_HEIGHT = 16
60+
const MAX_TEXT = 170
61+
62+
/** Text auf Zeilen umbrechen (an Leerzeichen, harte Umbrüche bleiben). */
63+
export function flowLines(text: string, max: number, measure: Measure): string[] {
64+
const out: string[] = []
65+
for (const para of text.split('\n')) {
66+
let line = ''
67+
for (const word of para.split(/\s+/).filter(Boolean)) {
68+
const next = line ? `${line} ${word}` : word
69+
if (line && measure(next) > max) {
70+
out.push(line)
71+
line = word
72+
} else line = next
73+
}
74+
out.push(line)
75+
}
76+
return out.length ? out : ['']
77+
}
78+
79+
const snap = (v: number) => Math.round(v / FLOW_GRID) * FLOW_GRID
80+
81+
/** Größe eines Symbols aus seinem Text; Breite und Höhe auf das Raster gerundet. */
82+
export function flowBox(node: FlowNode, measure: Measure): FlowBox {
83+
const lines = flowLines(node.text, MAX_TEXT, measure)
84+
const textW = Math.max(0, ...lines.map(measure))
85+
const textH = lines.length * FLOW_LINE_HEIGHT
86+
let w: number
87+
let h: number
88+
if (node.shape === 'verzweigung') {
89+
// Raute: Text passt in das innere Rechteck, wenn Breite und Höhe doppelt so groß sind.
90+
w = Math.max(140, textW * 1.7 + 30)
91+
h = Math.max(80, textH * 1.8 + 24)
92+
} else {
93+
const pad = node.shape === 'ein' ? 44 : node.shape === 'unterprogramm' ? 40 : node.shape === 'anweisung' ? 24 : 36
94+
w = Math.max(node.shape === 'start' || node.shape === 'ende' ? 100 : 120, textW + pad)
95+
h = Math.max(40, textH + 18)
96+
}
97+
return { x: node.x, y: node.y, w: Math.ceil(w / FLOW_GRID) * FLOW_GRID, h: Math.ceil(h / FLOW_GRID) * FLOW_GRID, lines }
98+
}
99+
100+
/** Welche Ausgänge ein Symbol hat. */
101+
export function flowPorts(shape: FlowShape): FlowPort[] {
102+
if (shape === 'ende') return []
103+
if (shape === 'verzweigung') return ['ja', 'nein']
104+
return ['out']
105+
}
106+
107+
export const hasInput = (shape: FlowShape) => shape !== 'start'
108+
109+
type Side = 'bottom' | 'right'
110+
111+
/** Seite eines Ausgangs. Raute: Standard „ja“ unten, „nein“ rechts; umschaltbar. */
112+
export function portSide(doc: FlowDoc, node: FlowNode, port: FlowPort): Side {
113+
if (port === 'out') return 'bottom'
114+
const yesRight = doc.yesRight?.includes(node.id) ?? false
115+
return (port === 'ja') === yesRight ? 'right' : 'bottom'
116+
}
117+
118+
export function portPoint(box: FlowBox, side: Side): [number, number] {
119+
return side === 'bottom' ? [box.x, box.y + box.h / 2] : [box.x + box.w / 2, box.y]
120+
}
121+
122+
export const inputPoint = (box: FlowBox): [number, number] => [box.x, box.y - box.h / 2]
123+
124+
/**
125+
* Rechtwinkliger Weg eines Pfeils vom Ausgang zum Eingang oben. Geht es nach oben zurück (Schleife),
126+
* läuft der Pfeil rechts an beiden Symbolen vorbei.
127+
*/
128+
export function routeEdge(from: FlowBox, side: Side, to: FlowBox, lane = 0): [number, number][] {
129+
const [sx, sy] = portPoint(from, side)
130+
const [tx, ty] = inputPoint(to)
131+
const gap = FLOW_GRID
132+
const outside = Math.max(from.x + from.w / 2, to.x + to.w / 2) + gap * (1.5 + lane)
133+
if (side === 'bottom') {
134+
if (ty >= sy + gap) {
135+
if (sx === tx) return [[sx, sy], [tx, ty]]
136+
const my = sy + gap
137+
return [[sx, sy], [sx, my], [tx, my], [tx, ty]]
138+
}
139+
return [[sx, sy], [sx, sy + gap], [outside, sy + gap], [outside, ty - gap], [tx, ty - gap], [tx, ty]]
140+
}
141+
// Ausgang rechts; liegt das Ziel rechts daneben auf gleicher Höhe, endet der Pfeil links an seiner Seite.
142+
const left = to.x - to.w / 2
143+
if (left >= sx + gap && Math.abs(to.y - sy) < to.h / 2) return [[sx, sy], [left, sy]]
144+
if (ty >= sy + gap) {
145+
if (tx >= sx + gap) return [[sx, sy], [tx, sy], [tx, ty]]
146+
const rx = sx + gap
147+
return [[sx, sy], [rx, sy], [rx, ty - gap], [tx, ty - gap], [tx, ty]]
148+
}
149+
const rx = Math.max(sx + gap, outside)
150+
return [[sx, sy], [rx, sy], [rx, ty - gap], [tx, ty - gap], [tx, ty]]
151+
}
152+
153+
// MARK: - Bearbeiten (immer neue Objekte, damit Rückgängig einfach den alten Stand behält)
154+
155+
export function emptyFlow(title = ''): FlowDoc {
156+
return { title, nodes: [], edges: [] }
157+
}
158+
159+
export function nextId(doc: FlowDoc, prefix: 'n' | 'e'): string {
160+
const used = new Set([...doc.nodes.map((n) => n.id), ...doc.edges.map((e) => e.id)])
161+
let i = 1
162+
while (used.has(`${prefix}${i}`)) i++
163+
return `${prefix}${i}`
164+
}
165+
166+
export function addNode(doc: FlowDoc, shape: FlowShape, x: number, y: number, text: string): { doc: FlowDoc; id: string } {
167+
const id = nextId(doc, 'n')
168+
return { doc: { ...doc, nodes: [...doc.nodes, { id, shape, x: snap(x), y: snap(y), text }] }, id }
169+
}
170+
171+
export function moveNode(doc: FlowDoc, id: string, x: number, y: number): FlowDoc {
172+
return { ...doc, nodes: doc.nodes.map((n) => (n.id === id ? { ...n, x: Math.max(FLOW_GRID * 4, snap(x)), y: Math.max(FLOW_GRID * 2, snap(y)) } : n)) }
173+
}
174+
175+
export function setText(doc: FlowDoc, id: string, text: string): FlowDoc {
176+
return { ...doc, nodes: doc.nodes.map((n) => (n.id === id ? { ...n, text } : n)) }
177+
}
178+
179+
/** Pfeil ziehen: Ein Ausgang hat höchstens einen Pfeil, ein neuer ersetzt den alten. */
180+
export function connect(doc: FlowDoc, from: string, port: FlowPort, to: string): FlowDoc {
181+
const a = doc.nodes.find((n) => n.id === from)
182+
const b = doc.nodes.find((n) => n.id === to)
183+
if (!a || !b || from === to || !flowPorts(a.shape).includes(port) || !hasInput(b.shape)) return doc
184+
const edges = doc.edges.filter((e) => !(e.from === from && e.port === port))
185+
return { ...doc, edges: [...edges, { id: nextId(doc, 'e'), from, port, to }] }
186+
}
187+
188+
export function freePort(doc: FlowDoc, node: FlowNode): FlowPort | undefined {
189+
return flowPorts(node.shape).find((p) => !doc.edges.some((e) => e.from === node.id && e.port === p))
190+
}
191+
192+
export function removeNode(doc: FlowDoc, id: string): FlowDoc {
193+
return {
194+
...doc,
195+
nodes: doc.nodes.filter((n) => n.id !== id),
196+
edges: doc.edges.filter((e) => e.from !== id && e.to !== id),
197+
yesRight: doc.yesRight?.filter((n) => n !== id),
198+
}
199+
}
200+
201+
export function removeEdge(doc: FlowDoc, id: string): FlowDoc {
202+
return { ...doc, edges: doc.edges.filter((e) => e.id !== id) }
203+
}
204+
205+
/** Raute: „ja“ und „nein“ tauschen die Seiten (die Pfeile bleiben an ihrem Ausgang). */
206+
export function swapYesNo(doc: FlowDoc, id: string): FlowDoc {
207+
const set = new Set(doc.yesRight ?? [])
208+
if (set.has(id)) set.delete(id)
209+
else set.add(id)
210+
return { ...doc, yesRight: [...set] }
211+
}
212+
213+
// MARK: - Datei (.visuflow, JSON)
214+
215+
export function flowToJSON(doc: FlowDoc): string {
216+
return JSON.stringify({ format: 'visuflow', version: 1, ...doc }, null, 2)
217+
}
218+
219+
export function flowFromJSON(text: string): FlowDoc | undefined {
220+
let raw: unknown
221+
try {
222+
raw = JSON.parse(text)
223+
} catch {
224+
return undefined
225+
}
226+
if (!raw || typeof raw !== 'object') return undefined
227+
const r = raw as Record<string, unknown>
228+
if (!Array.isArray(r.nodes) || !Array.isArray(r.edges)) return undefined
229+
const nodes: FlowNode[] = []
230+
for (const n of r.nodes as Record<string, unknown>[]) {
231+
if (!n || typeof n.id !== 'string' || !FLOW_SHAPES.includes(n.shape as FlowShape)) continue
232+
if (!Number.isFinite(n.x) || !Number.isFinite(n.y) || nodes.some((m) => m.id === n.id)) continue
233+
nodes.push({ id: n.id, shape: n.shape as FlowShape, x: n.x as number, y: n.y as number, text: typeof n.text === 'string' ? n.text : '' })
234+
}
235+
let doc: FlowDoc = { title: typeof r.title === 'string' ? r.title : '', nodes, edges: [] }
236+
for (const e of r.edges as Record<string, unknown>[]) {
237+
if (!e || typeof e.from !== 'string' || typeof e.to !== 'string' || typeof e.port !== 'string') continue
238+
doc = connect(doc, e.from, e.port as FlowPort, e.to)
239+
}
240+
if (Array.isArray(r.yesRight)) doc.yesRight = (r.yesRight as unknown[]).filter((id): id is string => typeof id === 'string' && nodes.some((n) => n.id === id))
241+
return doc
242+
}

‎web/src/i18n/de.strings‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,3 +690,53 @@
690690
"web_code_scopes" = "Blöcke farbig";
691691
"web_code_scopes_hint" = "Code und Struktogramm: Schleife rosa, Verzweigung blau, Methode gelb, Klasse grün";
692692
"web_pseudocode" = "Pseudocode";
693+
694+
/* VisuFlow (PAP zeichnen) */
695+
"web_visuflow_subtitle" = "PAP zeichnen";
696+
"web_visuflow_world_hint" = "VisuFlow: einen Programmablaufplan (PAP, englisch flowchart) frei zeichnen und als Bild oder PDF exportieren. Wechseln geht jederzeit im Zahnrad.";
697+
"web_visuflow_palette" = "Symbole";
698+
"web_visuflow_palette_hint" = "Antippen hängt das Symbol unter das gewählte an und verbindet es mit einem Pfeil. Du kannst es auch auf die Fläche ziehen.";
699+
"web_visuflow_shape_start" = "Start";
700+
"web_visuflow_shape_ende" = "Ende";
701+
"web_visuflow_shape_anweisung" = "Anweisung";
702+
"web_visuflow_shape_ein" = "Ein-/Ausgabe";
703+
"web_visuflow_shape_verzweigung" = "Verzweigung";
704+
"web_visuflow_shape_unterprogramm" = "Unterprogramm";
705+
"web_visuflow_shape_start_tip" = "Oval: Hier beginnt der Ablauf";
706+
"web_visuflow_shape_ende_tip" = "Oval: Hier endet der Ablauf";
707+
"web_visuflow_shape_anweisung_tip" = "Rechteck: eine Anweisung, zum Beispiel „Wasser kochen“";
708+
"web_visuflow_shape_ein_tip" = "Parallelogramm: Eingabe oder Ausgabe, zum Beispiel „Eingabe: Zahl“";
709+
"web_visuflow_shape_verzweigung_tip" = "Raute: eine Frage mit den Ausgängen ja und nein";
710+
"web_visuflow_shape_unterprogramm_tip" = "Rechteck mit zwei Strichen: ruft ein Unterprogramm auf";
711+
"web_visuflow_text_start" = "Start";
712+
"web_visuflow_text_ende" = "Ende";
713+
"web_visuflow_text_anweisung" = "Anweisung";
714+
"web_visuflow_text_ein" = "Eingabe: Zahl";
715+
"web_visuflow_text_verzweigung" = "Bedingung?";
716+
"web_visuflow_text_unterprogramm" = "Unterprogramm";
717+
"web_visuflow_new_tip" = "Neu: leerer PAP mit Start (Rückgängig holt den alten zurück)";
718+
"web_visuflow_open_tip" = "PAP-Datei (.visuflow) öffnen";
719+
"web_visuflow_save_tip" = "PAP als Datei (.visuflow) speichern";
720+
"web_visuflow_delete" = "Löschen";
721+
"web_visuflow_delete_tip" = "Gewähltes Symbol oder gewählten Pfeil löschen (Entf)";
722+
"web_visuflow_swap" = "ja/nein tauschen";
723+
"web_visuflow_swap_tip" = "Raute: „ja“ und „nein“ tauschen die Seiten";
724+
"web_visuflow_example" = "Beispiel";
725+
"web_visuflow_example_tip" = "Beispiel „Tee kochen“ aus Lektion 1 laden (Rückgängig holt den alten PAP zurück)";
726+
"web_visuflow_example_title" = "Tee kochen";
727+
"web_visuflow_example_water" = "Wasser kochen";
728+
"web_visuflow_example_bag" = "Teebeutel in die Tasse";
729+
"web_visuflow_example_hot" = "Tee noch zu heiß?";
730+
"web_visuflow_example_wait" = "Warten";
731+
"web_visuflow_example_drink" = "Tee trinken";
732+
"web_visuflow_title_label" = "Titel des PAP";
733+
"web_visuflow_title_ph" = "Titel des PAP";
734+
"web_visuflow_canvas_label" = "Zeichenfläche des PAP";
735+
"web_visuflow_open_error" = "Die Datei ist kein VisuFlow-PAP.";
736+
"web_visuflow_saved" = "Gespeichert als %@";
737+
"web_visuflow_status_empty" = "Zieh ein Symbol aus der Leiste auf die Fläche oder tippe es an.";
738+
"web_visuflow_status_idle" = "Symbol antippen wählt es aus. Vom Punkt am Ausgang ziehst du einen Pfeil zum nächsten Symbol.";
739+
"web_visuflow_status_node" = "Ziehen verschiebt das Symbol, nochmal antippen ändert den Text. Vom Punkt am Ausgang einen Pfeil ziehen.";
740+
"web_visuflow_status_edge" = "Pfeil gewählt. Löschen entfernt ihn.";
741+
"web_visuflow_status_connect" = "Loslassen über dem Symbol, zu dem der Pfeil führt.";
742+
"web_visuflow_edit_label" = "Text im Symbol";

‎web/src/i18n/en.strings‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,3 +690,53 @@
690690
"web_code_scopes" = "Colored blocks";
691691
"web_code_scopes_hint" = "Code and structogram: loop pink, branch blue, method yellow, class green";
692692
"web_pseudocode" = "Pseudocode";
693+
694+
/* VisuFlow (draw flowcharts) */
695+
"web_visuflow_subtitle" = "Draw flowcharts";
696+
"web_visuflow_world_hint" = "VisuFlow: draw a program flowchart freely and export it as an image or PDF. You can switch at any time in the gear menu.";
697+
"web_visuflow_palette" = "Symbols";
698+
"web_visuflow_palette_hint" = "Tap a symbol to add it below the selected one, joined by an arrow. You can also drag it onto the canvas.";
699+
"web_visuflow_shape_start" = "Start";
700+
"web_visuflow_shape_ende" = "End";
701+
"web_visuflow_shape_anweisung" = "Statement";
702+
"web_visuflow_shape_ein" = "Input/Output";
703+
"web_visuflow_shape_verzweigung" = "Decision";
704+
"web_visuflow_shape_unterprogramm" = "Subroutine";
705+
"web_visuflow_shape_start_tip" = "Oval: the flow starts here";
706+
"web_visuflow_shape_ende_tip" = "Oval: the flow ends here";
707+
"web_visuflow_shape_anweisung_tip" = "Rectangle: one statement, for example “Boil water”";
708+
"web_visuflow_shape_ein_tip" = "Parallelogram: input or output, for example “Input: number”";
709+
"web_visuflow_shape_verzweigung_tip" = "Diamond: a question with the exits yes and no";
710+
"web_visuflow_shape_unterprogramm_tip" = "Rectangle with two bars: calls a subroutine";
711+
"web_visuflow_text_start" = "Start";
712+
"web_visuflow_text_ende" = "End";
713+
"web_visuflow_text_anweisung" = "Statement";
714+
"web_visuflow_text_ein" = "Input: number";
715+
"web_visuflow_text_verzweigung" = "Condition?";
716+
"web_visuflow_text_unterprogramm" = "Subroutine";
717+
"web_visuflow_new_tip" = "New: empty flowchart with Start (Undo brings the old one back)";
718+
"web_visuflow_open_tip" = "Open a flowchart file (.visuflow)";
719+
"web_visuflow_save_tip" = "Save the flowchart as a file (.visuflow)";
720+
"web_visuflow_delete" = "Delete";
721+
"web_visuflow_delete_tip" = "Delete the selected symbol or arrow (Del)";
722+
"web_visuflow_swap" = "swap yes/no";
723+
"web_visuflow_swap_tip" = "Diamond: “yes” and “no” swap sides";
724+
"web_visuflow_example" = "Example";
725+
"web_visuflow_example_tip" = "Load the example “Make tea” from lesson 1 (Undo brings the old flowchart back)";
726+
"web_visuflow_example_title" = "Make tea";
727+
"web_visuflow_example_water" = "Boil water";
728+
"web_visuflow_example_bag" = "Tea bag into the cup";
729+
"web_visuflow_example_hot" = "Tea still too hot?";
730+
"web_visuflow_example_wait" = "Wait";
731+
"web_visuflow_example_drink" = "Drink tea";
732+
"web_visuflow_title_label" = "Flowchart title";
733+
"web_visuflow_title_ph" = "Flowchart title";
734+
"web_visuflow_canvas_label" = "Flowchart canvas";
735+
"web_visuflow_open_error" = "This file is not a VisuFlow flowchart.";
736+
"web_visuflow_saved" = "Saved as %@";
737+
"web_visuflow_status_empty" = "Drag a symbol from the bar onto the canvas or tap it.";
738+
"web_visuflow_status_idle" = "Tap a symbol to select it. Drag from the dot at its exit to draw an arrow to the next symbol.";
739+
"web_visuflow_status_node" = "Drag to move the symbol, tap again to change the text. Drag an arrow from the dot at the exit.";
740+
"web_visuflow_status_edge" = "Arrow selected. Delete removes it.";
741+
"web_visuflow_status_connect" = "Let go over the symbol the arrow leads to.";
742+
"web_visuflow_edit_label" = "Text in the symbol";

0 commit comments

Comments
 (0)