Flutter for Harmony 跨平台开发实战:德劳内三角剖分——点集连接的几何美学

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


🔺 一、德劳内三角剖分:数学之美

📚 1.1 三角剖分基础

三角剖分是将平面点集连接成不重叠三角形集合的过程。德劳内三角剖分是最优三角剖分方法之一,由俄国数学家鲍里斯·德劳内(Boris Delaunay)于1934年提出。

三角剖分定义

复制代码
给定平面点集 P = {p₁, p₂, ..., pₙ}

三角剖分 T 是满足以下条件的三角形集合:
1. 三角形顶点都是 P 中的点
2. 任意两个三角形内部不相交
3. 所有三角形的并集是点集的凸包

三角剖分数量:
- n 个点的凸多边形有 C_{n-2} 种三角剖分
- C_{n-2} 是卡特兰数

三角剖分示意

复制代码
点集:        一种三角剖分:      另一种三角剖分:

  ●   ●           ●───●             ●   ●
    ●             │╲ ╱│               ╲ ╱
  ●   ●           ●─●─●             ●───●
                  │╱ ╲│               ╱╲
                ●───●             ●───●

同一点集可以有多种三角剖分方式
德劳内三角剖分是最优的一种

📐 1.2 德劳内条件

德劳内三角剖分的核心是空圆性质(Empty Circle Property)。

空圆性质

复制代码
对于三角剖分中的任意三角形:

其外接圆内部不包含任何其他点

形式化:
对于三角形 T = (pᵢ, pⱼ, pₖ)
设 C(T) 为其外接圆
则对于所有 pₗ ∈ P \ {pᵢ, pⱼ, pₖ}
pₗ 不在 C(T) 内部

外接圆示意

复制代码
德劳内三角形:          非德劳内三角形:

      ●                      ●
     ╱ ╲                    ╱ ╲
    ╱   ╲                  ╱ ● ╲  ← 点在圆内
   ╱     ╲                ╱     ╲
  ●───────●              ●───────●
  └───────┘              └───────┘
   外接圆                   外接圆
  (空,无点)              (不空,有点)

德劳内三角形的外接圆是"空"的

等价条件

复制代码
以下条件等价:

1. 空圆性质
   每个三角形的外接圆不含其他点

2. 最大化最小角
   在所有可能的三角剖分中
   德劳内三角剖分的最小角最大

3. 边翻转最优
   对于任意四边形,对角线选择使最小角最大

4. Voronoi 对偶
   德劳内三角剖分是 Voronoi 图的对偶图

🔬 1.3 Voronoi 图与对偶性

Voronoi 图是德劳内三角剖分的对偶结构,两者密切相关。

Voronoi 图定义

复制代码
对于点集 P = {p₁, p₂, ..., pₙ}

点 pᵢ 的 Voronoi 区域:
V(pᵢ) = {x ∈ ℝ² : d(x, pᵢ) ≤ d(x, pⱼ), ∀j ≠ i}

Voronoi 图是所有 Voronoi 区域的边界

性质:
- 每个 Voronoi 区域是凸多边形
- Voronoi 边是两点连线的垂直平分线
- Voronoi 顶点是三个或更多区域的交点

对偶关系

复制代码
Voronoi 图 ←→ 德劳内三角剖分

对应关系:
- Voronoi 区域 ←→ 德劳内顶点
- Voronoi 边 ←→ 德劳内边
- Voronoi 顶点 ←→ 德劳内三角形

示意:

Voronoi 图:          德劳内三角剖分:

  ┌───┬───┐              ●───●
  │ ● │ ● │              │╲ ╱│
  ├───┼───┤    对偶      ●─●─●
  │ ● │ ● │   ────→      │╱ ╲│
  └───┴───┘              ●───●

🎯 1.4 德劳内三角剖分的性质

几何性质

性质 描述
空圆性 外接圆不含其他点
最大化最小角 最小角最大
唯一性 无四点共圆时唯一
局部性 局部修改不影响全局
凸包性 边界是点集凸包

最优性证明

复制代码
最大化最小角定理:

在所有可能的三角剖分中
德劳内三角剖分的最小角最大

证明思路:
1. 任何非德劳内边都可以翻转
2. 边翻转会增加最小角
3. 最终达到德劳内条件时最小角最大

边翻转操作:
    ●───●              ●───●
     ╲ ╱                ╱ ╲
      ●        →        ●
     ╱ ╲                ╲ ╱
    ●───●              ●───●

翻转对角线,使最小角增大

🔧 二、德劳内三角剖分的 Dart 实现

🧮 2.1 基础数据结构

dart 复制代码
import 'dart:math';

/// 二维点
class Point2D {
  final double x;
  final double y;
  
  const Point2D(this.x, this.y);
  
