Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/GeometryOps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ include("methods/clipping/overlayng/noding/node_identity.jl")
include("methods/clipping/overlayng/noding/split.jl")
include("methods/clipping/overlayng/noding/emit.jl")

# OverlayNG engine core (phase 2a): the half-edge graph over the arrangement.
include("methods/clipping/overlayng/overlay_label.jl")
include("methods/clipping/overlayng/edge_source.jl")
include("methods/clipping/overlayng/half_edge.jl")
include("methods/clipping/overlayng/overlay_graph.jl")

# OverlayNG engine core (phase 2b): labeller, result builders, and the driver.
include("methods/clipping/overlayng/overlay_labeller.jl")
include("methods/clipping/overlayng/maximal_edge_ring.jl")
include("methods/clipping/overlayng/polygon_builder.jl")
include("methods/clipping/overlayng/line_builder.jl")
include("methods/clipping/overlayng/intersection_point_builder.jl")
include("methods/clipping/overlayng/overlay_ng.jl")

include("methods/orientation.jl")
include("methods/polygonize.jl")
include("methods/minimum_bounding_circle.jl")
Expand Down
59 changes: 59 additions & 0 deletions src/methods/clipping/overlayng/edge_source.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# # EdgeSourceInfo — per-ring/line source topology (port of JTS `EdgeSourceInfo`)
#
# Phase 2a of the OverlayNG port (design doc §2.7, §3 amendment 3). Records the
# topological information carried by one source linework (a polygon ring or a
# line) from a single input geometry: which input it came from, its dimension,
# its ring role, and — for area rings — the signed `depth_delta`.
#
# One `EdgeSourceInfo` exists per `RelateSegmentString`, indexed by the segment
# string's position in the arrangement's `segstrings`. A `NodedEdge` reaches its
# source info through `NodedEdge.string_idx`; nothing is copied per noded edge
# (design §2.1 invariant 5), so nothing can desynchronize.
#
# Everything here is internal to GeometryOps — nothing is exported.

"""
EdgeSourceInfo

Source topology of one input linework (port of JTS `EdgeSourceInfo`): `index`
(`0` = A, `1` = B), `dim` (`DIM_A` for a ring, `DIM_L` for a line), `is_hole`
(ring role), and `depth_delta` (the signed side-labelling delta of an area ring,
`0` for a line). Consumed by the edge merger to build `OverlayLabel`s.
"""
struct EdgeSourceInfo
index :: Int8
dim :: Int8
is_hole :: Bool
depth_delta :: Int8
end

# The depth delta of one area ring, derived ONCE from the material-interior
# authority (design §2.7). JTS `EdgeNodingBuilder.computeDepthDelta` assigns the
# canonical delta `+1` to a ring in "Exterior-on-Left" orientation and flips to
# `-1` otherwise; equivalently (`Edge.locationLeft/Right`) a positive delta means
# Left = EXTERIOR, Right = INTERIOR — i.e. the material interior lies on the
# RIGHT. So `depth_delta = material_interior_on_left ? -1 : +1`, computed on the
# ring's stored order and matching that convention exactly. Folding the hole flip
# into `_ring_material_interior_on_left` keeps relate / overlay / extents in
# agreement by construction.
function _ring_depth_delta(m::Manifold, pts::Vector, is_hole::Bool; exact)
return _ring_material_interior_on_left(m, pts, is_hole; exact) ? Int8(-1) : Int8(1)
end

# Build the source info for one segment string. Area rings carry a depth delta;
# lines carry none (`dim == DIM_L`, `depth_delta == 0`).
function _edge_source_info(m::Manifold, ss::RelateSegmentString; exact = True())
index = ss.is_a ? Int8(0) : Int8(1)
if ss.dim == DIM_A
is_hole = _ss_is_hole(ss)
return EdgeSourceInfo(index, DIM_A, is_hole, _ring_depth_delta(m, ss.pts, is_hole; exact))
end
#-- line (or any non-area edge): no side labelling
return EdgeSourceInfo(index, DIM_L, false, Int8(0))
end

