Flutter Delaunay三角剖分插件delaunay的使用

发布于 1周前 作者 eggper 来自 Flutter

Flutter Delaunay三角剖分插件delaunay的使用

简介

delaunay 是一个用于Dart开发者的Delaunay三角剖分库。该实现改编自 Delaunator JavaScript 库。

使用方法

基本用法

以下是一个简单的使用示例:

import 'package:delaunay/delaunay.dart';

void main() {
  // 定义点集
  Float32List points = Float32List.fromList(<double>[
    143.0, 178.5,
    50.2, -100.7,
    // 添加更多点...
  ]);

  // 创建Delaunay对象
  Delaunay delaunay = Delaunay(points);
  
  // 更新三角剖分
  delaunay.update();

  // 遍历三角形
  for (int i = 0; i < delaunay.triangles.length; i += 3) {
    int a = delaunay.triangles[i];
    int b = delaunay.triangles[i + 1];
    int c = delaunay.triangles[i + 2];

    double ax = delaunay.coords[2 * a];
    double ay = delaunay.coords[2 * a + 1];
    double bx = delaunay.coords[2 * b];
    double by = delaunay.coords[2 * b + 1];
    double cx = delaunay.coords[2 * c];
    double cy = delaunay.coords[2 * c + 1];

    // 处理三角形顶点
    print('Triangle vertices: ($ax, $ay), ($bx, $by), ($cx, $cy)');
  }

  // 修改点集并重新更新三角剖分
  points[0] = 140.0;
  delaunay.update();
}

特性和Bug

请在 GitHub issue tracker 上提交功能请求和Bug报告。

示例代码

以下是一个更复杂的示例,该示例从输入图像中提取颜色,并生成一个包含随机点的Delaunay三角剖分的PNG文件。

示例代码

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// @dart = 2.12

// 这个程序创建一个PNG文件,其中包含从输入图像中提取颜色的随机点的Delaunay三角剖分。
//
// 运行 'dart delaunay_example.dart --help' 获取详细信息。

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:args/args.dart';
import 'package:delaunay/delaunay.dart';
import 'package:image/image.dart' as image;

const String description =
    'delaunay_example.dart: 一个示例程序,创建一个包含从输入图像中提取颜色的随机点的Delaunay三角剖分的PNG文件。';

Future<int> main(List<String> args) async {
  final ArgParser argParser = ArgParser()
    ..addFlag(
      'help',
      abbr: 'h',
      help: '打印帮助信息',
      negatable: false,
    )
    ..addFlag(
      'verbose',
      abbr: 'v',
      help: '详细输出',
      negatable: false,
    )
    ..addOption(
      'input',
      abbr: 'i',
      help: '从中提取三角形颜色的输入图像',
    )
    ..addOption(
      'output',
      abbr: 'o',
      help: '输出文件路径',
      defaultsTo: 'delaunay.png',
    )
    ..addOption(
      'points',
      abbr: 'p',
      help: '点的数量',
      defaultsTo: '1000',
    )
    ..addOption(
      'seed',
      abbr: 's',
      help: '随机数生成器种子',
      defaultsTo: '42',
    );
  final ArgResults argResults = argParser.parse(args);
  final Options? options = Options.fromArgResults(argResults);
  if (options == null || options.help) {
    stderr.writeln(description);
    stderr.writeln();
    stderr.writeln(argParser.usage);
    return options == null ? 1 : 0;
  }
  if (options.inputImage == null) {
    return 1;
  }
  final image.Image inputImage = options.inputImage!;

  final Random r = Random(options.seed);

  const double minX = 0.0;
  final double maxX = inputImage.width.toDouble();
  const double minY = 0.0;
  final double maxY = inputImage.height.toDouble();

  final image.Image img = image.Image(
    inputImage.width,
    inputImage.height,
  );

  final int numPoints = options.points;
  final List<Point<double>> points = <Point<double>>[];
  for (int i = 0; i < numPoints; i++) {
    points.add(Point<double>(r.nextDouble() * maxX, r.nextDouble() * maxY));
  }

  points.add(const Point<double>(minX, minY));
  points.add(const Point<double>(minX, maxY));
  points.add(const Point<double>(maxX, minY));
  points.add(const Point<double>(maxX, maxY));

  final Delaunay triangulator = Delaunay.from(points);

  final Stopwatch sw = Stopwatch()..start();
  triangulator.initialize();

  if (options.verbose) {
    print('Triangulator initialized in ${sw.elapsedMilliseconds}ms.');
  }

  sw.reset();
  sw.start();
  triangulator.processAllPoints();

  if (options.verbose) {
    print('Triangulated with ${triangulator.triangles.length ~/ 3} triangles '
        'in ${sw.elapsedMilliseconds}ms');
  }

  sw.reset();
  sw.start();

  for (int i = 0; i < triangulator.triangles.length; i += 3) {
    final Point<double> a = triangulator.getPoint(
      triangulator.triangles[i],
    );
    final Point<double> b = triangulator.getPoint(
      triangulator.triangles[i + 1],
    );
    final Point<double> c = triangulator.getPoint(
      triangulator.triangles[i + 2],
    );
    final int color = inputImage.getPixel(
      (a.x.toInt() + b.x.toInt() + c.x.toInt()) ~/ 3,
      (a.y.toInt() + b.y.toInt() + c.y.toInt()) ~/ 3,
    );
    drawTriangle(
      img,
      a.x.round(), a.y.round(),
      b.x.round(), b.y.round(),
      c.x.round(), c.y.round(),
      image.Color.fromRgb(0, 0, 0), // 黑色
      color,
    );
  }

  if (options.verbose) {
    print('Image drawn in ${sw.elapsedMilliseconds}ms.');
  }

  sw.reset();
  sw.start();
  final List<int> imageData = image.encodePng(img, level: 2);
  File(options.output).writeAsBytesSync(imageData);
  sw.stop();
  if (options.verbose) {
    print('PNG文档写入完成 in ${sw.elapsedMilliseconds}ms.');
  }

  return 0;
}