  double distanceTo(Point2D other) {
    return sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y));
  }
  
  double squaredDistanceTo(Point2D other) {
    return (x - other.x) * (x - other.x) + (y - other.y) * (y - other.y);
  }
  
  Offset toOffset() => Offset(x, y);
  
  @override
  bool operator ==(Object other) =>
      other is Point2D && x == other.x && y == other.y;
  
  @override
  int get hashCode => Object.hash(x, y);
  
  @override
  String toString() => 'Point2D($x, $y)';
}

/// 边
class Edge {
  final Point2D p1;
  final Point2D p2;
  
  const Edge(this.p1, this.p2);
  
  double get length => p1.distanceTo(p2);
  
  Point2D get midpoint => Point2D((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
  
  bool contains(Point2D p) => p == p1 || p == p2;
  
  bool sharesVertexWith(Edge other) =>
      p1 == other.p1 || p1 == other.p2 || p2 == other.p1 || p2 == other.p2;
  
  @override
  bool operator ==(Object other) =>
      other is Edge && ((p1 == other.p1 && p2 == other.p2) || (p1 == other.p2 && p2 == other.p1));
  
  @override
  int get hashCode => Object.hash(p1, p2) ^ Object.hash(p2, p1);
}

/// 三角形
class Triangle {
  final Point2D p1;
  final Point2D p2;
  final Point2D p3;
  
  const Triangle(this.p1, this.p2, this.p3);
  
  List<Point2D> get vertices => [p1, p2, p3];
  
  List<Edge> get edges => [Edge(p1, p2), Edge(p2, p3), Edge(p3, p1)];
  
  bool containsVertex(Point2D p) => p == p1 || p == p2 || p == p3;
  
  bool containsEdge(Edge e) => edges.contains(e);
  
  Point2D get circumcenter {
    final ax = p1.x, ay = p1.y;
    final bx = p2.x, by = p2.y;
    final cx = p3.x, cy = p3.y;
    
    final d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
    
    if (d.abs() < 1e-10) {
      return Point2D(double.infinity, double.infinity);
    }
    
    final ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
    final uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
    
    return Point2D(ux, uy);
  }
  
  double get circumradius {
    final center = circumcenter;
    return center.distanceTo(p1);
  }
  
  bool isPointInCircumcircle(Point2D p) {
    final center = circumcenter;
    final radius = circumradius;
    return center.distanceTo(p) < radius - 1e-10;
  }
  
  bool isPointInside(Point2D p) {
    final v0 = p3.x - p1.x, v1 = p3.y - p1.y;
    final v2 = p.x - p1.x, v3 = p.y - p1.y;
    final v4 = p2.x - p1.x, v5 = p2.y - p1.y;
    
    final denom = v4 * v1 - v5 * v0;
    if (denom.abs() < 1e-10) return false;
    
    final a = (v2 * v1 - v3 * v0) / denom;
    final b = (v4 * v3 - v5 * v2) / denom;
    
    return a >= 0 && b >= 0 && a + b <= 1;
  }
  
  double get area {
    return ((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).abs() / 2;
  }
  
  double get minAngle {
    final a = p2.distanceTo(p3);
    final b = p1.distanceTo(p3);
    final c = p1.distanceTo(p2);
    
    final angleA = acos((b * b + c * c - a * a) / (2 * b * c));
    final angleB = acos((a * a + c * c - b * b) / (2 * a * c));
    final angleC = acos((a * a + b * b - c * c) / (2 * a * b));
    
    return [angleA, angleB, angleC].reduce(min);
  }
}

⚡ 2.2 Bowyer-Watson 算法

dart 复制代码
/// 德劳内三角剖分器
class DelaunayTriangulation {
  final List<Point2D> points;
  List<Triangle> triangles = [];
  
  DelaunayTriangulation(this.points);
  
  void compute() {
    if (points.length < 3) return;
    
    double minX = points[0].x, maxX = points[0].x;
    double minY = points[0].y, maxY = points[0].y;
    
    for (final p in points) {
      if (p.x < minX) minX = p.x;
      if (p.x > maxX) maxX = p.x;
      if (p.y < minY) minY = p.y;
      if (p.y > maxY) maxY = p.y;
    }
    
    final dx = maxX - minX;
    final dy = maxY - minY;
    final deltaMax = max(dx, dy) * 2;
    
    final midX = (minX + maxX) / 2;
    final midY = (minY + maxY) / 2;
    
    final p1 = Point2D(midX - deltaMax, midY - deltaMax);
    final p2 = Point2D(midX, midY + deltaMax * 2);
    final p3 = Point2D(midX + deltaMax * 2, midY - deltaMax);
    
    final superTriangle = Triangle(p1, p2, p3);
    triangles = [superTriangle];
    
    for (final point in points) {
      _addPoint(point);
    }
    
    triangles = triangles.where((t) =>
        !t.containsVertex(p1) && !t.containsVertex(p2) && !t.containsVertex(p3)
    ).toList();
  }
  
  void _addPoint(Point2D point) {
    final badTriangles = <Triangle>[];
    
    for (final triangle in triangles) {
      if (triangle.isPointInCircumcircle(point)) {
        badTriangles.add(triangle);
      }
    }
    
    final polygon = <Edge>[];
    
    for (final triangle in badTriangles) {
      for (final edge in triangle.edges) {
        bool isShared = false;
        for (final other in badTriangles) {
          if (triangle != other && other.containsEdge(edge)) {
            isShared = true;
            break;
          }
        }
        if (!isShared) {
          polygon.add(edge);
        }
      }
    }
    
    triangles.removeWhere((t) => badTriangles.contains(t));
    
    for (final edge in polygon) {
      final newTriangle = Triangle(edge.p1, edge.p2, point);
      triangles.add(newTriangle);
    }
  }
  
  List<Edge> getEdges() {
    final edges = <Edge>{};
    for (final triangle in triangles) {
      edges.addAll(triangle.edges);
    }
    return edges.toList();
  }
  
  double get minAngle {
    if (triangles.isEmpty) return 0;
    return triangles.map((t) => t.minAngle).reduce(min);
  }
  
  double get totalArea {
    return triangles.fold(0.0, (sum, t) => sum + t.area);
  }
}

🎨 2.3 Voronoi 图生成

dart 复制代码
/// Voronoi 图生成器
class VoronoiDiagram {
  final List<Point2D> points;
  final DelaunayTriangulation delaunay;
  
  List<Point2D> vertices = [];
  List<VoronoiEdge> edges = [];
  
  VoronoiDiagram(this.points) : delaunay = DelaunayTriangulation(points);
  
  void compute() {
    delaunay.compute();
    
    final triangleToVertex = <Triangle, Point2D>{};
    
    for (final triangle in delaunay.triangles) {
      final center = triangle.circumcenter;
      triangleToVertex[triangle] = center;
      if (!vertices.contains(center)) {
        vertices.add(center);
      }
    }
    
    for (final triangle in delaunay.triangles) {
      final center = triangleToVertex[triangle]!;
      
      for (final edge in triangle.edges) {
        final neighbor = _findNeighborTriangle(triangle, edge);
        
        if (neighbor != null) {
          final neighborCenter = triangleToVertex[neighbor]!;
          
          final voronoiEdge = VoronoiEdge(center, neighborCenter, edge);
          if (!edges.any((e) => e == voronoiEdge)) {
            edges.add(voronoiEdge);
          }
        }
      }
    }
  }
  
  Triangle? _findNeighborTriangle(Triangle triangle, Edge edge) {
    for (final other in delaunay.triangles) {
      if (other != triangle && other.containsEdge(edge)) {
        return other;
      }
    }
    return null;
  }
  
  List<Point2D> getCell(Point2D point) {
    final cellVertices = <Point2D>[];
    
    for (final triangle in delaunay.triangles) {
      if (triangle.containsVertex(point)) {
        cellVertices.add(triangle.circumcenter);
      }
    }
    
    return _sortVerticesClockwise(cellVertices);
  }
  
  List<Point2D> _sortVerticesClockwise(List<Point2D> vertices) {
    if (vertices.length < 3) return vertices;
    
    double cx = 0, cy = 0;
    for (final v in vertices) {
      cx += v.x;
      cy += v.y;
    }
    cx /= vertices.length;
    cy /= vertices.length;
    
    vertices.sort((a, b) {
      final angleA = atan2(a.y - cy, a.x - cx);
      final angleB = atan2(b.y - cy, b.x - cx);
      return angleA.compareTo(angleB);
    });
    
    return vertices;
  }
}

class VoronoiEdge {
  final Point2D start;
  final Point2D end;
  final Edge delaunayEdge;
  
  VoronoiEdge(this.start, this.end, this.delaunayEdge);
  
  @override
  bool operator ==(Object other) =>
      other is VoronoiEdge && start == other.start && end == other.end;
  
  @override
  int get hashCode => Object.hash(start, end);
}

📦 三、完整示例代码

以下是完整的德劳内三角剖分可视化示例代码:

dart 复制代码
import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(const DelaunayApp());
}

class DelaunayApp extends StatelessWidget {
  const DelaunayApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '德劳内三角剖分',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark),
        useMaterial3: true,
      ),
      home: const DelaunayHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class DelaunayHomePage extends StatelessWidget {
  const DelaunayHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('🔺 德劳内三角剖分'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildCard(context, title: '交互式剖分', description: '点击添加点,实时计算', icon: Icons.touch_app, color: Colors.indigo,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const InteractiveDemo()))),
          _buildCard(context, title: '随机点集', description: '生成随机点并剖分', icon: Icons.shuffle, color: Colors.purple,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const RandomPointsDemo()))),
          _buildCard(context, title: 'Voronoi 图', description: '德劳内的对偶图', icon: Icons.grid_on, color: Colors.teal,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const VoronoiDemo()))),
          _buildCard(context, title: '动画演示', description: '算法执行过程', icon: Icons.animation, color: Colors.orange,
              onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AnimationDemo()))),
        ],
      ),
    );
  }

  Widget _buildCard(BuildContext context, {required String title, required String description, required IconData icon, required Color color, required VoidCallback onTap}) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(16),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(children: [
            Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
                child: Icon(icon, color: color, size: 28)),
            const SizedBox(width: 16),
            Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
              const SizedBox(height: 4),
              Text(description, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
            ])),
            Icon(Icons.chevron_right, color: Colors.grey[400]),
          ]),
        ),
      ),
    );
  }
}

