znlgis 博客

GIS开发与技术分享

第2章:核心数据结构 - Point64、PointD

2.1 概述

在 Clipper2 中,所有几何运算的基础是点(Point)数据结构。与 Clipper1 不同,Clipper2 提供了两种点类型:

  • Point64:使用 64 位整数坐标
  • PointD:使用双精度浮点坐标

这两种类型定义在 Clipper.Core.cs 文件中,是整个库的基石。

2.2 Point64 结构

2.2.1 结构定义

public struct Point64
{
    public long X;
    public long Y;

#if USINGZ
    public long Z;
#endif
}

设计特点

  1. 值类型(struct):避免堆分配,提高性能
  2. 公共字段:直接暴露 X、Y 字段,避免属性访问开销
  3. 条件 Z 坐标:通过预处理器指令可选启用

2.2.2 构造函数

Point64 提供了多个构造函数以适应不同场景:

// 从另一个 Point64 复制
public Point64(Point64 pt)
{
    X = pt.X;
    Y = pt.Y;
#if USINGZ
    Z = pt.Z;
#endif
}

// 从 Point64 复制并缩放
public Point64(Point64 pt, double scale)
{
    X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero);
    Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero);
#if USINGZ
    Z = (long) Math.Round(pt.Z * scale, MidpointRounding.AwayFromZero);
#endif
}

// 从整数坐标创建
public Point64(long x, long y
#if USINGZ
    , long z = 0
#endif
) {
    X = x;
    Y = y;
#if USINGZ
    Z = z;
#endif
}

// 从浮点坐标创建(自动四舍五入)
public Point64(double x, double y
#if USINGZ
    , double z = 0.0
#endif
) {
    X = (long) Math.Round(x, MidpointRounding.AwayFromZero);
    Y = (long) Math.Round(y, MidpointRounding.AwayFromZero);
#if USINGZ
    Z = (long) Math.Round(z, MidpointRounding.AwayFromZero);
#endif
}

// 从 PointD 创建
public Point64(PointD pt)
{
    X = (long) Math.Round(pt.x, MidpointRounding.AwayFromZero);
    Y = (long) Math.Round(pt.y, MidpointRounding.AwayFromZero);
#if USINGZ
    Z = pt.z;
#endif
}

// 从 PointD 创建并缩放
public Point64(PointD pt, double scale)
{
    X = (long) Math.Round(pt.x * scale, MidpointRounding.AwayFromZero);
    Y = (long) Math.Round(pt.y * scale, MidpointRounding.AwayFromZero);
#if USINGZ
    Z = pt.z;
#endif
}

2.2.3 舍入策略

注意所有涉及浮点到整数转换的地方都使用了 MidpointRounding.AwayFromZero

Math.Round(x, MidpointRounding.AwayFromZero)

这种舍入方式的特点:

  • 0.5 向上舍入到 1
  • -0.5 向下舍入到 -1
  • 即”四舍五入”的传统行为

与默认的 MidpointRounding.ToEven(银行家舍入)不同,确保了一致性。

2.2.4 运算符重载

// 相等判断
public static bool operator ==(Point64 lhs, Point64 rhs)
{
    return lhs.X == rhs.X && lhs.Y == rhs.Y;
}

public static bool operator !=(Point64 lhs, Point64 rhs)
{
    return lhs.X != rhs.X || lhs.Y != rhs.Y;
}

// 加法
public static Point64 operator +(Point64 lhs, Point64 rhs)
{
    return new Point64(lhs.X + rhs.X, lhs.Y + rhs.Y
#if USINGZ
        , lhs.Z + rhs.Z
#endif
    );
}

// 减法
public static Point64 operator -(Point64 lhs, Point64 rhs)
{
    return new Point64(lhs.X - rhs.X, lhs.Y - rhs.Y
#if USINGZ
        , lhs.Z - rhs.Z
#endif
    );
}

注意:相等判断不比较 Z 坐标!这是设计决策——在 2D 裁剪操作中,只关心 X、Y 坐标的相等性。

2.2.5 ToString 方法

public readonly override string ToString()
{
    // nb: trailing space
#if USINGZ
    return $"{X},{Y},{Z} ";
#else
    return $"{X},{Y} ";
#endif
}

输出格式为 "X,Y ""X,Y,Z ",注意末尾有空格,便于路径的字符串连接。

2.2.6 Equals 和 GetHashCode

public readonly override bool Equals(object? obj)
{
    if (obj != null && obj is Point64 p)
        return this == p;
    return false;
}