class Options {
  Options._(
    this.output,
    this.points,
    this.seed,
    this.verbose,
    this.help,
    this.inputImage,
  );

  static Options? fromArgResults(ArgResults results) {
    final bool verbose = results['verbose']!;
    final int? points = int.tryParse(results['points']!);
    if (points == null || points <= 0) {
      stderr.writeln('--points 必须是一个正整数');
      return null;
    }
    final int? seed = int.tryParse(results['seed']!);
    if (seed == null || seed <= 0) {
      stderr.writeln('--seed 必须是一个正整数');
      return null;
    }
    final bool help = results['help']!;
    if (!results.wasParsed('input') && help) {
      stderr.writeln('请使用 --input 标志提供图像。');
      return null;
    }
    return Options._(
      results['output']!,
      points,
      seed,
      verbose,
      results['help']!,
      _imageFromArgResults(results, verbose),
    );
  }

  static image.Image? _imageFromArgResults(ArgResults results, bool verbose) {
    if (!results.wasParsed('input')) {
      return null;
    }
    final String inputImagePath = results['input']!;
    image.Image inputImage;
    final File inputFile = File(inputImagePath);
    if (!inputFile.existsSync()) {
      stderr.writeln('--input 图像 "$inputImagePath" 不存在。');
      return null;
    }
    final Stopwatch sw = Stopwatch();
    sw.start();
    final List<int> imageData = inputFile.readAsBytesSync();
    if (verbose) {
      final int kb = imageData.length >> 10;
      print('Image data (${kb}KB) read in ${sw.elapsedMilliseconds}ms');
    }
    sw.reset();
    inputImage = image.decodeImage(imageData)!;
    sw.stop();
    if (verbose) {
      final int w = inputImage.width;
      final int h = inputImage.height;
      print('Image data ${w}x$h decoded in ${sw.elapsedMilliseconds}ms');
    }
    return inputImage;
  }

  final String output;
  final int points;
  final int seed;
  final bool verbose;
  final bool help;
  final image.Image? inputImage;
}

void drawTriangle(
  image.Image img,
  int ax,
  int ay,
  int bx,
  int by,
  int cx,
  int cy,
  int lineColor,
  int fillColor,
) {
  void fillBottomFlat(int x1, int y1, int x2, int y2, int x3, int y3) {
    final double slope1 = (x2 - x1).toDouble() / (y2 - y1).toDouble();
    final double slope2 = (x3 - x1).toDouble() / (y3 - y1).toDouble();

    double curx1 = x1.toDouble();
    double curx2 = curx1;

    for (int sy = y1; sy <= y2; sy++) {
      final int cx1 = curx1.toInt();
      final int cx2 = curx2.toInt();
      image.drawLine(img, cx1, sy, cx2, sy, fillColor);
      curx1 += slope1;
      curx2 += slope2;
    }
  }

  void fillTopFlat(int x1, int y1, int x2, int y2, int x3, int y3) {
    final double slope1 = (x3 - x1).toDouble() / (y3 - y1).toDouble();
    final double slope2 = (x3 - x2).toDouble() / (y3 - y2).toDouble();

    double curx1 = x3.toDouble();
    double curx2 = curx1;

    for (int sy = y3; sy > y1; sy--) {
      final int cx1 = curx1.toInt();
      final int cx2 = curx2.toInt();
      image.drawLine(img, cx1, sy, cx2, sy, fillColor);
      curx1 -= slope1;
      curx2 -= slope2;
    }
  }

  // 按y坐标升序排序点
  if (ay > cy) {
    final int tmpx = ax, tmpy = ay;
    ax = cx;
    ay = cy;
    cx = tmpx;
    cy = tmpy;
  }

  if (ay > by) {
    final int tmpx = ax, tmpy = ay;
    ax = bx;
    ay = by;
    bx = tmpx;
    by = tmpy;
  }

  if (by > cy) {
    final int tmpx = bx, tmpy = by;
    bx = cx;
    by = cy;
    cx = tmpx;
    cy = tmpy;
  }

  if (by == cy) {
    fillBottomFlat(ax, ay, bx, by, cx, cy);
  } else if (ay == by) {
    fillTopFlat(ax, ay, bx, by, cx, cy);
  } else {
    final int dy = by;
    final int dx = ax +
        (((by - ay).toDouble() / (cy - ay).toDouble()) * (cx - ax).toDouble())
            .toInt();

    fillBottomFlat(ax, ay, bx, by, dx, dy);
    fillTopFlat(bx, by, dx, dy, cx, cy);
  }
}