/// 点类
class P {
  final double x, y;
  const P(this.x, this.y);
  double dist(P o) => sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y));
  @override bool operator ==(Object o) => o is P && x == o.x && y == o.y;
  @override int get hashCode => Object.hash(x, y);
}

/// 三角形类
class Tri {
  final P a, b, c;
  Tri(this.a, this.b, this.c);
  
  List<P> get verts => [a, b, c];
  
  P circumcenter() {
    final d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
    if (d.abs() < 1e-10) return P(double.infinity, double.infinity);
    final ux = ((a.x*a.x + a.y*a.y) * (b.y - c.y) + (b.x*b.x + b.y*b.y) * (c.y - a.y) + (c.x*c.x + c.y*c.y) * (a.y - b.y)) / d;
    final uy = ((a.x*a.x + a.y*a.y) * (c.x - b.x) + (b.x*b.x + b.y*b.y) * (a.x - c.x) + (c.x*c.x + c.y*c.y) * (b.x - a.x)) / d;
    return P(ux, uy);
  }
  
  double circumradius() => circumcenter().dist(a);
  
  bool inCircumcircle(P p) => circumcenter().dist(p) < circumradius() - 1e-10;
  
  bool hasVert(P p) => a == p || b == p || c == p;
  
  double minAngle() {
    final ab = a.dist(b), bc = b.dist(c), ca = c.dist(a);
    return [acos((ab*ab + ca*ca - bc*bc)/(2*ab*ca)), acos((ab*ab + bc*bc - ca*ca)/(2*ab*bc)), acos((bc*bc + ca*ca - ab*ab)/(2*bc*ca))].reduce(min);
  }
}

