/* Compare the slope of a to the slope of b, returning 1, 0, -1 if the * slope a is respectively greater than, equal to, or less than the * slope of b. * * For each edge, consider the direction vector formed from: * * top -> bottom * * which is: * * (dx, dy) = (line.p2.x - line.p1.x, line.p2.y - line.p1.y) * * We then define the slope of each edge as dx/dy, (which is the * inverse of the slope typically used in math instruction). We never * compute a slope directly as the value approaches infinity, but we * can derive a slope comparison without division as follows, (where * the ? represents our compare operator). * * 1. slope(a) ? slope(b) * 2. adx/ady ? bdx/bdy * 3. (adx * bdy) ? (bdx * ady) * * Note that from step 2 to step 3 there is no change needed in the * sign of the result since both ady and bdy are guaranteed to be * greater than or equal to 0. * * When using this slope comparison to sort edges, some care is needed * when interpreting the results. Since the slope compare operates on * distance vectors from top to bottom it gives a correct left to * right sort for edges that have a common top point, (such as two * edges with start events at the same location). On the other hand, * the sense of the result will be exactly reversed for two edges that * have a common stop point.
*/ staticinlineint
_slope_compare (const cairo_bo_edge_t *a, const cairo_bo_edge_t *b)
{ /* XXX: We're assuming here that dx and dy will still fit in 32 * bits. That's not true in general as there could be overflow. We * should prevent that before the tessellation algorithm * begins.
*/
int32_t adx = a->edge.line.p2.x - a->edge.line.p1.x;
int32_t bdx = b->edge.line.p2.x - b->edge.line.p1.x;
/* Since the dy's are all positive by construction we can fast * path several common cases.
*/
/* First check for vertical lines. */ if (adx == 0) return -bdx; if (bdx == 0) return adx;
/* Then where the two edges point in different directions wrt x. */ if ((adx ^ bdx) < 0) return adx;
/* Finally we actually need to do the general comparison. */
{
int32_t ady = a->edge.line.p2.y - a->edge.line.p1.y;
int32_t bdy = b->edge.line.p2.y - b->edge.line.p1.y;
cairo_int64_t adx_bdy = _cairo_int32x32_64_mul (adx, bdy);
cairo_int64_t bdx_ady = _cairo_int32x32_64_mul (bdx, ady);
return _cairo_int64_cmp (adx_bdy, bdx_ady);
}
}
/* * We need to compare the x-coordinate of a line for a particular y wrt to a * given x, without loss of precision. * * The x-coordinate along an edge for a given y is: * X = A_x + (Y - A_y) * A_dx / A_dy * * So the inequality we wish to test is: * A_x + (Y - A_y) * A_dx / A_dy ∘ X * where ∘ is our inequality operator. * * By construction, we know that A_dy (and (Y - A_y)) are * all positive, so we can rearrange it thus without causing a sign change: * (Y - A_y) * A_dx ∘ (X - A_x) * A_dy * * Given the assumption that all the deltas fit within 32 bits, we can compute * this comparison directly using 64 bit arithmetic. * * See the similar discussion for _slope_compare() and * edges_compare_x_for_y_general().
*/ staticint
edge_compare_for_y_against_x (const cairo_bo_edge_t *a,
int32_t y,
int32_t x)
{
int32_t adx, ady;
int32_t dx, dy;
cairo_int64_t L, R;
if (x < a->edge.line.p1.x && x < a->edge.line.p2.x) return 1; if (x > a->edge.line.p1.x && x > a->edge.line.p2.x) return -1;
adx = a->edge.line.p2.x - a->edge.line.p1.x;
dx = x - a->edge.line.p1.x;
if (adx == 0) return -dx; if (dx == 0 || (adx ^ dx) < 0) return adx;
dy = y - a->edge.line.p1.y;
ady = a->edge.line.p2.y - a->edge.line.p1.y;
L = _cairo_int32x32_64_mul (dy, adx);
R = _cairo_int32x32_64_mul (dx, ady);
staticinline cairo_int64_t
det32_64 (int32_t a, int32_t b,
int32_t c, int32_t d)
{ /* det = a * d - b * c */ return _cairo_int64_sub (_cairo_int32x32_64_mul (a, d),
_cairo_int32x32_64_mul (b, c));
}
staticinline cairo_int128_t
det64x32_128 (cairo_int64_t a, int32_t b,
cairo_int64_t c, int32_t d)
{ /* det = a * d - b * c */ return _cairo_int128_sub (_cairo_int64x32_128_mul (a, d),
_cairo_int64x32_128_mul (c, b));
}
/* Compute the intersection of two lines as defined by two edges. The * result is provided as a coordinate pair of 128-bit integers. * * Returns %CAIRO_BO_STATUS_INTERSECTION if there is an intersection or * %CAIRO_BO_STATUS_PARALLEL if the two lines are exactly parallel.
*/ static cairo_bool_t
intersect_lines (cairo_bo_edge_t *a,
cairo_bo_edge_t *b,
cairo_bo_intersect_point_t *intersection)
{
cairo_int64_t a_det, b_det;
/* XXX: We're assuming here that dx and dy will still fit in 32 * bits. That's not true in general as there could be overflow. We * should prevent that before the tessellation algorithm begins. * What we're doing to mitigate this is to perform clamping in * cairo_bo_tessellate_polygon().
*/
int32_t dx1 = a->edge.line.p1.x - a->edge.line.p2.x;
int32_t dy1 = a->edge.line.p1.y - a->edge.line.p2.y;
/* Q: Can we determine that the lines do not intersect (within range) * much more cheaply than computing the intersection point i.e. by * avoiding the division? * * X = ax + t * adx = bx + s * bdx; * Y = ay + t * ady = by + s * bdy; * ∴ t * (ady*bdx - bdy*adx) = bdx * (by - ay) + bdy * (ax - bx) * => t * L = R * * Therefore we can reject any intersection (under the criteria for * valid intersection events) if: * L^R < 0 => t < 0, or * L<R => t > 1 * * (where top/bottom must at least extend to the line endpoints). * * A similar substitution can be performed for s, yielding: * s * (ady*bdx - bdy*adx) = ady * (ax - bx) - adx * (ay - by)
*/
R = det32_64 (dx2, dy2,
b->edge.line.p1.x - a->edge.line.p1.x,
b->edge.line.p1.y - a->edge.line.p1.y); if (_cairo_int64_negative (den_det)) { if (_cairo_int64_ge (den_det, R)) returnFALSE;
} else { if (_cairo_int64_le (den_det, R)) returnFALSE;
}
R = det32_64 (dy1, dx1,
a->edge.line.p1.y - b->edge.line.p1.y,
a->edge.line.p1.x - b->edge.line.p1.x); if (_cairo_int64_negative (den_det)) { if (_cairo_int64_ge (den_det, R)) returnFALSE;
} else { if (_cairo_int64_le (den_det, R)) returnFALSE;
}
/* We now know that the two lines should intersect within range. */
staticint
_cairo_bo_intersect_ordinate_32_compare (cairo_bo_intersect_ordinate_t a,
int32_t b)
{ /* First compare the quotient */ if (a.ordinate > b) return +1; if (a.ordinate < b) return -1; /* With quotient identical, if remainder is 0 then compare equal */ /* Otherwise, the non-zero remainder makes a > b */ return INEXACT == a.exactness;
}
/* Does the given edge contain the given point. The point must already * be known to be contained within the line determined by the edge, * (most likely the point results from an intersection of this edge * with another). * * If we had exact arithmetic, then this function would simply be a * matter of examining whether the y value of the point lies within * the range of y values of the edge. But since intersection points * are not exact due to being rounded to the nearest integer within * the available precision, we must also examine the x value of the * point. * * The definition of "contains" here is that the given intersection * point will be seen by the sweep line after the start event for the * given edge and before the stop event for the edge. See the comments * in the implementation for more details.
*/ static cairo_bool_t
_cairo_bo_edge_contains_intersect_point (cairo_bo_edge_t *edge,
cairo_bo_intersect_point_t *point)
{ int cmp_top, cmp_bottom;
/* XXX: When running the actual algorithm, we don't actually need to * compare against edge->top at all here, since any intersection above * top is eliminated early via a slope comparison. We're leaving these * here for now only for the sake of the quadratic-time intersection * finder which needs them.
*/
/* At this stage, the point lies on the same y value as either * edge->top or edge->bottom, so we have to examine the x value in
* order to properly determine containment. */
/* If the y value of the point is the same as the y value of the * top of the edge, then the x value of the point must be greater * to be considered as inside the edge. Similarly, if the y value * of the point is the same as the y value of the bottom of the * edge, then the x value of the point must be less to be
* considered as inside. */
/* Compute the intersection of two edges. The result is provided as a * coordinate pair of 128-bit integers. * * Returns %CAIRO_BO_STATUS_INTERSECTION if there is an intersection * that is within both edges, %CAIRO_BO_STATUS_NO_INTERSECTION if the * intersection of the lines defined by the edges occurs outside of * one or both edges, and %CAIRO_BO_STATUS_PARALLEL if the two edges * are exactly parallel. * * Note that when determining if a candidate intersection is "inside" * an edge, we consider both the infinitesimal shortening and the * infinitesimal tilt rules described by John Hobby. Specifically, if * the intersection is exactly the same as an edge point, it is * effectively outside (no intersection is returned). Also, if the * intersection point has the same
*/ static cairo_bool_t
_cairo_bo_edge_intersect (cairo_bo_edge_t *a,
cairo_bo_edge_t *b,
cairo_bo_point32_t *intersection)
{
cairo_bo_intersect_point_t quorem;
if (! intersect_lines (a, b, &quorem)) returnFALSE;
if (! _cairo_bo_edge_contains_intersect_point (a, &quorem)) returnFALSE;
if (! _cairo_bo_edge_contains_intersect_point (b, &quorem)) returnFALSE;
/* Now that we've correctly compared the intersection point and * determined that it lies within the edge, then we know that we * no longer need any more bits of storage for the intersection * than we do for our edge coordinates. We also no longer need the
* remainder from the division. */
intersection->x = quorem.x.ordinate;
intersection->y = quorem.y.ordinate;
if (MAX (left->edge.line.p1.x, left->edge.line.p2.x) <=
MIN (right->edge.line.p1.x, right->edge.line.p2.x)) return CAIRO_STATUS_SUCCESS;
if (cairo_lines_equal (&left->edge.line, &right->edge.line)) return CAIRO_STATUS_SUCCESS;
/* The names "left" and "right" here are correct descriptions of * the order of the two edges within the active edge list. So if a * slope comparison also puts left less than right, then we know * that the intersection of these two segments has already
* occurred before the current sweep line position. */ if (_slope_compare (left, right) <= 0) return CAIRO_STATUS_SUCCESS;
if (! _cairo_bo_edge_intersect (left, right, &intersection)) return CAIRO_STATUS_SUCCESS;
/* The choice of y is not truly arbitrary since we must guarantee that it * is greater than the start of either line.
*/ if (p != 0) { /* colinear if either end-point are coincident */
p = (((p >> 1) & p) & 5) != 0;
} elseif (a->edge.line.p1.y < b->edge.line.p1.y) {
p = edge_compare_for_y_against_x (b,
a->edge.line.p1.y,
a->edge.line.p1.x) == 0;
} else {
p = edge_compare_for_y_against_x (a,
b->edge.line.p1.y,
b->edge.line.p1.x) == 0;
}
a->colinear = MARK_COLINEAR(b, p); return p;
}
/* Adds the trapezoid, if any, of the left edge to the #cairo_traps_t */ staticvoid
_cairo_bo_edge_end_trap (cairo_bo_edge_t *left,
int32_t bot,
cairo_traps_t *traps)
{
cairo_bo_trap_t *trap = &left->deferred_trap;
/* Only emit (trivial) non-degenerate trapezoids with positive height. */ if (likely (trap->top < bot)) {
_cairo_traps_add_trap (traps,
trap->top, bot,
&left->edge.line, &trap->right->edge.line);
/* Start a new trapezoid at the given top y coordinate, whose edges * are `edge' and `edge->next'. If `edge' already has a trapezoid, * then either add it to the traps in `traps', if the trapezoid's * right edge differs from `edge->next', or do nothing if the new
* trapezoid would be a continuation of the existing one. */ staticinlinevoid
_cairo_bo_edge_start_or_continue_trap (cairo_bo_edge_t *left,
cairo_bo_edge_t *right, int top,
cairo_traps_t *traps)
{ if (left->deferred_trap.right == right) return;
assert (right); if (left->deferred_trap.right != NULL) { if (edges_colinear (left->deferred_trap.right, right))
{ /* continuation on right, so just swap edges */
left->deferred_trap.right = right; return;
}
#if DEBUG_PRINT_STATE
printf ("Processing active edges for %x\n", top); #endif
in_out = 0;
left = pos; while (pos != NULL) { if (pos != left && pos->deferred_trap.right) { /* XXX It shouldn't be possible to here with 2 deferred traps * on colinear edges... See bug-bo-rictoz.
*/ if (left->deferred_trap.right == NULL &&
edges_colinear (left, pos))
{ /* continuation on left */
left->deferred_trap = pos->deferred_trap;
pos->deferred_trap.right = NULL;
} else
{
_cairo_bo_edge_end_trap (pos, top, traps);
}
}
/* Execute a single pass of the Bentley-Ottmann algorithm on edges, * generating trapezoids according to the fill_rule and appending them
* to traps. */ static cairo_status_t
_cairo_bentley_ottmann_tessellate_bo_edges (cairo_bo_event_t **start_events, int num_events, unsigned fill_rule,
cairo_traps_t *traps, int *num_intersections)
{
cairo_status_t status; int intersection_count = 0;
cairo_bo_event_queue_t event_queue;
cairo_bo_sweep_line_t sweep_line;
cairo_bo_event_t *event;
cairo_bo_edge_t *left, *right;
cairo_bo_edge_t *e1, *e2;
/* convert the fill_rule into a winding mask */ if (fill_rule == CAIRO_FILL_RULE_WINDING)
fill_rule = (unsigned) -1; else
fill_rule = 1;
switch (event->type) { case CAIRO_BO_EVENT_TYPE_START:
e1 = &((cairo_bo_start_event_t *) event)->edge;
_cairo_bo_sweep_line_insert (&sweep_line, e1);
status = _cairo_bo_event_queue_insert_stop (&event_queue, e1); if (unlikely (status)) goto unwind;
/* check to see if this is a continuation of a stopped edge */ /* XXX change to an infinitesimal lengthening rule */ for (left = sweep_line.stopped; left; left = left->next) { if (e1->edge.top <= left->edge.bottom &&
edges_colinear (e1, left))
{
e1->deferred_trap = left->deferred_trap; if (left->prev != NULL)
left->prev = left->next; else
sweep_line.stopped = left->next; if (left->next != NULL)
left->next->prev = left->prev; break;
}
}
left = e1->prev;
right = e1->next;
if (left != NULL) {
status = _cairo_bo_event_queue_insert_if_intersect_below_current_y (&event_queue, left, e1); if (unlikely (status)) goto unwind;
}
if (right != NULL) {
status = _cairo_bo_event_queue_insert_if_intersect_below_current_y (&event_queue, e1, right); if (unlikely (status)) goto unwind;
}
break;
case CAIRO_BO_EVENT_TYPE_STOP:
e1 = ((cairo_bo_queue_event_t *) event)->e1;
_cairo_bo_event_queue_delete (&event_queue, event);
left = e1->prev;
right = e1->next;
_cairo_bo_sweep_line_delete (&sweep_line, e1);
/* first, check to see if we have a continuation via a fresh edge */ if (e1->deferred_trap.right != NULL) {
e1->next = sweep_line.stopped; if (sweep_line.stopped != NULL)
sweep_line.stopped->prev = e1;
sweep_line.stopped = e1;
e1->prev = NULL;
}
if (left != NULL && right != NULL) {
status = _cairo_bo_event_queue_insert_if_intersect_below_current_y (&event_queue, left, right); if (unlikely (status)) goto unwind;
}
/* XXX: This would be the convenient place to throw in multiple * passes of the Bentley-Ottmann algorithm. It would merely * require storing the results of each pass into a temporary
* cairo_traps_t. */
status = _cairo_bentley_ottmann_tessellate_bo_edges (event_ptrs, num_events,
fill_rule, traps,
&intersections); #if DEBUG_TRAPS
dump_traps (traps, "bo-polygon-out.txt"); #endif
for (i = 0; i < traps->num_traps; i++) {
status = _cairo_polygon_add_line (&polygon,
&traps->traps[i].left,
traps->traps[i].top,
traps->traps[i].bottom,
1); if (unlikely (status)) goto CLEANUP;
status = _cairo_polygon_add_line (&polygon,
&traps->traps[i].right,
traps->traps[i].top,
traps->traps[i].bottom,
-1); if (unlikely (status)) goto CLEANUP;
}
_cairo_traps_clear (traps);
status = _cairo_bentley_ottmann_tessellate_polygon (traps,
&polygon,
fill_rule);
#if 0 static cairo_bool_t
edges_have_an_intersection_quadratic (cairo_bo_edge_t *edges, int num_edges)
{ int i, j;
cairo_bo_edge_t *a, *b;
cairo_bo_point32_t intersection;
/* We must not be given any upside-down edges. */ for (i = 0; i < num_edges; i++) {
assert (_cairo_bo_point32_compare (&edges[i].top, &edges[i].bottom) < 0);
edges[i].line.p1.x <<= CAIRO_BO_GUARD_BITS;
edges[i].line.p1.y <<= CAIRO_BO_GUARD_BITS;
edges[i].line.p2.x <<= CAIRO_BO_GUARD_BITS;
edges[i].line.p2.y <<= CAIRO_BO_GUARD_BITS;
}
for (i = 0; i < num_edges; i++) { for (j = 0; j < num_edges; j++) { if (i == j) continue;
a = &edges[i];
b = &edges[j];
if (! _cairo_bo_edge_intersect (a, b, &intersection)) continue;
printf ("Found intersection (%d,%d) between (%d,%d)-(%d,%d) and (%d,%d)-(%d,%d)\n",
intersection.x,
intersection.y,
a->line.p1.x, a->line.p1.y,
a->line.p2.x, a->line.p2.y,
b->line.p1.x, b->line.p1.y,
b->line.p2.x, b->line.p2.y);
returnTRUE;
}
} returnFALSE;
}
#define TEST_MAX_EDGES 10
typedefstruct test { constchar *name; constchar *description; int num_edges;
cairo_bo_edge_t edges[TEST_MAX_EDGES];
} test_t;
static test_t
tests[] = {
{ "3 near misses", "3 edges all intersecting very close to each other",
3,
{
{ { 4, 2}, {0, 0}, { 9, 9}, NULL, NULL },
{ { 7, 2}, {0, 0}, { 2, 3}, NULL, NULL },
{ { 5, 2}, {0, 0}, { 1, 7}, NULL, NULL }
}
},
{ "inconsistent data", "Derived from random testing---was leading to skip list and edge list disagreeing.",
2,
{
{ { 2, 3}, {0, 0}, { 8, 9}, NULL, NULL },
{ { 2, 3}, {0, 0}, { 6, 7}, NULL, NULL }
}
},
{ "failed sort", "A test derived from random testing that leads to an inconsistent sort --- looks like we just can't attempt to validate the sweep line with edge_compare?",
3,
{
{ { 6, 2}, {0, 0}, { 6, 5}, NULL, NULL },
{ { 3, 5}, {0, 0}, { 5, 6}, NULL, NULL },
{ { 9, 2}, {0, 0}, { 5, 6}, NULL, NULL },
}
},
{ "minimal-intersection", "Intersection of a two from among the smallest possible edges.",
2,
{
{ { 0, 0}, {0, 0}, { 1, 1}, NULL, NULL },
{ { 1, 0}, {0, 0}, { 0, 1}, NULL, NULL }
}
},
{ "simple", "A simple intersection of two edges at an integer (2,2).",
2,
{
{ { 1, 1}, {0, 0}, { 3, 3}, NULL, NULL },
{ { 2, 1}, {0, 0}, { 2, 3}, NULL, NULL }
}
},
{ "bend-to-horizontal", "With intersection truncation one edge bends to horizontal",
2,
{
{ { 9, 1}, {0, 0}, {3, 7}, NULL, NULL },
{ { 3, 5}, {0, 0}, {9, 9}, NULL, NULL }
}
}
};
/* { "endpoint", "An intersection that occurs at the endpoint of a segment.", { { { 4, 6}, { 5, 6}, NULL, { { NULL }} }, { { 4, 5}, { 5, 7}, NULL, { { NULL }} }, { { 0, 0}, { 0, 0}, NULL, { { NULL }} }, } } { name = "overlapping", desc = "Parallel segments that share an endpoint, with different slopes.", edges = { { top = { x = 2, y = 0}, bottom = { x = 1, y = 1}}, { top = { x = 2, y = 0}, bottom = { x = 0, y = 2}}, { top = { x = 0, y = 3}, bottom = { x = 1, y = 3}}, { top = { x = 0, y = 3}, bottom = { x = 2, y = 3}}, { top = { x = 0, y = 4}, bottom = { x = 0, y = 6}}, { top = { x = 0, y = 5}, bottom = { x = 0, y = 6}} } }, { name = "hobby_stage_3", desc = "A particularly tricky part of the 3rd stage of the 'hobby' test below.", edges = { { top = { x = -1, y = -2}, bottom = { x = 4, y = 2}}, { top = { x = 5, y = 3}, bottom = { x = 9, y = 5}}, { top = { x = 5, y = 3}, bottom = { x = 6, y = 3}}, } }, { name = "hobby", desc = "Example from John Hobby's paper. Requires 3 passes of the iterative algorithm.", edges = { { top = { x = 0, y = 0}, bottom = { x = 9, y = 5}}, { top = { x = 0, y = 0}, bottom = { x = 13, y = 6}}, { top = { x = -1, y = -2}, bottom = { x = 9, y = 5}} } }, { name = "slope", desc = "Edges with same start/stop points but different slopes", edges = { { top = { x = 4, y = 1}, bottom = { x = 6, y = 3}}, { top = { x = 4, y = 1}, bottom = { x = 2, y = 3}}, { top = { x = 2, y = 4}, bottom = { x = 4, y = 6}}, { top = { x = 6, y = 4}, bottom = { x = 4, y = 6}} } }, { name = "horizontal", desc = "Test of a horizontal edge", edges = { { top = { x = 1, y = 1}, bottom = { x = 6, y = 6}}, { top = { x = 2, y = 3}, bottom = { x = 5, y = 3}} } }, { name = "vertical", desc = "Test of a vertical edge", edges = { { top = { x = 5, y = 1}, bottom = { x = 5, y = 7}}, { top = { x = 2, y = 4}, bottom = { x = 8, y = 5}} } }, { name = "congruent", desc = "Two overlapping edges with the same slope", edges = { { top = { x = 5, y = 1}, bottom = { x = 5, y = 7}}, { top = { x = 5, y = 2}, bottom = { x = 5, y = 6}}, { top = { x = 2, y = 4}, bottom = { x = 8, y = 5}} } }, { name = "multi", desc = "Several segments with a common intersection point", edges = { { top = { x = 1, y = 2}, bottom = { x = 5, y = 4} }, { top = { x = 1, y = 1}, bottom = { x = 5, y = 5} }, { top = { x = 2, y = 1}, bottom = { x = 4, y = 5} }, { top = { x = 4, y = 1}, bottom = { x = 2, y = 5} }, { top = { x = 5, y = 1}, bottom = { x = 1, y = 5} }, { top = { x = 5, y = 2}, bottom = { x = 1, y = 4} } } } };
*/
staticint
run_test (constchar *test_name,
cairo_bo_edge_t *test_edges, int num_edges)
{ int i, intersections, passes;
cairo_bo_edge_t *edges;
cairo_array_t intersected_edges;
intersections = _cairo_bentley_ottmann_intersect_edges (test_edges, num_edges, &intersected_edges); if (intersections)
printf ("Pass 1 found %d intersections:\n", intersections);
/* XXX: Multi-pass Bentley-Ottmmann. Preferable would be to add a
* pass of Hobby's tolerance-square algorithm instead. */
passes = 1; while (intersections) { int num_edges = _cairo_array_num_elements (&intersected_edges);
passes++;
edges = _cairo_malloc_ab (num_edges, sizeof (cairo_bo_edge_t));
assert (edges != NULL);
memcpy (edges, _cairo_array_index (&intersected_edges, 0), num_edges * sizeof (cairo_bo_edge_t));
_cairo_array_fini (&intersected_edges);
_cairo_array_init (&intersected_edges, sizeof (cairo_bo_edge_t));
intersections = _cairo_bentley_ottmann_intersect_edges (edges, num_edges, &intersected_edges);
free (edges);
if (intersections){
printf ("Pass %d found %d remaining intersections:\n", passes, intersections);
} else { if (passes > 3) for (i = 0; i < passes; i++)
printf ("*");
printf ("No remainining intersections found after pass %d\n", passes);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.