Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
04aa358
Move undo/redo and tab editing out of ContentView
claude Oct 18, 2026
efabe1b
Update DiagramSheetTabsModel doc comment
claude Oct 18, 2026
054d6bf
Move simulation state and controls into SimulationController
claude Oct 18, 2026
8c92504
Extract PaletteActionList and SimulationDetailView from ContentView
claude Oct 18, 2026
fc3ecc7
Move inspector hints and Fallauswahl path helpers out of ContentView
claude Oct 18, 2026
eb79ba5
Bundle PNG, file, PDF and print export in DiagramExporter
claude Oct 18, 2026
3ae4ee1
Split DiagramCanvasView into view, drop zones and renderer
claude Oct 18, 2026
1c61e23
Merge branch 'core-text-logic' into editor-store
claude Oct 19, 2026
e48aecb
Merge branch 'editor-store' into simulation-controller
claude Oct 19, 2026
0e77012
Adapt merged portrait simulation layout to SimulationController
claude Oct 19, 2026
cbf53ae
Merge branch 'palette-view' into inspector-helpers
claude Oct 19, 2026
d852cb9
Merge simulation-controller into palette-view
claude Oct 19, 2026
d2972b4
Merge branch 'inspector-helpers' into export-actions
claude Oct 19, 2026
55fd215
Merge branch 'export-actions' into canvas-split
claude Oct 19, 2026
dcc8adb
Merge branch 'core-text-logic' into editor-store
claude Oct 19, 2026
9febfde
Merge branch 'editor-store' into simulation-controller
claude Oct 19, 2026
a2a3759
Merge branch 'palette-view' into inspector-helpers
claude Oct 19, 2026
fab9f8f
Merge branch 'simulation-controller' into palette-view
claude Oct 19, 2026
820be7d
Merge branch 'inspector-helpers' into export-actions
claude Oct 19, 2026
3057857
Merge export-actions into canvas-split
claude Oct 19, 2026
d9e4351
Merge branch 'editor-store' into simulation-controller
claude Oct 19, 2026
753c2e4
Merge branch 'core-text-logic' into editor-store
claude Oct 19, 2026
a7b0a39
Merge branch 'palette-view' into inspector-helpers
claude Oct 19, 2026
9bf2f73
Merge branch 'simulation-controller' into palette-view
claude Oct 19, 2026
6ad16af
Merge branch 'export-actions' into canvas-split
claude Oct 19, 2026
2cf9ea9
Merge branch 'inspector-helpers' into export-actions
claude Oct 19, 2026
14d0997
Merge pull request #18 from code4teaching/canvas-split
code4teaching Oct 19, 2026
69d6259
Merge pull request #17 from code4teaching/export-actions
code4teaching Oct 19, 2026
74ec7ac
Merge pull request #16 from code4teaching/inspector-helpers
code4teaching Oct 19, 2026
251cb84
Merge pull request #15 from code4teaching/palette-view
code4teaching Oct 19, 2026
6c6a3be
Merge pull request #14 from code4teaching/simulation-controller
code4teaching Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