/// 德劳内剖分
List<Tri> delaunay(List<P> pts) {
  if (pts.length < 3) return [];
  
  double minX = pts[0].x, maxX = pts[0].x, minY = pts[0].y, maxY = pts[0].y;
  for (final p in pts) {
    if (p.x < minX) minX = p.x;
    if (p.x > maxX) maxX = p.x;
    if (p.y < minY) minY = p.y;
    if (p.y > maxY) maxY = p.y;
  }
  
  final dx = maxX - minX, dy = maxY - minY;
  final dm = max(dx, dy) * 3;
  final mx = (minX + maxX) / 2, my = (minY + maxY) / 2;
  
  final sp1 = P(mx - dm, my - dm), sp2 = P(mx, my + dm), sp3 = P(mx + dm, my - dm);
  var tris = [Tri(sp1, sp2, sp3)];
  
  for (final pt in pts) {
    final bad = <Tri>[];
    for (final t in tris) { if (t.inCircumcircle(pt)) bad.add(t); }
    
    final poly = <(P, P)>[];
    for (final t in bad) {
      for (final e in [(t.a, t.b), (t.b, t.c), (t.c, t.a)]) {
        var shared = false;
        for (final o in bad) {
          if (t != o && ((o.a == e.$1 && o.b == e.$2) || (o.a == e.$2 && o.b == e.$1) ||
              (o.b == e.$1 && o.c == e.$2) || (o.b == e.$2 && o.c == e.$1) ||
              (o.c == e.$1 && o.a == e.$2) || (o.c == e.$2 && o.a == e.$1))) {
            shared = true; break;
          }
        }
        if (!shared) poly.add(e);
      }
    }
    
    tris.removeWhere((t) => bad.contains(t));
    for (final e in poly) { tris.add(Tri(e.$1, e.$2, pt)); }
  }
  
  return tris.where((t) => !t.hasVert(sp1) && !t.hasVert(sp2) && !t.hasVert(sp3)).toList();
}

/// 交互式演示
class InteractiveDemo extends StatefulWidget {
  const InteractiveDemo({super.key});
  @override
  State<InteractiveDemo> createState() => _InteractiveDemoState();
}

class _InteractiveDemoState extends State<InteractiveDemo> {
  final List<P> _points = [];
  List<Tri> _tris = [];
  bool _showCircum = false;

