116 lines
3.3 KiB
Dart
116 lines
3.3 KiB
Dart
|
|
// lib/editor/board/board.dart
|
||
|
|
//
|
||
|
|
// Infinite-board model (F7 — 便利贴 + 双链). A board is a set of positioned
|
||
|
|
// cards (sticky notes) in board content coordinates; each card has text that may
|
||
|
|
// contain [[links]] to other cards, so a board derives a LinkGraph for
|
||
|
|
// backlinks. Cards also host ink (via a StrokeHost keyed by the card id) — the
|
||
|
|
// same host-agnostic engine as PDF pages.
|
||
|
|
//
|
||
|
|
// Pure, immutable value model (no widgets/storage); fully unit-tested. The board
|
||
|
|
// canvas + persistence wrap it.
|
||
|
|
|
||
|
|
import 'dart:ui' show Offset, Rect, Size;
|
||
|
|
|
||
|
|
import 'package:flutter/foundation.dart';
|
||
|
|
|
||
|
|
import '../link/link_graph.dart';
|
||
|
|
|
||
|
|
/// One sticky-note card on the board.
|
||
|
|
@immutable
|
||
|
|
class BoardCard {
|
||
|
|
const BoardCard({
|
||
|
|
required this.id,
|
||
|
|
required this.position,
|
||
|
|
required this.size,
|
||
|
|
this.text = '',
|
||
|
|
});
|
||
|
|
|
||
|
|
final String id;
|
||
|
|
|
||
|
|
/// Top-left in board content coordinates.
|
||
|
|
final Offset position;
|
||
|
|
final Size size;
|
||
|
|
|
||
|
|
/// Card body; may contain `[[other-card]]` links.
|
||
|
|
final String text;
|
||
|
|
|
||
|
|
Rect get bounds => position & size;
|
||
|
|
|
||
|
|
BoardCard copyWith({Offset? position, Size? size, String? text}) => BoardCard(
|
||
|
|
id: id,
|
||
|
|
position: position ?? this.position,
|
||
|
|
size: size ?? this.size,
|
||
|
|
text: text ?? this.text,
|
||
|
|
);
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool operator ==(Object other) =>
|
||
|
|
other is BoardCard &&
|
||
|
|
other.id == id &&
|
||
|
|
other.position == position &&
|
||
|
|
other.size == size &&
|
||
|
|
other.text == text;
|
||
|
|
|
||
|
|
@override
|
||
|
|
int get hashCode => Object.hash(id, position, size, text);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// An immutable infinite board: an ordered list of cards with copy-on-write
|
||
|
|
/// edits. Card ids are unique.
|
||
|
|
@immutable
|
||
|
|
class Board {
|
||
|
|
Board(List<BoardCard> cards) : cards = List<BoardCard>.unmodifiable(cards);
|
||
|
|
|
||
|
|
static final Board empty = Board(const []);
|
||
|
|
|
||
|
|
final List<BoardCard> cards;
|
||
|
|
|
||
|
|
int get length => cards.length;
|
||
|
|
|
||
|
|
BoardCard? cardById(String id) {
|
||
|
|
for (final c in cards) {
|
||
|
|
if (c.id == id) return c;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Add a card (throws if [card].id already exists).
|
||
|
|
Board add(BoardCard card) {
|
||
|
|
if (cardById(card.id) != null) {
|
||
|
|
throw ArgumentError('duplicate card id: ${card.id}');
|
||
|
|
}
|
||
|
|
return Board([...cards, card]);
|
||
|
|
}
|
||
|
|
|
||
|
|
Board removeById(String id) =>
|
||
|
|
Board([for (final c in cards) if (c.id != id) c]);
|
||
|
|
|
||
|
|
/// Replace card [id] via [update]; no-op if absent.
|
||
|
|
Board updateCard(String id, BoardCard Function(BoardCard) update) =>
|
||
|
|
Board([for (final c in cards) if (c.id == id) update(c) else c]);
|
||
|
|
|
||
|
|
Board moveCard(String id, Offset position) =>
|
||
|
|
updateCard(id, (c) => c.copyWith(position: position));
|
||
|
|
|
||
|
|
Board setText(String id, String text) =>
|
||
|
|
updateCard(id, (c) => c.copyWith(text: text));
|
||
|
|
|
||
|
|
/// Cards whose bounds overlap [viewport] (board broad-phase culling).
|
||
|
|
List<BoardCard> cardsIn(Rect viewport) =>
|
||
|
|
[for (final c in cards) if (c.bounds.overlaps(viewport)) c];
|
||
|
|
|
||
|
|
/// Derive the 双链 graph from card texts (card id → its [[links]]).
|
||
|
|
LinkGraph linkGraph() =>
|
||
|
|
LinkGraph.fromTexts({for (final c in cards) c.id: c.text});
|
||
|
|
|
||
|
|
/// Backlinks to card [id] (ids of cards whose text links to it).
|
||
|
|
Set<String> backlinksOf(String id) => linkGraph().backlinksOf(id);
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool operator ==(Object other) =>
|
||
|
|
other is Board && listEquals(other.cards, cards);
|
||
|
|
|
||
|
|
@override
|
||
|
|
int get hashCode => Object.hashAll(cards);
|
||
|
|
}
|