2D and 3D Linear Geometry Kernel ( Geometry Kernels) CGAL 4.13 -User Manual

Introduction

CGAL, the Computational Geometry Algorithms Library, is written in C++ and consists of three major parts. The first part is the kernel, which consists of constant-size non-modifiable geometric primitive objects and operations on these objects. The objects are represented both as stand-alone classes that are parameterized by a representation class, which specifies the underlying number types used for calculations and as members of the kernel classes, which allows for more flexibility and adaptability of the kernel. The second part is a collection of basic geometric data structures and algorithms, which are parameterized by traits classes that define the interface between the data structure or algorithm and the primitives they use. In many cases, the kernel classes provided in CGAL can be used as traits classes for these data structures and algorithms. The third part of the library consists of non-geometric support facilities, such as circulators, random sources, I/O support for debugging and for interfacing CGAL to various visualization tools.

This part of the reference manual covers the kernel. The kernel contains objects of constant size, such as point, vector, direction, line, ray, segment, triangle, iso-oriented rectangle and tetrahedron. With each type comes a set of functions which can be applied to an object of this type. You will typically find access functions (e.g. to the coordinates of a point), tests of the position of a point relative to the object, a function returning the bounding box, the length, or the area of an object, and so on. The CGALkernel further contains basic operations such as affine transformations, detection and computation of intersections, and distance computations.

CGAL,Computational Geometry Algorithms Library,包括了三个主要部分。第一部分是内核(kernel),包括了固定大小的不可更改的几何元对象及针对这些对象的操作。这些对象既可以用一个(由表达类参数化的)单独的类 (stand-alone classes that are parameterized by a representation class)表示,其指定了低层用于计算的数的类型,也可以是一个内核类的成员,使内核(kernel)更多的可扩展性和适用性。第二部分是基本几何数据结构和算法集( collection of basic geometric data structures and algorithms),这个集由 trait 类参数化,trait 类定义了数据结构或算法与很多应用场景中的元语的接口, 内核(kernel)类可以作为这些数据结构和算法的 traits 类使用。第三个部分包括了非几何支持设施,包括环行器(circulators),随机源(random sources),调试的I/O支持及CGAL与可视化工具的接口。

本部分参考涵盖了核心(kernel)。核心包括固定大小的常量,如点(point)、向量(vector)、方向(direction)、直线(line)、射线(ray)、线段(segment)、三角(triangle)、面向ISO的矩形(iso-oriented rectangle)、四面体(tetrahedron)。每一种类型都有一个用于操作该类型的函数集。典型地,你可以找到数据接口函数(如一个点的坐标),测试一个点与一个对象的相对位置,一个返回包围盒(bounding box)的函数,一个对象的长度或面积等等。

GCAL内核另外还包含基本的操作,如仿射变换(affine transformation),相交的探测和计算及距离计算。

1.1 Robustness

The correctness proof of nearly all geometric algorithms presented in theory papers assumes exact computation with real numbers. This leads to a fundamental problem with the implementation of geometric algorithms. Naively, often the exact real arithmetic is replaced by inexact floating-point arithmetic in the implementation. This often leads to acceptable results for many input data. However, even for the implementation of the simplest geometric algorithms this simplification occasionally does not work. Rounding errors introduced by an inaccurate arithmetic may lead to inconsistent decisions, causing unexpected failures for some correct input data. There are many approaches to this problem, one of them is to compute exactly (compute so accurate that all decisions made by the algorithm are exact) which is possible in many cases but more expensive than standard floating-point arithmetic. C. M. Hoffmann [3][2] illustrates some of the problems arising in the implementation of geometric algorithms and discusses some approaches to solve them. A more recent overview is given in [5]. The exact computation paradigm is discussed by Yap and Dubé [6] and Yap [7].

In CGAL you can choose the underlying number types and arithmetic. You can use different types of arithmetic simultaneously and the choice can be easily changed, e.g. for testing. So you can choose between implementations with fast but occasionally inexact arithmetic and implementations guaranteeing exact computation and exact results. Of course you have to pay for the exactness in terms of execution time and storage space. See the dedicated chapter for more details on number types and their capabilities and performance.

几何算法理论论文的正确性假定实数的精确计算(exact computation)这一前提。这导致了几何算法实现的基本问题。实现中非精确算法的浮点运算经常取代了精确浮点运算。这经常导致输入数据的可接收性结果(This often leads to acceptable results for many input data)。但是,即使是最简单的几何算法实现,这种方式仍然可能出问题。舍入误差(Rounding errors)的引入会导致不一致的决策,引起正确的输入导致错误的结果。有很多解决这些问题的方法,其中之一是通过精确计算(即算法的精确性保证所有的结果的正确性),这种方法比标准的浮点算法昂贵得多。C. M. Hoffmann 举出了几何算法实现中引起的问题,讨论了解决的一些方法。文献【5】给出了最新的分析。精确计算范例由 Yap 和 Dubé [6] 和 Yap [7].进行了讨论。

CGAL可以选择底层的数据类型和算法。你可以同时使用不同的算术类型并容易改变选择(即通过测试)。所以你可以在运算速度和运算精度之间进行取舍。当然,你需要付出计算时间和存储空间的代价。请参考关于数据类型和它们的能力和性能之间如对此详细分析。

Kernel Representations

Our object of study is the d-dimensional affine Euclidean space. Here we are mainly concerned with cases d=2 and d=3. Objects in that space are sets of points. A common way to represent the points is the use of Cartesian coordinates, which assumes a reference frame (an origin and d orthogonal axes). In that framework, a point is represented by a d-tuple (c0,c1,,cd1), and so are vectors in the underlying linear space. Each point is represented uniquely by such Cartesian coordinates. Another way to represent points is by homogeneous coordinates. In that framework, a point is represented by a (d+1)-tuple (h0,h1,,hd). Via the formulae ci=hi/hd, the corresponding point with Cartesian coordinates (c0,c1,,cd1) can be computed. Note that homogeneous coordinates are not unique. For λ0, the tuples (h0,h1,,hd) and (λh0,λh1,,λhd) represent the same point. For a point with Cartesian coordinates (c0,c1,,cd1) a possible homogeneous representation is (c0,c1,,cd1,1). Homogeneous coordinates in fact allow to represent objects in a more general space, the projective space Pd. In CGAL we do not compute in projective geometry. Rather, we use homogeneous coordinates to avoid division operations, since the additional coordinate can serve as a common denominator.