  void _addPoint(Offset pos) {
    setState(() {
      _points.add(P(pos.dx, pos.dy));
      _tris = delaunay(_points);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('交互式剖分'), actions: [
        IconButton(icon: const Icon(Icons.refresh), onPressed: () => setState(() { _points.clear(); _tris.clear(); })),
        IconButton(icon: Icon(_showCircum ? Icons.circle : Icons.circle_outlined), onPressed: () => setState(() => _showCircum = !_showCircum)),
      ]),
      body: GestureDetector(
        onTapDown: (d) => _addPoint(d.localPosition),
        child: CustomPaint(painter: DelaunayPainter(_points, _tris, _showCircum), size: Size.infinite),
      ),
      bottomNavigationBar: Container(
        padding: const EdgeInsets.all(12),
        color: Colors.grey[900],
        child: Text('点数: ${_points.length}  三角形: ${_tris.length}  最小角: ${_tris.isEmpty ? 0 : _tris.map((t) => t.minAngle() * 180 / pi).reduce(min).toStringAsFixed(1)}°',
            style: const TextStyle(color: Colors.white70), textAlign: TextAlign.center),
      ),
    );
  }
}

class DelaunayPainter extends CustomPainter {
  final List<P> pts;
  final List<Tri> tris;
  final bool showCircum;
  DelaunayPainter(this.pts, this.tris, this.showCircum);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    if (showCircum) {
      for (final t in tris) {
        final c = t.circumcenter();
        final r = t.circumradius();
        canvas.drawCircle(Offset(c.x, c.y), r, Paint()..color = Colors.indigo.withOpacity(0.1)..style = PaintingStyle.fill);
        canvas.drawCircle(Offset(c.x, c.y), r, Paint()..color = Colors.indigo.withOpacity(0.3)..style = PaintingStyle.stroke);
        canvas.drawCircle(Offset(c.x, c.y), 3, Paint()..color = Colors.indigo);
      }
    }
    
    for (final t in tris) {
      final path = Path()..moveTo(t.a.x, t.a.y)..lineTo(t.b.x, t.b.y)..lineTo(t.c.x, t.c.y)..close();
      final hue = (t.minAngle() * 180 / pi) * 3;
      canvas.drawPath(path, Paint()..color = HSVColor.fromAHSV(0.3, hue, 0.6, 0.8).toColor()..style = PaintingStyle.fill);
      canvas.drawPath(path, Paint()..color = Colors.white.withOpacity(0.5)..style = PaintingStyle.stroke..strokeWidth = 1);
    }
    
    for (final p in pts) {
      canvas.drawCircle(Offset(p.x, p.y), 5, Paint()..color = Colors.white);
      canvas.drawCircle(Offset(p.x, p.y), 3, Paint()..color = Colors.indigo);
    }
  }

  @override
  bool shouldRepaint(covariant DelaunayPainter old) => true;
}

/// 随机点集演示
class RandomPointsDemo extends StatefulWidget {
  const RandomPointsDemo({super.key});
  @override
  State<RandomPointsDemo> createState() => _RandomPointsDemoState();
}

class _RandomPointsDemoState extends State<RandomPointsDemo> {
  List<P> _pts = [];
  List<Tri> _tris = [];
  int _count = 50;

  @override
  void initState() {
    super.initState();
    _generate();
  }
  
  void _generate() {
    final r = Random();
    _pts = List.generate(_count, (_) => P(r.nextDouble() * 300 + 50, r.nextDouble() * 400 + 100));
    _tris = delaunay(_pts);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('随机点集')),
      body: Column(children: [
        Expanded(child: CustomPaint(painter: DelaunayPainter(_pts, _tris, false), size: Size.infinite)),
        Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(color: Colors.grey[900], borderRadius: const BorderRadius.vertical(top: Radius.circular(20))),
          child: Column(mainAxisSize: MainAxisSize.min, children: [
            Row(children: [
              const Text('点数: ', style: TextStyle(color: Colors.white70)),
              Expanded(child: Slider(value: _count.toDouble(), min: 10, max: 200, onChanged: (v) => setState(() => _count = v.toInt()))),
              Text('$_count', style: const TextStyle(color: Colors.purple)),
            ]),
            ElevatedButton(onPressed: _generate, child: const Text('生成新点集')),
          ]),
        ),
      ]),
    );
  }
}

/// Voronoi 图演示
class VoronoiDemo extends StatefulWidget {
  const VoronoiDemo({super.key});
  @override
  State<VoronoiDemo> createState() => _VoronoiDemoState();
}

class _VoronoiDemoState extends State<VoronoiDemo> {
  final List<P> _pts = [];
  List<Tri> _tris = [];
  bool _showDelaunay = true;

  void _addPoint(Offset pos) {
    setState(() {
      _pts.add(P(pos.dx, pos.dy));
      _tris = delaunay(_pts);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Voronoi 图'), actions: [
        IconButton(icon: const Icon(Icons.refresh), onPressed: () => setState(() { _pts.clear(); _tris.clear(); })),
        IconButton(icon: Icon(_showDelaunay ? Icons.grid_on : Icons.grid_off), onPressed: () => setState(() => _showDelaunay = !_showDelaunay)),
      ]),
      body: GestureDetector(
        onTapDown: (d) => _addPoint(d.localPosition),
        child: CustomPaint(painter: VoronoiPainter(_pts, _tris, _showDelaunay), size: Size.infinite),
      ),
    );
  }
}

