// 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 cards) : cards = List.unmodifiable(cards); static final Board empty = Board(const []); final List 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 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 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); }