# The per-string source-info table of an arrangement, aligned to `segstrings`
# (so `sources[NodedEdge.string_idx]` is the noded edge's source info).
function _edge_source_infos(m::Manifold, arr::NodedArrangement; exact = True())
return [_edge_source_info(m, ss; exact) for ss in arr.segstrings]
end
139 changes: 139 additions & 0 deletions src/methods/clipping/overlayng/half_edge.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# # Half-edge substrate — the winged-edge algebra (port of JTS `edgegraph/HalfEdge`)
#
# Phase 2a of the OverlayNG port (design doc §3). A port of
# `edgegraph/HalfEdge.java`, adapted to integer-indexed storage: half-edges live
# in a `Vector` and refer to each other by 1-based index (`0` = null), not by
# object reference — the spike-validated representation ("indices, not object
# references, proved painless"). Directed half-edges occur in symmetric pairs;
# each stores the index of its symmetric partner (`sym`) and of the next
# half-edge CCW around its origin's star.
#
# Field contract (every element of the edge store must provide these mutable
# fields; `OverlayEdge` in overlay_graph.jl is the sole concrete implementation):
# - `origin :: Int32` — the origin node id (into the arrangement's node table).
# - `sym :: Int32` — index of the symmetric (oppositely-oriented) half-edge.
# - `o_next :: Int32` — index of the next half-edge CCW around the origin, with
# the same origin (JTS `oNext`); a degree-1 origin points
# it at the half-edge itself.
# - `dir_pt :: P` — the direction point for angular ordering: the parent
# segment's far endpoint, an original kernel vertex
# (design §3 amendment 1) — never a constructed coordinate.
#
# Adaptation note (winged-edge field choice): JTS stores `next` (the next edge CCW
# around the DESTINATION) and derives `oNext() == sym.next`. We store `o_next`
# (around the ORIGIN) directly, because it is the primitive every overlay
# traversal uses and it makes `oNext`/`degree`/`prev` index-only with no `sym`
# indirection; JTS's `next` is recoverable as `next(e) == o_next(sym(e))`. We
# bulk-build each node's star and sort it once with the exact angular comparator
# (the spike's `_order_stars!`), in place of JTS's incremental `insert`: the exact
# comparator is costly, so one `O(d log d)` sort per node beats `insert`'s repeated
# rescans, and integer storage makes a bulk sort trivial.
#
# Everything here is internal to GeometryOps — nothing is exported.

# ## Core edge algebra (integer-indexed ports of the HalfEdge methods)

# The symmetric (oppositely-oriented) partner (JTS `sym`).
@inline he_sym(edges, i::Integer) = @inbounds edges[i].sym

# The next half-edge CCW around the origin, same origin (JTS `oNext`).
@inline he_onext(edges, i::Integer) = @inbounds edges[i].o_next

# The origin node id.
@inline he_origin(edges, i::Integer) = @inbounds edges[i].origin

# The destination node id: the origin of the sym edge (JTS `dest`).
@inline he_dest(edges, i::Integer) = @inbounds edges[he_sym(edges, i)].origin

# The next half-edge CCW around the destination (JTS `next`), recovered from the
# stored `o_next`: `next(e) == oNext(sym(e))`.
@inline he_next(edges, i::Integer) = he_onext(edges, he_sym(edges, i))

# The previous half-edge CW around the origin, with that vertex as its
# destination. Always `he_next(he_prev(e)) == e`. Port of JTS `prev` (scan the
# origin star, return the last edge's sym).
function he_prev(edges, i::Integer)
curr = Int32(i); prev = Int32(i)
while true
prev = curr
curr = he_onext(edges, curr)
curr == i && break
end
return he_sym(edges, prev)
end

# The degree of the origin vertex: the number of half-edges originating there
# (port of JTS `degree`).
function he_degree(edges, i::Integer)
d = 0
e = Int32(i)
while true
d += 1
e = he_onext(edges, e)
e == i && break
end
return d
end