class VoronoiPainter extends CustomPainter {
  final List<P> pts;
  final List<Tri> tris;
  final bool showDelaunay;
  VoronoiPainter(this.pts, this.tris, this.showDelaunay);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    final colors = [Colors.red, Colors.green, Colors.blue, Colors.yellow, Colors.purple, Colors.cyan, Colors.orange, Colors.pink];
    
    for (int i = 0; i < pts.length; i++) {
      final p = pts[i];
      final cellTris = tris.where((t) => t.hasVert(p)).toList();
      if (cellTris.isEmpty) continue;
      
      final centers = cellTris.map((t) => t.circumcenter()).toList();
      centers.sort((a, b) => atan2(a.y - p.y, a.x - p.x).compareTo(atan2(b.y - p.y, b.x - p.x)));
      
      final path = Path();
      for (int j = 0; j < centers.length; j++) {
        if (j == 0) path.moveTo(centers[j].x, centers[j].y);
        else path.lineTo(centers[j].x, centers[j].y);
      }
      path.close();
      
      canvas.drawPath(path, Paint()..color = colors[i % colors.length].withOpacity(0.3)..style = PaintingStyle.fill);
    }
    
    if (showDelaunay) {
      for (final t in tris) {
        final path = Path()..moveTo(t.a.x, t.a.y)..lineTo(t.b.x, t.b.y)..lineTo(t.c.x, t.c.y)..close();
        canvas.drawPath(path, Paint()..color = Colors.white24..style = PaintingStyle.stroke..strokeWidth = 1);
      }
    }
    
    for (final p in pts) {
      canvas.drawCircle(Offset(p.x, p.y), 4, Paint()..color = Colors.white);
    }
  }

  @override
  bool shouldRepaint(covariant VoronoiPainter old) => true;
}

/// 动画演示
class AnimationDemo extends StatefulWidget {
  const AnimationDemo({super.key});
  @override
  State<AnimationDemo> createState() => _AnimationDemoState();
}

class _AnimationDemoState extends State<AnimationDemo> with SingleTickerProviderStateMixin {
  late AnimationController _ctrl;
  List<P> _pts = [];
  List<Tri> _tris = [];
  int _step = 0;
  double _time = 0;

  @override
  void initState() {
    super.initState();
    _generatePoints();
    _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 16))..repeat();
    _ctrl.addListener(_update);
  }
  
  void _generatePoints() {
    final r = Random();
    _pts = List.generate(30, (_) => P(r.nextDouble() * 300 + 50, r.nextDouble() * 400 + 100));
  }
  
  void _update() {
    _time += 0.016;
    if (_time > 0.5) {
      _time = 0;
      _step++;
      if (_step > _pts.length) { _step = 0; _generatePoints(); _tris.clear(); }
      else { _tris = delaunay(_pts.sublist(0, _step)); }
      setState(() {});
    }
  }

  @override
  void dispose() { _ctrl.removeListener(_update); _ctrl.dispose(); super.dispose(); }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('动画演示')),
      body: Column(children: [
        Expanded(child: CustomPaint(painter: AnimationPainter(_pts, _tris, _step), size: Size.infinite)),
        Padding(
          padding: const EdgeInsets.all(16),
          child: Text('添加点: $_step / ${_pts.length}', style: const TextStyle(color: Colors.white70)),
        ),
      ]),
    );
  }
}

class AnimationPainter extends CustomPainter {
  final List<P> pts;
  final List<Tri> tris;
  final int step;
  AnimationPainter(this.pts, this.tris, this.step);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = const Color(0xFF0a0a15));
    
    for (final t in tris) {
      final path = Path()..moveTo(t.a.x, t.a.y)..lineTo(t.b.x, t.b.y)..lineTo(t.c.x, t.c.y)..close();
      canvas.drawPath(path, Paint()..color = Colors.orange.withOpacity(0.2)..style = PaintingStyle.fill);
      canvas.drawPath(path, Paint()..color = Colors.orange..style = PaintingStyle.stroke..strokeWidth = 1);
    }
    
    for (int i = 0; i < pts.length; i++) {
      final p = pts[i];
      canvas.drawCircle(Offset(p.x, p.y), i < step ? 5 : 3, Paint()..color = i < step ? Colors.white : Colors.white30);
    }
  }

  @override
  bool shouldRepaint(covariant AnimationPainter old) => true;
}

📝 四、数学原理深入解析

📐 4.1 外接圆计算

外接圆公式推导

复制代码
给定三角形三点 A(x₁,y₁), B(x₂,y₂), C(x₃,y₃)

外接圆圆心 O(x₀,y₀) 满足:
|AO| = |BO| = |CO|