extension VisuStructDocumentEditing {
/// Innerste **Fallauswahl** (`multiway`), die `path` enthält (oder selbst ist) — für „Fall hinzufügen/entfernen“.
public static func multiwayAncestorPath(for path: [Int]?, in doc: VisuStructDocument) -> [Int]? {
guard let path, !path.isEmpty else { return nil }
for len in stride(from: path.count, through: 1, by: -1) {
let prefix = Array(path.prefix(len))
if let el = try? element(at: prefix, in: doc),
el.type == .fallauswahl {
return prefix
}
}
return nil
}

/// Spaltenindex der Fallauswahl, wenn die Auswahl **in** einer Spalte liegt (`[…, fall, spalte, …]`).
public static func caseIndex(of selectedPath: [Int]?, inMultiwayAt multiwayPath: [Int]) -> Int? {
guard let sp = selectedPath, sp.count > multiwayPath.count else { return nil }
return sp[multiwayPath.count]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

extension VisuStructDocumentEditing {
/// Auswahl nach Undo/Redo oder Löschen: leeres Dokument → keine Auswahl; fehlende oder ungültige Auswahl → erstes Element.
public static func sanitizedSelection(_ path: [Int]?, in doc: VisuStructDocument) -> [Int]? {
if doc.elements.isEmpty { return nil }
guard let path else { return [0] }
if (try? element(at: path, in: doc)) == nil { return [0] }
return path
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation

/// Undo-/Redo-Verlauf eines Diagramms: Schnappschüsse des Dokuments vor jeder Änderung.
public struct VisuStructEditHistory: Equatable, Sendable {
public static let defaultMaxSteps = 80

public private(set) var undoStack: [VisuStructDocument] = []
public private(set) var redoStack: [VisuStructDocument] = []
public let maxSteps: Int

public init(maxSteps: Int = VisuStructEditHistory.defaultMaxSteps) {
self.maxSteps = maxSteps
}

public var canUndo: Bool { !undoStack.isEmpty }
public var canRedo: Bool { !redoStack.isEmpty }

/// Vor einer Änderung: bisherigen Zustand ablegen, Redo verwerfen.
public mutating func record(_ current: VisuStructDocument) {
pushUndo(current)
redoStack.removeAll()
}

/// Liefert den vorherigen Zustand und legt `current` für Redo ab; `nil`, wenn nichts rückgängig zu machen ist.
public mutating func undo(from current: VisuStructDocument) -> VisuStructDocument? {
guard let previous = undoStack.popLast() else { return nil }
redoStack.append(current)
return previous
}

/// Liefert den wiederhergestellten Zustand und legt `current` für Undo ab; `nil`, wenn nichts wiederherzustellen ist.
public mutating func redo(from current: VisuStructDocument) -> VisuStructDocument? {
guard let next = redoStack.popLast() else { return nil }
pushUndo(current)
return next
}

public mutating func reset() {
undoStack.removeAll()
redoStack.removeAll()
}

private mutating func pushUndo(_ document: VisuStructDocument) {
undoStack.append(document)
while undoStack.count > maxSteps {
undoStack.removeFirst()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import XCTest
import VisuStructCore

final class EditHistoryTests: XCTestCase {
private func document(_ texts: String...) -> VisuStructDocument {
var doc = VisuStructDocumentEditing.newEmpty()
doc.elements = texts.map { VisuStructElement(type: .anweisung, textLines: [$0]) }
return doc
}

func testUndoAndRedoRestoreSnapshots() {
let a = document("a"), b = document("b"), c = document("c")
var history = VisuStructEditHistory()
XCTAssertFalse(history.canUndo)
history.record(a)
history.record(b)
XCTAssertEqual(history.undo(from: c), b)
XCTAssertEqual(history.undo(from: b), a)
XCTAssertNil(history.undo(from: a))
XCTAssertEqual(history.redo(from: a), b)
XCTAssertEqual(history.redo(from: b), c)
XCTAssertNil(history.redo(from: c))
}

func testRecordClearsRedo() {
var history = VisuStructEditHistory()
history.record(document("a"))
_ = history.undo(from: document("b"))
XCTAssertTrue(history.canRedo)
history.record(document("a"))
XCTAssertFalse(history.canRedo)
}

func testUndoStackIsLimited() {
var history = VisuStructEditHistory(maxSteps: 3)
for i in 0..<5 { history.record(document("\(i)")) }
XCTAssertEqual(history.undoStack, [document("2"), document("3"), document("4")])
XCTAssertEqual(VisuStructEditHistory().maxSteps, 80)
}

func testRedoAlsoRespectsLimit() {
var history = VisuStructEditHistory(maxSteps: 2)
history.record(document("0"))
history.record(document("1"))
_ = history.undo(from: document("2"))
history.record(document("x"))
history.record(document("y"))
XCTAssertEqual(history.undoStack.count, 2)
}

func testReset() {
var history = VisuStructEditHistory()
history.record(document("a"))
_ = history.undo(from: document("b"))
history.record(document("c"))
history.reset()
XCTAssertFalse(history.canUndo)
XCTAssertFalse(history.canRedo)
}

func testSanitizedSelection() {
let doc = document("a", "b")
XCTAssertEqual(VisuStructDocumentEditing.sanitizedSelection([1], in: doc), [1])
XCTAssertEqual(VisuStructDocumentEditing.sanitizedSelection([5], in: doc), [0])
XCTAssertEqual(VisuStructDocumentEditing.sanitizedSelection(nil, in: doc), [0])
var empty = doc
empty.elements = []
XCTAssertNil(VisuStructDocumentEditing.sanitizedSelection([0], in: empty))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import XCTest
import VisuStructCore

final class MultiwayPathTests: XCTestCase {
/// `[0]` Anweisung, `[1]` Fallauswahl mit zwei Fällen; im zweiten Fall eine innere Fallauswahl `[1, 1, 0]`.
private func document() -> VisuStructDocument {
let statement = VisuStructElement(type: .anweisung, textLines: ["x = 1"])
let inner = VisuStructElement(
type: .fallauswahl,
textLines: ["y"],
cases: [
VisuStructCase(name: "1", elements: [statement]),
VisuStructCase(name: "default", elements: [statement]),
]
)
let outer = VisuStructElement(
type: .fallauswahl,
textLines: ["x"],
cases: [
VisuStructCase(name: "1", elements: [statement]),
VisuStructCase(name: "default", elements: [inner]),
]
)
var doc = VisuStructDocumentEditing.newEmpty()
doc.elements = [statement, outer]
return doc
}

func testAncestorIsInnermostFallauswahl() {
let doc = document()
XCTAssertNil(VisuStructDocumentEditing.multiwayAncestorPath(for: nil, in: doc))
XCTAssertNil(VisuStructDocumentEditing.multiwayAncestorPath(for: [0], in: doc))
XCTAssertEqual(VisuStructDocumentEditing.multiwayAncestorPath(for: [1], in: doc), [1])
XCTAssertEqual(VisuStructDocumentEditing.multiwayAncestorPath(for: [1, 0, 0], in: doc), [1])
XCTAssertEqual(VisuStructDocumentEditing.multiwayAncestorPath(for: [1, 1, 0], in: doc), [1, 1, 0])
XCTAssertEqual(VisuStructDocumentEditing.multiwayAncestorPath(for: [1, 1, 0, 1, 0], in: doc), [1, 1, 0])
}

func testCaseIndex() {
XCTAssertNil(VisuStructDocumentEditing.caseIndex(of: [1], inMultiwayAt: [1]))
XCTAssertNil(VisuStructDocumentEditing.caseIndex(of: nil, inMultiwayAt: [1]))
XCTAssertEqual(VisuStructDocumentEditing.caseIndex(of: [1, 1, 0], inMultiwayAt: [1]), 1)
}
}
Loading
Loading