# The first node (degree != 2) reached walking `prev` from this edge, or `0` if
# the edge is part of a ring with no such node (port of JTS `prevNode`).
function he_prev_node(edges, i::Integer)
e = Int32(i)
while he_degree(edges, e) == 2
e = he_prev(edges, e)
e == i && return Int32(0)
end
return e
end

# The half-edge originating at this edge's origin with the given destination node
# id, or `0` if none (port of JTS `find`, on node ids rather than coordinates).
function he_find(edges, i::Integer, dest::Integer)
o = Int32(i)
while true
he_dest(edges, o) == dest && return o
o = he_onext(edges, o)
o == i && break
end
return Int32(0)
end

# ## Symmetric-pair linkage and star ordering

# Link a freshly created symmetric pair `(i, j)`: set the sym pointers and the
# single-segment star (each edge's `o_next` is itself, so a degree-1 origin's
# `oNext` is the edge itself — JTS `link`, restated for the origin star). Both are
# overwritten for interior-star edges once their origin's star is ordered.
@inline function he_link!(edges, i::Integer, j::Integer)
@inbounds edges[i].sym = Int32(j)
@inbounds edges[j].sym = Int32(i)
@inbounds edges[i].o_next = Int32(i)
@inbounds edges[j].o_next = Int32(j)
return nothing
end

# Angular comparison of two half-edges that share an origin, about that origin's
# symbolic apex (the node's `NodeKey`), via the exact kernel comparator
# (`rk_compare_edge_dir`, design §3 amendment 1). Foreign directions at a
# coincidence-merged apex take the kernel's exact-rational slow path — no special
# casing here.
@inline function he_compare_angular(m::Manifold, edges, keys, i::Integer, j::Integer; exact)
apex = @inbounds keys[he_origin(edges, i)]
return rk_compare_edge_dir(m, apex, (@inbounds edges[i].dir_pt), (@inbounds edges[j].dir_pt); exact)
end

# Order one node's star of outgoing half-edge indices CCW and wire the origin ring
# so that `he_onext` walks it (the spike's `_order_stars!`). `star` is mutated
# into CCW order. All members must share the origin whose apex is `keys[origin]`.
function he_order_star!(m::Manifold, edges, keys, star::Vector{Int32}; exact)
n = length(star)
if n == 1
@inbounds edges[star[1]].o_next = star[1]
return nothing
end
sort!(star; lt = (i, j) -> he_compare_angular(m, edges, keys, i, j; exact) < 0)
@inbounds for t in 1:n
edges[star[t]].o_next = star[t == n ? 1 : t + 1]
end
return nothing
end
41 changes: 41 additions & 0 deletions src/methods/clipping/overlayng/intersection_point_builder.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# # IntersectionPointBuilder — result points (port of JTS `IntersectionPointBuilder`)
#
# Phase 2b of the OverlayNG port (design doc §3). A port of
# `operation/overlayng/IntersectionPointBuilder.java`: extracts `Point` results
# from an Intersection of non-point inputs, at nodes incident on edges of BOTH
# inputs where none of the incident edges is itself in the result (i.e. the
# inputs meet at an isolated point). `isAllowCollapseLines` is always true here
# (non-strict), so the boundary-collapse guard in `isEdgeOf` is elided.
#
# Returns the result points as coordinate tuples; the driver wraps them. Nothing
# here is exported.

function _build_points(g::OverlayGraph)
edges = g.edges
points = Tuple{Float64, Float64}[]
for ne in graph_node_edges(g)
if _is_result_point(edges, ne)
push!(points, node_point(g.arr, he_origin(edges, ne)))
end
end
return points
end