展开:
(x₀-x₁)² + (y₀-y₁)² = (x₀-x₂)² + (y₀-y₂)²
(x₀-x₁)² + (y₀-y₁)² = (x₀-x₃)² + (y₀-y₃)²

简化得:
2(x₂-x₁)x₀ + 2(y₂-y₁)y₀ = x₂²+y₂²-x₁²-y₁²
2(x₃-x₁)x₀ + 2(y₃-y₁)y₀ = x₃²+y₃²-x₁²-y₁²

用行列式求解:
D = 2(x₁(y₂-y₃) + x₂(y₃-y₁) + x₃(y₁-y₂))

x₀ = ((x₁²+y₁²)(y₂-y₃) + (x₂²+y₂²)(y₃-y₁) + (x₃²+y₃²)(y₁-y₂)) / D
y₀ = ((x₁²+y₁²)(x₃-x₂) + (x₂²+y₂²)(x₁-x₃) + (x₃²+y₃²)(x₂-x₁)) / D

半径:
R = √((x₀-x₁)² + (y₀-y₁)²)

🔄 4.2 边翻转操作

边翻转条件

复制代码
对于四边形 ABCD,对角线 AC 和 BD

如果点 D 在三角形 ABC 的外接圆内
则应该翻转边 AC 为 BD

判断条件:
θ₁ + θ₃ > π 时需要翻转

其中:
θ₁ = ∠ABC
θ₃ = ∠ADC

边翻转示意:
    A───B              A───B
     ╲ ╱                ╱ ╲
      C        →        C
     ╱ ╲                ╲ ╱
    D───?              D───?

翻转后最小角增大

翻转算法

复制代码
function flipEdge(quad):
    if not isDelaunay(quad):
        remove diagonal AC
        add diagonal BD
        return true
    return false

function isDelaunay(quad):
    // 检查点 D 是否在三角形 ABC 的外接圆内
    return not inCircumcircle(D, A, B, C)

🌸 4.3 复杂度分析

Bowyer-Watson 算法复杂度

复制代码
时间复杂度:
- 最坏情况:O(n²)
- 平均情况:O(n log n)
- 随机点集:期望 O(n log n)

空间复杂度:O(n)

优化策略:
1. 空间索引加速点查找
2. 增量插入顺序优化
3. 并行化处理

其他算法比较

算法 时间复杂度 特点
Bowyer-Watson O(n log n) 增量式,易实现
分治法 O(n log n) 递归,高效
平面扫描 O(n log n) 顺序处理
随机增量 O(n log n) 期望复杂度

🎯 4.4 应用领域

计算机图形学

复制代码
- 地形渲染:TIN(不规则三角网)
- 纹理映射:参数化
- 网格简化:LOD 生成
- 表面重建:点云处理

地理信息系统

复制代码
- DEM 生成:数字高程模型
- 等高线绘制:插值计算
- 空间分析:邻近查询
- 地形分析:坡度计算

科学计算

复制代码
- 有限元分析:网格生成
- 流体模拟:网格划分
- 气象预报:数据插值
- 地震分析:波传播模拟

🔬 五、高级应用场景

🎨 5.1 地形生成

基于德劳内的地形网格

dart 复制代码
class TerrainMesh {
  final List<Point2D> points;
  final List<double> heights;
  late List<Tri> triangles;
  
  TerrainMesh(this.points, this.heights) {
    triangles = delaunay(points);
  }
  
  double getHeightAt(double x, double y) {
    for (final t in triangles) {
      if (_pointInTriangle(x, y, t)) {
        return _interpolateHeight(x, y, t);
      }
    }
    return 0;
  }
  
  bool _pointInTriangle(double x, double y, Tri t) {
    // 重心坐标判断
    return true;
  }
  
  double _interpolateHeight(double x, double y, Tri t) {
    // 三角形内插值
    return 0;
  }
}

🌐 5.2 网格简化

边收缩简化

dart 复制代码
class MeshSimplification {
  List<Tri> simplify(List<Tri> triangles, int targetCount) {
    var current = List<Tri>.from(triangles);
    
    while (current.length > targetCount) {
      final edge = _findBestEdgeToCollapse(current);
      current = _collapseEdge(current, edge);
    }
    
    return current;
  }
  
  (P, P)? _findBestEdgeToCollapse(List<Tri> tris) {
    double minCost = double.infinity;
    (P, P)? best;
    
    for (final t in tris) {
      for (final edge in [(t.a, t.b), (t.b, t.c), (t.c, t.a)]) {
        final cost = _collapseCost(tris, edge);
        if (cost < minCost) {
          minCost = cost;
          best = edge;
        }
      }
    }
    
    return best;
  }
  
  double _collapseCost(List<Tri> tris, (P, P) edge) {
    // 计算边收缩的代价
    return edge.$1.dist(edge.$2);
  }
  