说明

  1. 命令行参数:

    • --help-h: 打印帮助信息。
    • --verbose-v: 详细输出。
    • --input-i: 输入图像路径。
    • --output-o: 输出文件路径,默认为 delaunay.png
    • --points-p: 随机点的数量,默认为 1000。
    • --seed-s: 随机数生成器种子,默认为 42。
  2. 主要逻辑:

    • 从输入图像中读取数据。
    • 生成指定数量的随机点。
    • 使用 Delaunay 类进行三角剖分。
    • 将三角形绘制到新图像中,并保存为PNG文件。

希望这个示例能帮助你理解和使用 delaunay 插件。如果有任何问题或需要进一步的帮助,请随时提问!


更多关于Flutter Delaunay三角剖分插件delaunay的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter Delaunay三角剖分插件delaunay的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter中使用delaunay插件进行Delaunay三角剖分的示例代码。delaunay插件用于在给定一组二维点的情况下生成Delaunay三角剖分。

首先,你需要在你的Flutter项目中添加delaunay插件。你可以通过编辑pubspec.yaml文件来添加依赖:

dependencies:
  flutter:
    sdk: flutter
  delaunay: ^最新版本号  # 请替换为实际的最新版本号

然后,运行flutter pub get来获取依赖。

接下来,我们编写一个示例Flutter应用,该应用将在屏幕上绘制一组随机点,并使用delaunay插件生成Delaunay三角剖分。

import 'package:flutter/material.dart';
import 'package:delaunay/delaunay.dart';
import 'dart:math' as math;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Delaunay Triangulation',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Delaunay Triangulation Example'),
        ),
        body: DelaunayTriangulationExample(),
      ),
    );
  }
}

class DelaunayTriangulationExample extends StatefulWidget {
  @override
  _DelaunayTriangulationExampleState createState() => _DelaunayTriangulationExampleState();
}

class _DelaunayTriangulationExampleState extends State<DelaunayTriangulationExample> {
  List<Offset> points = [];
  List<Triangle> triangles = [];

  @override
  void initState() {
    super.initState();
    generateRandomPoints();
  }

  void generateRandomPoints() {
    final random = math.Random();
    points = List.generate(20, (index) => Offset(random.nextDouble() * 300, random.nextDouble() * 300));
    triangles = delaunay(points).map((triangle) => Triangle(
      Offset(triangle.a.x, triangle.a.y),
      Offset(triangle.b.x, triangle.b.y),
      Offset(triangle.c.x, triangle.c.y)
    )).toList();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(300, 300),
      painter: DelaunayPainter(points, triangles),
    );
  }
}

class DelaunayPainter extends CustomPainter {
  final List<Offset> points;
  final List<Triangle> triangles;

  DelaunayPainter(this.points, this.triangles);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 1.0
      ..style = PaintingStyle.stroke;

    // Draw points
    points.forEach((point) {
      canvas.drawCircle(point.toPoint(), 3.0, Paint()..color = Colors.red);
    });

    // Draw triangles
    paint.style = PaintingStyle.fill;
    triangles.forEach((triangle) {
      final path = Path()
        ..moveTo(triangle.a.dx, triangle.a.dy)
        ..lineTo(triangle.b.dx, triangle.b.dy)
        ..lineTo(triangle.c.dx, triangle.c.dy)
        ..close();
      canvas.drawPath(path, Paint()..color = Colors.blue.withOpacity(0.5));
    });

    // Draw triangle edges
    paint.style = PaintingStyle.stroke;
    triangles.forEach((triangle) {
      final path = Path()
        ..moveTo(triangle.a.dx, triangle.a.dy)
        ..lineTo(triangle.b.dx, triangle.b.dy)
        ..moveTo(triangle.b.dx, triangle.b.dy)
        ..lineTo(triangle.c.dx, triangle.c.dy)
        ..moveTo(triangle.c.dx, triangle.c.dy)
        ..lineTo(triangle.a.dx, triangle.a.dy);
      canvas.drawPath(path, paint);
    });
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate != this;
  }
}

class Triangle {
  final Offset a, b, c;

  Triangle(this.a, this.b, this.c);
}

在这个示例中,我们做了以下工作:

  1. 使用delaunay插件的delaunay函数生成Delaunay三角剖分。
  2. 使用CustomPaintCustomPainter来绘制点和三角形。
  3. generateRandomPoints方法生成随机点并计算三角剖分。
  4. DelaunayPainter类负责在画布上绘制点和三角形。

这个示例代码展示了如何在Flutter中使用delaunay插件,并展示了基本的Delaunay三角剖分结果。你可以根据需要进一步调整和扩展这个示例。

回到顶部