# Port of `isResultPoint`: a node incident on edges of both inputs, none of them
# in the result.
function _is_result_point(edges, node_edge::Integer)
is_edge_of_a = false
is_edge_of_b = false
e = Int32(node_edge)
while true
oe_in_result(edges, e) && return false
lbl = oe_label(edges, e)
is_edge_of_a |= _is_edge_of(lbl, 0)
is_edge_of_b |= _is_edge_of(lbl, 1)
e = he_onext(edges, e)
e == node_edge && break
end
return is_edge_of_a && is_edge_of_b
end

@inline _is_edge_of(lbl::OverlayLabel, i::Integer) = is_boundary(lbl, i) || is_line(lbl, i)
77 changes: 77 additions & 0 deletions src/methods/clipping/overlayng/line_builder.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# # LineBuilder — result lines from the overlay graph (port of JTS `LineBuilder`)
#
# Phase 2b of the OverlayNG port (design doc §3). A port of
# `operation/overlayng/LineBuilder.java`, restricted to the raw-edge extraction
# JTS actually uses (`addResultLines`); the merged path (`addResultLinesMerged`)
# is marked NOT USED in JTS and is skipped. Strict-mode branches are dropped —
# this engine runs the original (non-strict) JTS semantics, so
# `isAllowMixedResult` and `isAllowCollapseLines` are always true.
#
# Each `OverlayEdge` is a single segment, so every result line is one two-point
# `LineString` (the raw noded-edge output; JTS emits the same edges, just as
# possibly-longer chains). Everything here is internal — nothing is exported.

# Build the result lines from the graph (port of `getLines`: `markResultLines`
# then `addResultLines`).
function _build_lines(m::Manifold, g::OverlayGraph, input, has_result_area::Bool,
op::_OverlayOpCode; exact)
edges = g.edges
area_index = _input_area_index(input)
#-- markResultLines
for i in eachindex(edges)
oe_in_result_either(edges, i) && continue
if _is_result_line(oe_label(edges, i), op, has_result_area, area_index)
oe_mark_in_result_line!(edges, i)
end
end
#-- addResultLines (raw noded edges)
lines = Vector{typeof(_edge_line(g, 1))}()
for i in eachindex(edges)
oe_in_result_line(edges, i) || continue
oe_is_visited(edges, i) && continue
push!(lines, _edge_line(g, i))
oe_mark_visited_both!(edges, i)
end
return lines
end

# The `LineString` of one result edge (a single segment, in the half-edge's
# direction).
_edge_line(g::OverlayGraph, i::Integer) =
GI.LineString([node_point(g.arr, he_origin(g.edges, i)),
node_point(g.arr, he_dest(g.edges, i))])

# Port of `isResultLine`. `is_allow_collapse_lines` / `is_allow_mixed_result`
# are always true here (non-strict), so those guards are elided.
function _is_result_line(lbl::OverlayLabel, op::_OverlayOpCode, has_result_area::Bool,
area_index::Integer)
#-- a boundary of a single geometry is only in the result as part of an area
is_boundary_singleton(lbl) && return false
#-- a collapse interior to its parent area (narrow gore / hole spike)
is_interior_collapse(lbl) && return false

if op != OVERLAY_INTERSECTION
#-- collapsed edge in the other area's interior
is_collapse_and_not_part_interior(lbl) && return false
#-- a line inside the result area is subsumed by it
if has_result_area && area_index >= 0 && is_line_in_area(lbl, area_index)
return false
end
end

#-- touching area boundaries produce a line for Intersection (mixed result)
op == OVERLAY_INTERSECTION && is_boundary_touch(lbl) && return true

#-- otherwise, the op boolean logic over the effective line locations
a_loc = _effective_location(lbl, 0)
b_loc = _effective_location(lbl, 1)
return _is_result_of_op(op, a_loc, b_loc)
end

# Port of `effectiveLocation`: line and collapse edges report INTERIOR so the op
# logic can include them where warranted.
@inline function _effective_location(lbl::OverlayLabel, gi::Integer)
is_collapse(lbl, gi) && return LOC_INTERIOR
is_line(lbl, gi) && return LOC_INTERIOR
return get_line_location(lbl, gi)
end
Loading
Loading