public readonly override int GetHashCode()
{
    return HashCode.Combine(X, Y); // #599
}

GetHashCode 使用 HashCode.Combine 方法,这是 .NET Core 2.1+ 引入的高效哈希计算方法。注释中的 #599 是 GitHub issue 编号,表明这是一个经过社区讨论和优化的实现。

2.3 PointD 结构

2.3.1 结构定义

public struct PointD
{
    public double x;
    public double y;

#if USINGZ
    public long z;  // 注意:Z 仍然是整数
#endif
}

注意:即使在浮点点结构中,Z 坐标仍然是 long 类型。这是因为 Z 通常用于存储额外信息(如索引、标记),而非连续坐标。

2.3.2 命名风格差异

public struct Point64
{
    public long X;  // 大写
    public long Y;
}

public struct PointD
{
    public double x;  // 小写
    public double y;
}

这种命名差异可能是为了与其他库兼容或历史原因。在使用时需要注意。

2.3.3 构造函数

// 从另一个 PointD 复制
public PointD(PointD pt)
{
    x = pt.x;
    y = pt.y;
#if USINGZ
    z = pt.z;
#endif
}

// 从 Point64 创建
public PointD(Point64 pt)
{
    x = pt.X;
    y = pt.Y;
#if USINGZ
    z = pt.Z;
#endif
}

// 从 Point64 创建并缩放
public PointD(Point64 pt, double scale)
{
    x = pt.X * scale;
    y = pt.Y * scale;
#if USINGZ
    z = pt.Z;
#endif
}

// 从 PointD 创建并缩放
public PointD(PointD pt, double scale)
{
    x = pt.x * scale;
    y = pt.y * scale;
#if USINGZ
    z = pt.z;
#endif
}

// 从整数坐标创建
public PointD(long x, long y
#if USINGZ
    , long z = 0
#endif
) {
    this.x = x;
    this.y = y;
#if USINGZ
    this.z = z;
#endif
}

// 从浮点坐标创建
public PointD(double x, double y
#if USINGZ
    , long z = 0
#endif
) {
    this.x = x;
    this.y = y;
#if USINGZ
    this.z = z;
#endif
}

2.3.4 运算符重载

public static bool operator ==(PointD lhs, PointD rhs)
{
    return InternalClipper.IsAlmostZero(lhs.x - rhs.x) && 
           InternalClipper.IsAlmostZero(lhs.y - rhs.y);
}

public static bool operator !=(PointD lhs, PointD rhs)
{
    return !InternalClipper.IsAlmostZero(lhs.x - rhs.x) || 
           !InternalClipper.IsAlmostZero(lhs.y - rhs.y);
}

重要区别:PointD 的相等比较使用近似相等(IsAlmostZero),而不是精确相等。这是处理浮点数精度问题的标准做法。

internal const double floatingPointTolerance = 1E-12;

internal static bool IsAlmostZero(double value)
{
    return (Math.Abs(value) <= floatingPointTolerance);
}

容差值为 10⁻¹²,这是一个非常小的值,足以处理大多数双精度浮点数的舍入误差。

2.3.5 Negate 方法

public void Negate() 
{ 
    x = -x; 
    y = -y; 
}

这是一个原地修改方法,将点的坐标取反。用于向量反转等操作。

2.3.6 ToString 方法(带精度)

public readonly string ToString(int precision = 2)
{
#if USINGZ
    return string.Format($"},},", x, y, z);
#else
    return string.Format($"},}", x, y);
#endif
}

支持指定小数位数,默认 2 位。例如:"12.34,56.78"

2.4 点类型转换

Clipper2 提供了多种点类型转换方法:

2.4.1 静态转换方法

Clipper 静态类中:

// Point64 缩放
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point64 ScalePoint64(Point64 pt, double scale)
{
    Point64 result = new Point64()
    {
        X = (long) Math.Round(pt.X * scale, MidpointRounding.AwayFromZero),
        Y = (long) Math.Round(pt.Y * scale, MidpointRounding.AwayFromZero),
#if USINGZ
        Z = pt.Z
#endif
    };
    return result;
}

// Point64 转 PointD 并缩放
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointD ScalePointD(Point64 pt, double scale)
{
    PointD result = new PointD()
    {
        x = pt.X * scale,
        y = pt.Y * scale,
#if USINGZ
        z = pt.Z,
#endif
    };
    return result;
}

2.4.2 使用场景