我们研究的对象是d维仿射欧几里德空间(affine Euclidean space)。我们主要关注d=2和d=3时的情况,因为这个空间是点的集合。一个表达点集的通用方法是使用笛卡尔坐标系,它是一个参考框架(which reference frame)(一个原点和d维正交轴)。在这个框架下,一个点由d 维元组(c0,c1,,cd1)表示,在底层空间中是向量。每个点被这个笛卡尔坐标唯一表示。另一种表示方法是齐次坐标(homogeneous coordinates),其对一个点的表达不唯一(一个点对应多个坐标)。这个框架中一个点由(d+1)唯元组 (h0,h1,,hd)表示,通过公式ci = hi / hd 可以得到笛卡尔坐标(c0,c1,,cd1)。注意,齐次坐标不是唯一的,对于 λ0,(h0,h1,,hd)和((λh0,λh1,,λhd)表示同一个点。笛卡尔坐标(c0,c1,,cd1)可以用(c0,c1,,cd1,1)来表示。齐次坐标的更加广义的空间,使用它我们可以避免使用投影计算,即不需要进行除法运算,因为增加的一维可以作为分母。

2.1 Genericity Through Parameterization

Almost all the kernel objects (and the corresponding functions) are templates with a parameter that allows the user to choose the representation of the kernel objects. A type that is used as an argument for this parameter must fulfill certain requirements on syntax and semantics. The list of requirements defines an abstract kernel concept. For all kernel objects types, the types CGAL::Type<Kernel> and Kernel::Type are identical.

CGAL offers four families of concrete models for the concept Kernel, two based on the Cartesian representation of points and two based on the homogeneous representation of points. The interface of the kernel objects is designed such that it works well with both Cartesian and homogeneous representation. For example, points in 2D have a constructor with three arguments as well (the three homogeneous coordinates of the point). The common interfaces parameterized with a kernel class allow one to develop code independent of the chosen representation. We said "families" of models, because both families are parameterized too. A user can choose the number type used to represent the coordinates.

For reasons that will become evident later, a kernel class provides two typenames for number types, namely Kernel::FT and Kernel::RT. The type Kernel::FT must fulfill the requirements on what is called a FieldNumberType in CGAL. This roughly means that Kernel::FT is a type for which operations + and / are defined with semantics (approximately) corresponding to those of a field in a mathematical sense. Note that, strictly speaking, the built-in type int does not fulfill the requirements on a field type, since ints correspond to elements of a ring rather than a field, especially operation / is not the inverse of . The requirements on the type Kernel::RT are weaker. This type must fulfill the requirements on what is called a RingNumberType in CGAL. This roughly means that Kernel::RT is a type for which operations + are defined with semantics (approximately) corresponding to those of a ring in a mathematical sense.

几乎所有的核心对象和相应的函数都是模板,它带有一个模板参数允许用户选择核心对象的表示方式,该参数使用的类型必须满足一定语法和语义要求。一系列这种要求就定义了一个抽象内核概念。对于所有的核心对象,CGAL::Type<Kernel>和 Kernel::Type是相同的。

CGAL提供了4个关于 Kernel,的具体模型, 2个基于笛卡尔点集表示而2个基于齐次点集表示。核心对象的接口被设计为同时支持笛卡尔和齐次表达。如:2D的点有一个构造器带有3个参数(点的3个齐次坐标)。用一个核心类参数化的公共接口允许我们独立于选定的表达开发代码。我们说模型“家族”,原因是两个系列都是参数化的。用户可以选择一种数数类型用来表示坐标。

一个核心类为数字类型提供2个类型(typenames ) ,分别是Kernel::FT 和 Kernel::RT。类型Kernel::FT必须满足CGAL中FieldNumberType概念,这大致意味着Kernel::FT是一种定义了+,−,∗和/的类型,其数学语义(大致)与所关联的field的相关运算相同。需要注意的是:内置类型int不满足field类型的要求,因为int对应的是环(ring)而非域(field),特别/运算不是*运算的逆。Kernel::RT的类型要求则相对较弱,它必须满足CGAL中的RingNumberType概念,这大致意味着Kernel::RT是一种定义了+,−,的类型,其数学语义(大致)与所关联的ring的相关运算相同。

2.2 Cartesian Kernels

With Cartesian<FieldNumberType> you can choose a Cartesian representation of coordinates. When you choose Cartesian representation you have to declare at the same time the type of the coordinates. A number type used with the Cartesian representation class should be a FieldNumberType as described above. As mentioned above, the built-in type int is not a FieldNumberType. However, for some computations with Cartesian representation, no division operation is needed, i.e., a RingNumberType is sufficient in this case. With Cartesian<FieldNumberType>, both Cartesian<FieldNumberType>::FT and Cartesian<FieldNumberType>::RT are mapped to FieldNumberType.

Cartesian<FieldNumberType> uses reference counting internally to save copying costs. CGAL also provides Simple_cartesian<FieldNumberType>, a kernel that uses Cartesian representation but no reference counting. Debugging is easier with Simple_cartesian<FieldNumberType>, since the coordinates are stored within the class and hence direct access to the coordinates is possible. Depending on the algorithm, it can also be slightly more or less efficient than Cartesian<FieldNumberType>. Again, in Simple_cartesian<FieldNumberType> both Simple_cartesian<FieldNumberType>::FT and Simple_cartesian<FieldNumberType>::RT are mapped to FieldNumberType.

使用 Cartesian<FieldNumberType>类,你可以选择坐标的笛卡尔表示方式( Cartesian representation)。当你选择笛卡尔表示方式( Cartesian representation)你需要同时声明坐标的类型。与笛卡尔表达类共用的数字类型应当是一个域数字类型( FieldNumberType ),如上所说,内置类型int不是域数字类型。对于有些笛卡尔表示,不需要除法运算时,则环类型( RingNumberType )就足够了。对于Cartesian<FieldNumberType>,两个类Cartesian<FieldNumberType>::FT (域trait)和 Cartesian<FieldNumberType>::RT (环trait)都映射到域类型( FieldNumberType.)。

Cartesian<FieldNumberType> 使用内部引用 记数来节省拷贝开销。CGAL也提供了 Simple_cartesian<FieldNumberType>,它是一个使用笛卡尔表达的核心(kernel )但没有引用记数。使用 Simple_cartesian<FieldNumberType>时,调试会较为容易,因为坐标是直接保存在类中可以直接存取。依赖这些算法,它会比Cartesian<FieldNumberType>类多多少少效率高一些。另外,在 Simple_cartesian<FieldNumberType>中,两个类Simple_cartesian<FieldNumberType>::FT (域trait和 Simple_cartesian<FieldNumberType>::RT(环trait都映射到域类型( FieldNumberType.)。

2.3 Homogeneous Kernels

Homogeneous coordinates permit to avoid division operations in numerical computations, since the additional coordinate can serve as a common denominator. Avoiding divisions can be useful for exact geometric computation. With Homogeneous<RingNumberType> you can choose a homogeneous representation for the coordinates of the kernel objects. As for the Cartesian representation, one has to declare the type used to store the coordinates. Since the homogeneous representation does not use divisions, the number type associated with a homogeneous representation class must be a model for the weaker concept RingNumberType only. However, some operations provided by this kernel involve divisions, for example computing squared distances or Cartesian coordinates. To keep the requirements on the number type parameter of Homogeneous low, the number type Quotient<RingNumberType> is used for operations that require divisions. This number type can be viewed as an adaptor which turns a RingNumberType into a FieldNumberType. It maintains numbers as quotients, i.e., a numerator and a denominator. With Homogeneous<RingNumberType>Homogeneous<RingNumberType>::FT is equal to Quotient<RingNumberType>, while Homogeneous<RingNumberType>::RT is equal to RingNumberType.

Homogeneous<RingNumberType> uses reference counting internally to save copying costs. CGAL also provides Simple_homogeneous<RingNumberType>, a kernel that uses homogeneous representation but no reference counting. Debugging is easier with Simple_homogeneous<RingNumberType>, since the coordinates are stored within the class and hence direct access to the coordinates is possible. Depending on the algorithm, it can also be slightly more or less efficient than Homogeneous<RingNumberType>. Again, in Simple_homogeneous<RingNumberType> the type Simple_homogeneous<RingNumberType>::FT is equal to Quotient<RingNumberType> while Simple_homogeneous<RingNumberType>::RT is equal to RingNumberType.

齐次坐标允许在数的计算中避开除法,因为附加的坐标项可以作为公共的分母。避开除法对于几何精确计算十分有用。使用Homogeneous<RingNumberType>,你可以为内核对象(kernel objects)选择一种齐次表达。对于笛卡尔坐标表示,我们需要声明一个保存坐标的类型。因为齐次表示不需要用除法,所以使用的数类型必须是 RingNumberType的弱类型概念的模型。但这个核心有些操作用到了除法,如计算平方距离(squared distances)或笛卡尔坐标时。为了保持齐次的参数数字类型的低要求( To keep the requirements on the number type parameter of Homogeneous low),类型Quotient<RingNumberType>被用于需要除法的操作。这个数类型可以认为是一个由 RingNumberType向 FieldNumberType转换的适配器。它把数据维护为quotients形式,即一个分子和一个分母。在Homogeneous<RingNumberType>中,Homogeneous<RingNumberType>::FT 等于Quotient<RingNumberType>,同时Homogeneous<RingNumberType>::RT等于 RingNumberType

Homogeneous<RingNumberType> 使用内部的引用记数来节省拷贝开销。CGAL也提供了 Simple_homogeneous<RingNumberType>类,它是一个使用齐次表达的核心(kernel )但没有引用记数。使用  Simple_homogeneous<RingNumberType>时,调试会较为容易,因为坐标是直接保存在类中可以直接存取。依赖这些算法,它会比Homogeneous<RingNumberType>类多多少少效率高一些。另外,在  Simple_homogeneous<RingNumberType>中,两个类Simple_homogeneous<RingNumberType>::FT域trait等于 Quotient<RingNumberType>, Simple_homogeneous<RingNumberType>::RT(环trait等于 FieldNumberType

2.4 Naming Conventions

The use of kernel classes not only avoids problems, it also makes all CGAL classes very uniform. They always consist of:

使用kernel类不仅避免问题,也使用所有CGAL类非常统一。它们总是由下列组成:

  1. The capitalized base name of the geometric object, such as PointSegment, or Triangle.

     大写的几何对象的基名字,如PointSegment, 或 Triangle.

  2. An underscore followed by the dimension of the object, for example _2_3, or _d.

     引导维数的下划线,如_2, _3, 或 _d。

  3. kernel class as parameter, which itself is parameterized with a number type, such as Cartesian<double> or Homogeneous<leda_integer>.

    作为参数的类,这个类本身是由数的类型参数化的,如 Cartesian<double> 或 Homogeneous<leda_integer>

2.5 Kernel as a Traits Class

Algorithms and data structures in the basic library of CGAL are parameterized by a traits class that subsumes the objects on which the algorithm or data structure operates as well as the operations to do so. For most of the algorithms and data structures in the basic library you can use a kernel as a traits class. For some algorithms you even do not have to specify the kernel; it is detected automatically using the types of the geometric objects passed to the algorithm. In some other cases, the algorithms or data structures needs more than is provided by the kernel concept. In these cases, a kernel can not be used as a traits class.

CGAL的基本库中的算法和数据结构由一个traits类参数化(parameterized),这个traits包含了这些算法或数据结构操作的对象及这样做的操作。对于大多数基本库的的算法和数据结构,你能够将kernel作为一个traits类。对于 一些算法你甚至不需要指定kenel;它被使用传入算法的几何对象类型自动识别。其他情况下,算法或数据结构需要的东西比kernel概念提供的东西要多,所以这种情况下一个kernel不能用作traits类。

2.6 Choosing a Kernel and Predefined Kernels

If you start with integral Cartesian coordinates, many geometric computations will involve integral numerical values only. Especially, this is true for geometric computations that evaluate only predicates, which are tantamount to determinant computations. Examples are triangulation of point sets and convex hull computation. In this case, the Cartesian representation is probably the first choice, even with a ring type. You might use limited precision integer types like int or long, use double to present your integers (they have more bits in their mantissa than an int and overflow nicely), or an arbitrary precision integer type like the wrapper Gmpz for the GMP integers, leda_integer, or MP_Float. Note, that unless you use an arbitrary precision ring type, incorrect results might arise due to overflow.

If new points are to be constructed, for example the intersection point of two lines, computation of Cartesian coordinates usually involves divisions. Hence, one needs to use a FieldNumberType with Cartesian representation, or alternatively, switch to homogeneous representation. The type double is a - though imprecise - model for FieldNumberType. You can also put any RingNumberType into the Quotient adaptor to get a field type which then can be put into Cartesian. But using homogeneous representation on the RingNumberType is usually the better option. Other valid FieldNumberTypes are leda_rational and leda_real.

If it is crucial for you that the computation is reliable, the right choice is probably a number type that guarantees exact computation. The Filtered_kernel provides a way to apply filtering techniques [1] to achieve a kernel with exact and efficient predicates. Still other people will prefer the built-in type double, because they need speed and can live with approximate results, or even algorithms that, from time to time, crash or compute incorrect results due to accumulated rounding errors.

如果初始的笛卡尔坐标是整数(integral ),很多几何计算只涉及整数值。特别地,对于只计算判定(predicate)的几何计算是这样的,这与行列式(determinant )计算是相等的。例如:三角形点集和凸包等的计算。这种情况下笛卡尔表达可能是第一选择,即使用环类型(ring type)。你可能会用有限精度的类型如int 或 long,使用double来表示你的整数(这是因为double的尾数具有较多的位且溢出精确)或一个任意精度的整数类型如GMP整数的包装类型Gmpz,leda整数或MP_Float。注意,除非你使用一个任意精度的环类型,否则可能引起溢出错误。

如果新的点需要创建,如两条直线的交点,笛卡尔坐标通常涉及除法。所以,需要在笛卡尔表示中使用FieldNumberType或转换为齐次表达。double类型是一个(不精确的) FieldNumberType模型。你也可以将任何 RingNumberType类型经由 Quotient适配器来得到一个可以用于Cartesian的类型。但是使用基于 RingNumberType 的齐次表达通常是更好的选项。其他的 RingNumberType 包括leda_rationalleda_real

可靠的运算是十分关键的,正确的选择是使用可保证精确计算的数类型。Filtered_kernel 类提供了一个应用过滤技术的方法来达到精确和高效判定(predicate)的kernel。仍然有人喜欢使用内置的double类型,因为计算速度并且可以允许不精确的数据,即使算法通过一次次的崩溃或积累舍入误差导致的不准确。

2.6.1 Predefined Kernels

For the user's convenience, CGAL provides 3 typedefs to generally useful kernels.

CGAL为有用的kernel提供了3个typedef。

Kernel Geometry

3.1 Points and Vectors

In CGAL we strictly distinguish between points, vectors and directions. A point is a point in the Euclidean space Ed, a vector is the difference of two points p2p1 and denotes the direction and the distance from p1 to p2 in the vector space Rd, and a direction is a vector where we forget about its length. They are different mathematical concepts. For example, they behave different under affine transformations and an addition of two points is meaningless in affine geometry. By putting them in different classes we not only get cleaner code, but also type checking by the compiler which avoids ambiguous expressions. Hence, it pays twice to make this distinction.

CGAL defines a symbolic constant ORIGIN of type Origin which denotes the point at the origin. This constant is used in the conversion between points and vectors. Subtracting it from a point p results in the locus vector of p.

Cartesian<double>::Point_2 p(1.0, 1.0), q;
Cartesian<double>::Vector_2 v;
v = p - ORIGIN;
q = ORIGIN + v;
assert( p == q );

In order to obtain the point corresponding to a vector v you simply have to add v to ORIGIN. If you want to determine the point q in the middle between two points p1 and p2, you can write[2]

q = p_1 + (p_2 - p_1) / 2.0;

Note that these constructions do not involve any performance overhead for the conversion with the currently available representation classes.

CGAL中,严格区分了 point, vector 和 direction. 一个点是欧几里德空间Ed中的一个点,一个向量是向量空间Rd两个点p2p1的差(difference),并指出从p1到p2的距离,另外如果不考虑其长度一个向量表示一个方向(direction)。它们是不同的数学概念。如:它们在仿射变换中的行为是不同的,且在仿射几何中两个点相加是无意义的。将它们放入不同的类中,我们不仅能得到较清晰的代码,也能够得到编译器的类型检查以避免模糊的表达。Hence, it pays twice to make this distinction.

CGAL定义了一个代表类型 Origin常量符号ORIGIN,这个符号指示初始的点。这个常量用于在点和向量之间进行转换。从一个点p中减去它将得到一个p的locus vector(轨迹向量????)。

Cartesian<double>::Point_2 p(1.0, 1.0), q;
Cartesian<double>::Vector_2 v;
v = p - ORIGIN;
q = ORIGIN + v;
assert( p == q );
为了得到向量v对应的点,你只需要简单地将v加上ORIGIN。如果你想得到的p2p1中点,你可以这样写:
q = p_1 + (p_2 - p_1) / 2.0;
注意:这些构造不涉及当前表示类转换时的任何性能开销。

3.2 Kernel Objects

Besides points (Kernel::Point_2Kernel::Point_3), vectors (Kernel::Vector_2Kernel::Vector_3), and directions (Kernel::Direction_2Kernel::Direction_3), CGAL provides lines, rays, segments, planes, triangles, tetrahedra, iso-rectangles, iso-cuboids, circles and spheres.

Lines (Kernel::Line_2Kernel::Line_3) in CGAL are oriented. In two-dimensional space, they induce a partition of the plane into a positive side and a negative side. Any two points on a line induce an orientation of this line. A ray (Kernel::Ray_2Kernel::Ray_3) is semi-infinite interval on a line, and this line is oriented from the finite endpoint of this interval towards any other point in this interval. A segment (Kernel::Segment_2Kernel::Segment_3) is a bounded interval on a directed line, and the endpoints are ordered so that they induce the same direction as that of the line.

Planes are affine subspaces of dimension two in E3, passing through three points, or a point and a line, ray, or segment. CGAL provides a correspondence between any plane in the ambient space E3 and the embedding of E2 in that space. Just like lines, planes are oriented and partition space into a positive side and a negative side. In CGAL, there are no special classes for half-spaces. Half-spaces in 2D and 3D are supposed to be represented by oriented lines and planes, respectively.

Concerning polygons and polyhedra, the kernel provides triangles, iso-oriented rectangles, iso-oriented cuboids and tetrahedra. More complex polygons[3] and polyhedra or polyhedral surfaces can be obtained from the basic library (Polygon_2Polyhedron_3), so they are not part of the kernel. As with any Jordan curves, triangles, iso-oriented rectangles and circles separate the plane into two regions, one bounded and one unbounded.

除点(Kernel::Point_2Kernel::Point_3),向量(Kernel::Vector_2Kernel::Vector_3),和方向 (Kernel::Direction_2Kernel::Direction_3),CGAL提供了直线、射线、线段、平面、三角形、四面体(tetrahedra)、iso-矩形、iso-立方体(iso-cuboids)、圆和球体等。

CGAL中的直线(Kernel::Line_2Kernel::Line_3)是有方向性的。在2维空间,它将一个平面分为正半边和负半边。直线上的任何两个点指出了该直线的方向性。一个射线(Kernel::Ray_2Kernel::Ray_3)是一个直线的半无限区间,它的方向是从射线的端点指向无限区间的任何一个点。线段(Kernel::Segment_2Kernel::Segment_3)是一个有向直线上的有界区间( bounded interval),其端点顺序与直线的方向一致。

平面是欧几里德三维空间E3仿射子空间,通过给定3个点,1个点和1条直线、射线、线段。CGAL提供了3维空间E3中任何平面与E2空间之间的关联。正如直线,平面是有向的并将空间分为正半边和负半边。在CGAL中,没有半空间相对应的类。在2D和3D空间中,半空间由直线和平面分别表示半空间。

至于多边形和多面体(polyhedra),核心(kernel)提供了三角形,面向iso的矩形,面向iso的立方体和四面体。更复杂的多边形和多面体或多面体面能够通过基本库中的(Polygon_2Polyhedron_3,所以它们不是内核的组成部分。

而任何Jordan曲线(即:平面简单闭合曲线),三角形,面向iso的矩形和圆将一个平面分割为两个区域(region),一个有限区域和一个无限区域。

3.3 Orientation and Relative Position

Geometric objects in CGAL have member functions that test the position of a point relative to the object. Full dimensional objects and their boundaries are represented by the same type, e.g. half-spaces and hyperplanes are not distinguished, neither are balls and spheres and discs and circles. Such objects split the ambient space into two full-dimensional parts, a bounded part and an unbounded part (e.g. circles), or two unbounded parts (e.g. hyperplanes). By default these objects are oriented, i.e., one of the resulting parts is called the positive side, the other one is called the negative side. Both of these may be unbounded.

These objects have a member function oriented_side() that determines whether a test point is on the positive side, the negative side, or on the oriented boundary. These function returns a value of type Oriented_side.

Those objects that split the space in a bounded and an unbounded part, have a member function bounded_side() with return type Bounded_side.

If an object is lower dimensional, e.g. a triangle in three-dimensional space or a segment in two-dimensional space, there is only a test whether a point belongs to the object or not. This member function, which takes a point as an argument and returns a Boolean value, is called has_on().

CGAL中的几何对象有一个成员函数用于测试一个点相对于该对象的位置。全维对象和和它们的边界由相同类型表示,也即半空间(half-space)和超平面(hyperplane, 超平面是n维欧氏空间中余维度等于一的线性子空间,也就是必须是(n-1)维度)不做区分,不论是球体、碟或圆。这些对象将空间分为两个全维的部分,一个无限大小另一个有限大小(如圆)或两个无限部分(如超平面)。缺省下,这些对象是有向的,即一个部分是正的,另一个部分是负的。两个部分可能都是无限的。

这些对象有一个成员函数oriented_side(),它确定测试点是否在正的一侧、负的一侧或在边界上。这个函数返回一个类型为Oriented_side的值。

将空间分为一个有限区域和一个无限区域的对象有一个成员函数bounded_side(),它返回一个Bounded_side类型值 。

如果一个对象是较低维的,如一个三维的三角形或一个二维的线段,则只有一个测试函数has_on()确定一个点是否属于这个对象。这个成员函数以一个点作为参数并返回一个Boolean值。

Predicates and Constructions 

4.1 Predicates

Predicates are at the heart of a geometry kernel. They are basic units for the composition of geometric algorithms and encapsulate decisions. Hence their correctness is crucial for the control flow and hence for the correctness of an implementation of a geometric algorithm. CGAL uses the term predicate in a generalized sense. Not only components returning a Boolean value are called predicates but also components returning an enumeration type like a Comparison_result or an Orientation. We say components, because predicates are implemented both as functions and function objects (provided by a kernel class).

CGAL provides predicates for the orientation of point sets (orientation()left_turn()right_turn()collinear()coplanar()), for comparing points according to some given order, especially for comparing Cartesian coordinates (e.g. lexicographically_xy_smaller()), in-circle and in-sphere tests, and predicates to compare distances.

判定(Predicates )是几何内核的核心部分。它们是几何算法和封装决定构成的基本单元。由此它们的正确性对于控制流和几何算法实现的正确性十分关键。CGAL基于归纳的思想使用Predicate术语。判定(Predicates )不仅包含返回Boolean值的成员组件(),也包括返回一个枚举值(如Comparison_result )或一个方向(Orientation)的组件成员。我们说的组件成员,原因是判定的实现是函数或函数对象(由kernel类提供)。

CGAL提供了对一组点集的方向(orientation )的判定(orientation()left_turn()right_turn()collinear()coplanar()),提供了按照某个给定方向来比较点的判定,特别是提供了比较笛卡尔坐标(lexicographically_xy_smaller())、in-circle 和 in-sphere 的判定,以及比较距离的判定。

4.2 Constructions

Functions and function objects that generate objects that are neither of type bool nor enum types are called constructions. Constructions involve computation of new numerical values and may be imprecise due to rounding errors unless a kernel with an exact number type is used.

Affine transformations (Kernel::Aff_transformation_2Kernel::Aff_transformation_3) allow to generate new object instances under arbitrary affine transformations. These transformations include translations, rotations (in 2D only) and scaling. Most of the geometric objects in a kernel have a member function transform(Aff_transformation t) which applies the transformation to the object instance.

CGAL also provides a set of functions that detect or compute the intersection between objects of the 2D kernel, and many objects in the 3D kernel, and functions to calculate their squared distance. Moreover, some member functions of kernel objects are constructions.

So there are routines that compute the square of the Euclidean distance, but no routines that compute the distance itself. Why? First of all, the two values can be derived from each other quite easily (by taking the square root or taking the square). So, supplying only the one and not the other is only a minor inconvenience for the user. Second, often either value can be used. This is for example the case when (squared) distances are compared. Third, the library wants to stimulate the use of the squared distance instead of the distance. The squared distance can be computed in more cases and the computation is cheaper. We do this by not providing the perhaps more natural routine, The problem of a distance routine is that it needs the sqrt operation. This has two drawbacks:

  • The sqrt operation can be costly. Even if it is not very costly for a specific number type and platform, avoiding it is always cheaper.
  • There are number types on which no sqrt operation is defined, especially integer types and rationals.

生成非Boolean对象和非枚举对象的函数或函数对象称为构造(construction)。构造涉及到计算新的数字值和可能的舍入导致的不精确(除非使用一个精确的类型的核心)。

仿射变换(Affine transformations)(Kernel::Aff_transformation_2Kernel::Aff_transformation_3)允许任意仿射变换生成新的对象实例。这些变换包括位置变换、旋转(只在2D中)和缩放。内核的大部分几何对象有一个成员函数(transform(Aff_transformation t)),它对对象实例进行变换操作。

CGAL也提供一个函数集用来探测和计算2D kernel对象之间和很多3D kernel对象的交集,以及计算它们之间平方距离(squared distance)的函数。另外kernel对象的一些成员函数是构造(construction)。

4.3 Intersections and Variant Return Types

Some functions, for example intersection(), can return different types of objects. To achieve this in a type-safe way CGAL uses return values of type boost::optional< boost::variant< T... > > were T... is a list of all possible resulting geometric objects. The exact result type of an intersection can be determined through the metafunction cpp11::result_of<Kernel::Intersect_2(Type1, Type2)> or cpp11::result_of<Kernel::Intersect_3(Type1, Type2)>, where Type1 and Type2 are the types of the objects used in the intersection computation.

有些函数,如intersection(),能够返回不同的对象类型。为类型安全并实现函数,CGAL用返回类型 boost::optional< boost::variant< T... > >,假定 T...是所有可能的结果几何对象的列表。交集(Intersection)确切的返回类型可以由元函数(metafunction)cpp11::result_of<Kernel::Intersect_2(Type1, Type2)> or cpp11::result_of<Kernel::Intersect_3(Type1, Type2)>来确定,这里 Type1 和 Type2 是交集运算中使用的对象的类型。

4.4 Example

In the following example, result_of is used to query the type of the return value for the intersection computation:

下面的例子中,result_of用于查询交集运算返回值的类型。

typedef Cartesian<double> K;
typedef K::Point_2 Point_2;
typedef K::Segment_2 Segment_2;
 
Segment_2 segment_1, segment_2;
 
std::cin >> segment_1 >> segment_2;
 
 
/* C++11 */
// auto v = intersection(segment_1, segment_2);
 
/* C++03 */
v = intersection(segment_1, segment_2);
if(v) {
/* not empty */
if (const Point_2 *p = boost::get<Point_2>(&*v) ) {
/* do something with *p */
} else {
const Segment_2 *s = boost::get<Segment_2>(&*v);
/* do something with *s */
}
} else {
/* empty intersection */
}

4.5 Constructive Predicates

For testing where a point p lies with respect to a plane defined by three points qr and s, one may be tempted to construct the plane Kernel::Plane_3(q,r,s) and use the method oriented_side(p). This may pay off if many tests with respect to the plane are made. Nevertheless, unless the number type is exact, the constructed plane is only approximated, and round-off errors may lead oriented_side(p) to return an orientation which is different from the real orientation of pqr, and s.

In CGAL, we provide predicates in which such geometric decisions are made directly with a reference to the input points pqrs, without an intermediary object like a plane. For the above test, the recommended way to get the result is to use orientation(p,q,r,s). For exact number types, the situation is different. If several tests are to be made with the same plane, it pays off to construct the plane and to use oriented_side(p).

为了测试一个点p相对于三个点q,r和s的平面的位置,我们可能会构造一个平面Kernel::Plane_3(q,r,s)并使用oriented_side(p)函数。该函数可能成功,如果一些关于该平面的测试被完成。但是,除非使用精确类型,否则创建的平面是不精确的,舍入误差可导致oriented_side(p)返回一个与真实的p, q和r不同的方向。对于精确类型,情况则不同。通过几个测试,它会成功地创建该平面并调用oriented_side(p)。

Extensible Kernel

This manual section describe how users can plug user defined geometric classes in existing CGAL kernels. This is best illustrated by an example.

手册中描述了用户可以现有CGAL内核中插入自己定义的几何类。见下面的例子。

5.1 Introduction

CGAL defines the concept of a geometry kernel. Such a kernel provides types, construction objects and generalized predicates. Most implementations of Computational Geometry algorithms and data structures in the basic library of CGAL were done in a way that classes or functions can be parametrized with a geometric traits class.

In most cases this geometric traits class must be a model of the CGAL geometry kernel concept (but there are some exceptions).

CGAL定义了一个几何内核概念。这一概念提供了类型集、对象构造和归纳的判定集。大多CGAL基本库中的计算几何算法和数据结构可通过参数化使用几何 traits 类。

5.2 An Extensive Example

Assume we have the following point class, where the coordinates are stored in an array of doubles, where we have another data member color, which shows up in the constructor.

假定我们有下面的点类,在构造器中显示其坐标都保存在一个double数组中,另一个成员是color。


File Kernel_23/MyPointC2.h

#ifndef MY_POINTC2_H
#define MY_POINTC2_H
 
 
#include <CGAL/Origin.h>
#include <CGAL/Bbox_2.h>
 
 
class MyPointC2 {
 
private:
double vec[2];
int col;
 
public:
 
MyPointC2()
: col(0)
{
*vec = 0;
*(vec+1) = 0;
}
 
 
MyPointC2(const double x, const double y, int c = 0)
: col(c)
{
*vec = x;
*(vec+1) = y;
}
 
const double& x() const { return *vec; }
 
const double& y() const { return *(vec+1); }
 
double & x() { return *vec; }
 
double& y() { return *(vec+1); }
 
int color() const { return col; }
 
int& color() { return col; }
 
 
bool operator==(const MyPointC2 &p) const
{
return ( *vec == *(p.vec) ) && ( *(vec+1) == *(p.vec + 1) && ( col == p.col) );
}
 
bool operator!=(const MyPointC2 &p) const
{
return !(*this == p);
}
 
};
 
#endif // MY_POINTC2_H

As said earlier the class is pretty minimalistic, for example it has no bbox() method. One might assume that a basic library algorithm which computes a bounding box (e.g, to compute the bounding box of a polygon), will not compile. Luckily it will, because it does not use of member functions of geometric objects, but it makes use of the functor Kernel::Construct_bbox_2.

To make the right thing happen with MyPointC2 we have to provide the following functor.

为了简化,我们没有定义bbox()方法。我们可能认为一个计算一个多边形的边界盒子的基本库算法将不会被编译,但幸运的是,它去被编译,因为它不使用几何对象的成员函数,但它使用了仿函数(functor)Kernel::Construct_bbox_2


File Kernel_23/MyConstruct_bbox_2.h

#ifndef MYCONSTRUCT_BBOX_2_H
#define MYCONSTRUCT_BBOX_2_H
 
 
template <class ConstructBbox_2>
class MyConstruct_bbox_2 : public ConstructBbox_2 {
public:
using ConstructBbox_2::operator();
 
CGAL::Bbox_2 operator()(const MyPointC2& p) const {
return CGAL::Bbox_2(p.x(), p.y(), p.x(), p.y());
}
};
 
#endif //MYCONSTRUCT_BBOX_2_H

Things are similar for random access to the Cartesian coordinates of a point. As the coordinates are stored in an array of doubles we can use double* as random access iterator.

与随机存取笛卡尔坐标相似,我们将坐标存入一个double数组,我们可以使用double*来随机存取。
File Kernel_23/MyConstruct_coord_iterator.h

#ifndef MYCONSTRUCT_COORD_ITERATOR_H
#define MYCONSTRUCT_COORD_ITERATOR_H
 
class MyConstruct_coord_iterator {
public:
const double* operator()(const MyPointC2& p)
{
return &p.x();
}
 
const double* operator()(const MyPointC2& p, int)
{
const double* pyptr = &p.y();
pyptr++;
return pyptr;
}
};
 
#endif //MYCONSTRUCT_COORD_ITERATOR_H

The last functor we have to provide is the one which constructs points. That is you are not forced to add the constructor with the Origin as parameter to your class, nor the constructor with homogeneous coordinates. The functor is a kind of glue layer between the CGAL algorithms and your class.


File Kernel_23/MyConstruct_point_2.h

#ifndef MYCONSTRUCT_POINT_2_H
#define MYCONSTRUCT_POINT_2_H
 
template <typename K, typename OldK>
class MyConstruct_point_2
{
typedef typename K::RT RT;
typedef typename K::Point_2 Point_2;
typedef typename K::Line_2 Line_2;
typedef typename Point_2::Rep Rep;
public:
typedef Point_2 result_type;
 
// Note : the CGAL::Return_base_tag is really internal CGAL stuff.
// Unfortunately it is needed for optimizing away copy-constructions,
// due to current lack of delegating constructors in the C++ standard.
Rep // Point_2
operator()(CGAL::Return_base_tag, CGAL::Origin o) const
{ return Rep(o); }
 
Rep // Point_2
operator()(CGAL::Return_base_tag, const RT& x, const RT& y) const
{ return Rep(x, y); }
 
Rep // Point_2
operator()(CGAL::Return_base_tag, const RT& x, const RT& y, const RT& w) const
{ return Rep(x, y, w); }
 
Point_2
operator()(const CGAL::Origin&) const
{ return MyPointC2(0, 0, 0); }
 
Point_2
operator()(const RT& x, const RT& y) const
{
return MyPointC2(x, y, 0);
}
 
const Point_2&
operator()(const Point_2 & p) const
{
return p;
}
 
Point_2
operator()(const Line_2& l) const
{
typename OldK::Construct_point_2 base_operator;
Point_2 p = base_operator(l);
return p;
}
 
Point_2
operator()(const Line_2& l, int i) const
{
typename OldK::Construct_point_2 base_operator;
return base_operator(l, i);
}
 
// We need this one, as such a functor is in the Filtered_kernel
Point_2
operator()(const RT& x, const RT& y, const RT& w) const
{
if(w != 1){
return MyPointC2(x/w, y/w, 0);
} else {
return MyPointC2(x,y, 0);
}
}
};
 
#endif //MYCONSTRUCT_POINT_2_H

Now we are ready to put the puzzle together. We won't explain it in detail, but you see that there are typedefs to the new point class and the functors. All the other types are inherited.


File Kernel_23/MyKernel.h

#ifndef MYKERNEL_H
#define MYKERNEL_H
 
#include <CGAL/Cartesian.h>
#include "MyPointC2.h"
#include "MySegmentC2.h"
#include "MyConstruct_bbox_2.h"
#include "MyConstruct_coord_iterator.h"
#include "MyConstruct_point_2.h"
 
// K_ is the new kernel, and K_Base is the old kernel
template < typename K_, typename K_Base >
class MyCartesian_base
: public K_Base::template Base<K_>::Type
{
typedef typename K_Base::template Base<K_>::Type OldK;
public:
typedef K_ Kernel;
typedef MyPointC2 Point_2;
typedef MySegmentC2<Kernel> Segment_2;
typedef MyConstruct_point_2<Kernel, OldK> Construct_point_2;
typedef const double* Cartesian_const_iterator_2;
typedef MyConstruct_coord_iterator Construct_cartesian_const_iterator_2;
typedef MyConstruct_bbox_2<typename OldK::Construct_bbox_2>
Construct_bbox_2;
 
Construct_point_2
construct_point_2_object() const
{ return Construct_point_2(); }
 
Construct_bbox_2
construct_bbox_2_object() const
{ return Construct_bbox_2(); }
 
Construct_cartesian_const_iterator_2
construct_cartesian_const_iterator_2_object() const
{ return Construct_cartesian_const_iterator_2(); }
 
template < typename Kernel2 >
struct Base { typedef MyCartesian_base<Kernel2, K_Base> Type; };
};
 
 
template < typename FT_ >
struct MyKernel
: public CGAL::Type_equality_wrapper<
MyCartesian_base<MyKernel<FT_>, CGAL::Cartesian<FT_> >,
MyKernel<FT_> >
{};
 
#endif // MYKERNEL_H

Finally, we give an example how this new kernel can be used. Predicates and constructions work with the new point, they can be a used to construct segments and triangles with, and data structures from the Basic Library, as the Delaunay triangulation work with them.

The kernel itself can be made robust by plugging it in the Filtered_kernel.


File Kernel_23/MyKernel.cpp

#include <CGAL/basic.h>
#include <CGAL/Filtered_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <CGAL/squared_distance_2.h>
#include <cassert>
#include "MyKernel.h"
#include "MyPointC2_iostream.h"
 
typedef MyKernel<double> MK;
typedef CGAL::Delaunay_triangulation_2<K> Delaunay_triangulation_2;
 
 
typedef K::Point_2 Point;
typedef K::Segment_2 Segment;
typedef K::Ray_2 Ray;
typedef K::Line_2 Line;
typedef K::Triangle_2 Triangle;
typedef K::Iso_rectangle_2 Iso_rectangle;
 
const int RED= 1;
const int BLACK=2;
 
int main()
{
Point a(0,0), b(1,0), c(1,1), d(0,1);
a.color()=RED;
b.color()=BLACK;
d.color()=RED;
 
Delaunay_triangulation_2 dt;
dt.insert(a);
 
K::Orientation_2 orientation;
orientation(a,b,c);
 
Point p(1,2), q;
p.color() = RED;
q.color() = BLACK;
std::cout << p << std::endl;
 
K::Compute_squared_distance_2 squared_distance;
 
std::cout << "squared_distance(a, b) == "
<< squared_distance(a, b) << std::endl;
 
Segment s1(p,q), s2(a, c);
 
K::Construct_midpoint_2 construct_midpoint_2;
 
Point mp = construct_midpoint_2(p,q);
 
std::cout << "midpoint(" << p << " , " << q << ") == " << mp << std::endl;
assert(s1.source().color() == RED);
 
 
K::Intersect_2 intersection;
 
intersect = intersection(s1, s2);
 
K::Construct_cartesian_const_iterator_2 construct_it;
K::Cartesian_const_iterator_2 cit = construct_it(a);
assert(*cit == a.x());
 
cit = construct_it(a,0);
 
cit--;
assert(*cit == a.y());
 
Line l1(a,b), l2(p, q);
 
intersection(l1, l2);
 
 
intersection(s1, l1);
 
Ray r1(d,b), r2(d,c);
intersection(r1, r2);
 
intersection(r1, l1);
 
 
Triangle t1(a,b,c), t2(a,c,d);
intersection(t1, t2);
intersection(t1, l1);
 
intersection(t1, s1);
 
intersection(t1, r1);
 
Iso_rectangle i1(a,c), i2(d,p);
intersection(i1, i2);
intersection(i1, s1);
 
intersection(i1, r1);
intersection(i1, l1);
 
t1.orientation();
 
std::cout << s1.source() << std::endl;
 
std::cout << t1.bbox() << std::endl;
 
std::cout << "done" << std::endl;
return 0;
}

5.3 Limitations

The point class must have member functions x() and y() (and z() for the 3d point). We will probably introduce function objects that take care of coordinate access.

As we enforce type equality between MyKernel::Point_2 and Point_2<MyKernel>, the constructor with the color as third argument is not available.

Projection Traits Classes

It is sometimes useful to apply 2D algorithms to the projection of 3D points on a plane. Examples are triangulated terrains, which are points with elevation, or surface reconstruction from parallel slices, where one wants to check the simplicity or orientation of polygons.

For this purpose CGAL provides several projection traits classes, which are a model of traits class concepts of 2D triangulations, 2D polygon and 2D convex hull traits classes. The projection traits classes are listed in the "Is Model Of" sections of the concepts.

Design and Implementation History

At a meeting at Utrecht University in January 1995, Olivier Devillers, Andreas Fabri, Wolfgang Freiseisen, Geert-Jan Giezeman, Mark Overmars, Stefan Schirra, Otfried Schwarzkopf (now Otfried Cheong), and Sven Schönherr discussed the foundations of the CGAL kernel. Many design and software engineering issues were addressed, e.g. naming conventions, coupling of classes (flat versus deep class hierarchy), memory allocation, programming conventions, mutability of atomic objects, points and vectors, storing additional information, orthogonality of operations on the kernel objects, viewing non-constant-size objects like polygons as dynamic data structures (and hence not as part of the (innermost) kernel).

The people attending the meeting delegated the compilation of a draft specification to Stefan Schirra. The resulting draft specification was intentionally modeled on CGAL's precursors C++gal and Plageo as well as on the geometric part of LEDA. The specification already featured coexistence of Cartesian and homogeneous representation of point/vector data and parameterization by number type(s). During the discussion of the draft a kernel design group was formed. The members of this group were Andreas Fabri, Geert-Jan Giezeman, Lutz Kettner, Stefan Schirra, and Sven Schönherr. The work of the kernel design group led to significant changes and improvements of the original design, e.g. the strong separation between points and vectors. Probably the most important enhancement was the design of a common superstructure for the previously uncoupled Cartesian and homogeneous representations. One can say, that the kernel was designed by this group. The kernel was later revised based on suggestions by Hervé Brönnimann, Bernd Gärtner, Michael Hoffmann, and Lutz Kettner.

A first version of the kernel was internally made available at the beginning of the CGAL-project (esprit ltr iv project number 21957). Since then many more people contributed to the evolution of the kernel through discussions on the CGAL mailing lists. The implementation based on Cartesian representation was (initially) provided by Andreas Fabri, the homogeneous representation (initially) by Stefan Schirra. Intersection and distance computations were implemented by Geert-Jan Giezeman. Further work has been done by Susan Hert on the overall maintenance of the kernel. Philippe Guigue has provided efficient intersection tests for 3D triangles. Andreas Fabri, Michael Hoffmann and Sylvain Pion have improved the support for the extensibility and adaptability of the kernel. Pedro Machado Manhães de Castro and Monique Teillaud introduced 3D circles. In 2010, Pierre Alliez, Stéphane Tayeb and Camille Wormser added intersection constructions for 3D triangles and efficient intersection tests for bounding boxes.

7.1 Acknowledgment

This work was supported by the Graduiertenkolleg 'Algorithmische Diskrete Mathematik', under grant DFG We 1265/2-1, and by ESPRIT IV Long Term Research Projects No. 21957 (CGAL) and No. 28155 (GALIA).

    1. abcCurrently it requires having either LEDA or CORE installed.
    2. ^you might call midpoint(p_1,p_2) instead.
    3. ^Any sequence of points can be seen as a (not necessary simple) polygon or polyline. This view is used frequently in the basic library as well.
posted @ 2018-12-21 18:09  小船1968  阅读(884)  评论(0编辑  收藏  举报