  List<Tri> _collapseEdge(List<Tri> tris, (P, P) edge) {
    // 执行边收缩
    return tris;
  }
}

📱 5.3 鸿蒙多端适配

性能优化配置

dart 复制代码
class DelaunayConfig {
  static int getMaxPointsForDevice(BuildContext context) {
    final performance = DevicePerformance.getLevel();
    switch (performance) {
      case PerformanceLevel.high: return 1000;
      case PerformanceLevel.medium: return 500;
      case PerformanceLevel.low: return 200;
    }
  }
  
  static bool shouldUseOptimizedAlgorithm(BuildContext context) {
    return DevicePerformance.getLevel() != PerformanceLevel.high;
  }
}

📊 六、性能优化策略

⚡ 6.1 空间索引

网格索引加速

dart 复制代码
class SpatialGrid {
  final double cellSize;
  final Map<(int, int), List<P>> grid = {};
  
  SpatialGrid(this.cellSize);
  
  void insert(P p) {
    final key = (p.x ~/ cellSize, p.y ~/ cellSize);
    grid.putIfAbsent(key, () => []).add(p);
  }
  
  List<P> query(P center, double radius) {
    final result = <P>[];
    final r = radius ~/ cellSize + 1;
    final cx = center.x ~/ cellSize;
    final cy = center.y ~/ cellSize;
    
    for (int i = -r; i <= r; i++) {
      for (int j = -r; j <= r; j++) {
        final key = (cx + i, cy + j);
        if (grid.containsKey(key)) {
          for (final p in grid[key]!) {
            if (p.dist(center) <= radius) {
              result.add(p);
            }
          }
        }
      }
    }
    
    return result;
  }
}

💾 6.2 增量更新

局部更新优化

dart 复制代码
class IncrementalDelaunay {
  List<P> points = [];
  List<Tri> triangles = [];
  
  void addPoint(P p) {
    points.add(p);
    
    // 只更新受影响的三角形
    final affected = triangles.where((t) => t.inCircumcircle(p)).toList();
    
    // 局部重剖分
    for (final t in affected) {
      triangles.remove(t);
    }
    
    // 添加新三角形
    // ...
  }
  
  void removePoint(P p) {
    points.remove(p);
    
    // 找到包含该点的所有三角形
    final affected = triangles.where((t) => t.hasVert(p)).toList();
    
    // 移除并重剖分
    triangles.removeWhere((t) => t.hasVert(p));
    
    // 重新剖分空洞
    // ...
  }
}

🎓 七、学习资源与拓展

📚 推荐阅读

主题 资源 难度
计算几何 《计算几何:算法与应用》 ⭐⭐⭐
三角剖分 《Triangulations and Applications》 ⭐⭐⭐
Voronoi 图 《Voronoi Diagrams and Delaunay Triangulations》 ⭐⭐
网格生成 《Mesh Generation》 ⭐⭐⭐

🔗 相关项目

  • CGAL:计算几何算法库
  • Triangle:二维网格生成器
  • Tetgen:三维网格生成器

📝 八、总结

本篇文章深入探讨了德劳内三角剖分的数学原理及其在 Flutter 中的可视化实现。

✅ 核心知识点回顾

知识点 说明
🔺 空圆性质 外接圆不含其他点
📐 最大化最小角 最优三角剖分
🔄 Voronoi 对偶 两种图的关系
Bowyer-Watson 增量式算法
🎯 应用领域 图形学、GIS、科学计算

⭐ 最佳实践要点

  • ✅ 使用空间索引加速查找
  • ✅ 注意数值精度问题
  • ✅ 处理退化情况(四点共圆)
  • ✅ 增量更新提高效率

🚀 进阶方向

  • 🔮 三维德劳内剖分
  • ✨ 约束三角剖分
  • 📊 动态网格更新
  • 🎨 表面重建应用

💡 提示:本文代码基于 Flutter for Harmony 开发,可在鸿蒙设备上流畅运行。

相关推荐
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:三方库实战 flutter_lifecycle_detector 生命周期检测详解
flutter
2601_949593652 小时前
Flutter for Harmony 跨平台开发实战:双曲几何与庞加莱圆盘——非欧空间的视觉映射
flutter
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— parseDocxXml:正则驱动的 XML 文本提取
xml·flutter
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:三方库实战 flutter_phone_direct_caller 电话拨号详解
flutter
不爱吃糖的程序媛3 小时前
Flutter-OH 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos
松叶似针3 小时前
Flutter三方库适配OpenHarmony【doc_text】— 文件格式路由:.doc 与 .docx 的分流策略
flutter·harmonyos
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— FlutterPlugin 与 AbilityAware 双接口实现
flutter·harmonyos
LawrenceLan3 小时前
31.Flutter 零基础入门(三十一):Stack 与 Positioned —— 悬浮、角标与覆盖布局
开发语言·前端·flutter·dart
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— openLink API 与浏览器启动策略
flutter