// 将浮点坐标转换为整数(用于内部计算)
double scale = 1000.0;  // 保留3位小数
Point64 intPt = new Point64(floatPt, scale);

// 将整数结果转换回浮点
PointD floatResult = new PointD(intResult, 1.0 / scale);

2.5 性能考量

2.5.1 结构体 vs 类

点类型被设计为 struct 而非 class,原因:

  1. 避免堆分配:结构体存储在栈上或内联在容器中
  2. 无 GC 压力:不需要垃圾回收
  3. 复制语义:值类型按值传递,更符合点的数学性质
  4. 缓存友好:连续内存布局提高缓存命中率

2.5.2 公共字段 vs 属性

使用公共字段而非属性:

// Clipper2 的做法
public long X;
public long Y;

// 而不是
public long X { get; set; }
public long Y { get; set; }

虽然属性提供封装性,但在性能关键代码中,字段访问更快(尽管现代 JIT 通常会内联简单属性)。

2.5.3 readonly 修饰符

只读方法使用 readonly 修饰:

public readonly override string ToString()
{
    // ...
}

public readonly override bool Equals(object? obj)
{
    // ...
}

这告诉编译器方法不会修改结构体状态,允许更好的优化。

2.6 Z 坐标的应用

当启用 USINGZ 时,点包含额外的 Z 坐标:

2.6.1 用途

  1. 3D 坐标:存储真实的三维位置
  2. 索引标记:存储原始顶点索引
  3. 自定义数据:任意整数用户数据

2.6.2 Z 回调函数

public delegate void ZCallback64(Point64 bot1, Point64 top1,
    Point64 bot2, Point64 top2, ref Point64 intersectPt);

当计算交点时,可以通过回调函数决定交点的 Z 值:

clipper.ZCallback = (Point64 bot1, Point64 top1, 
    Point64 bot2, Point64 top2, ref Point64 ip) =>
{
    // 使用线性插值计算 Z 值
    double t = (ip.Y - bot1.Y) / (double)(top1.Y - bot1.Y);
    ip = new Point64(ip.X, ip.Y, 
        (long)(bot1.Z + t * (top1.Z - bot1.Z)));
};

2.6.3 内置 Z 处理

Clipper2 有默认的 Z 处理逻辑:

private void SetZ(Active e1, Active e2, ref Point64 intersectPt)
{
    if (_zCallback == null) return;

    // 优先使用主体顶点的 Z 值
    if (GetPolyType(e1) == PathType.Subject)
    {
        if (XYCoordsEqual(intersectPt, e1.bot))
            intersectPt = new Point64(intersectPt.X, intersectPt.Y, e1.bot.Z);
        else if (XYCoordsEqual(intersectPt, e1.top))
            intersectPt = new Point64(intersectPt.X, intersectPt.Y, e1.top.Z);
        // ... 更多逻辑
    }
    // ...
}

2.7 实用方法

2.7.1 中点计算

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point64 MidPoint(Point64 pt1, Point64 pt2)
{
    return new Point64((pt1.X + pt2.X) / 2, (pt1.Y + pt2.Y) / 2);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointD MidPoint(PointD pt1, PointD pt2)
{
    return new PointD((pt1.x + pt2.x) / 2, (pt1.y + pt2.y) / 2);
}

2.7.2 距离平方计算

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double DistanceSqr(Point64 pt1, Point64 pt2)
{
    return Sqr(pt1.X - pt2.X) + Sqr(pt1.Y - pt2.Y);
}

使用距离平方而非距离,避免开方运算:

// 比较两点距离时
if (DistanceSqr(pt1, pt2) < thresholdSqr)  // 避免 sqrt

2.7.3 点的近似相等

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool PointsNearEqual(PointD pt1, PointD pt2, double distanceSqrd)
{
    return Sqr(pt1.x - pt2.x) + Sqr(pt1.y - pt2.y) < distanceSqrd;
}

2.8 本章小结

本章详细分析了 Clipper2 的两种点数据结构:

  1. Point64:高精度整数点,用于核心裁剪算法
  2. PointD:浮点点,用于用户接口和输入输出

关键设计要点:

  • 使用 struct 值类型提高性能
  • 浮点比较使用容差值避免精度问题
  • 可选的 Z 坐标支持 3D 数据
  • MidpointRounding.AwayFromZero 确保一致的四舍五入
  • 大量使用 AggressiveInlining 优化性能

在下一章中,我们将学习基于这些点类型构建的路径和多边形数据结构。


上一章:概述与入门 返回目录 下一章:路径与多边形表示