Changelog
Source:NEWS.md
wkpool 0.4.0
New vignette “Triangulation”: ear cutting with
pool_triangulate(), the PSLG route (as_pslg()withpool_holes()) to RTriangle, and taking its triangles back onto a vertex pool withnew_wkpool_tri(), on silicate’sinlandwaters. The other vignettes now cover periodic x,wkpool(vertices, segments)and triangle pools, and point to it.pool_holes()returns a point inside each hole even when the hole is not convex: the centroid of an ear of the hole ring, clear of every pool vertex, instead of the mean of the ring’s vertices, which could fall outside the hole and make RTriangle erase the polygon around it (oninlandwatersthis lost most of the area). Holes filled exactly by another polygon of the pool (enclaves) are no longer returned, since they are not holes of the coverage.plot()forwkpool_triacceptsxlabandylab, and sets the plot extent from the triangles drawn rather than the whole vertex pool, so a subset is drawn at its own extent.New
plot()method forwkpool_tri: onepolygon()call indexing the vertex pool, filled by feature (40,000 triangles draw in about 0.4 s to png, where casting to wk polygons and plotting those took over a minute).
Triangles (experimental)
New, experimental
wkpool_triclass: a vector of triangles (.vx0,.vx1,.vx2,.feature,.part) on the same kind of vertex pool as a wkpool.pool_triangulate(x, engine = "decido")ear-cuts each polygon of a pool using its path provenance; ear cutting creates no vertices, so the vertex table is passed through unchanged.merge_coincident()accepts awkpool_tri(triangulate first, merge later gives the same result as merging first).pool_triangles()andtri_segments()give the triangle table and the triangle edges as a wkpool;wk_handle()presents one POLYGON per triangle, with crs and geodesic carried. ‘decido’ is suggested.wkpool_triis experimental and may change in a later release. In particular the part is stored as a.partfield on each triangle (the simplest option); the alternative is a.pathfield pointing at the outer ring’s path, recovering feature and part frompool_paths()as segments do.
Periodic x (antimeridian)
-
merge_coincident()andtopology_report()gainperiod_x(defaultNULL, behaviour unchanged). Withperiod_x = 360, vertices are keyed onx %% 360, so the two copies of a +-180 seam fuse (and 0 to 360 data fuses at 0 and 360); after merging, the seam is an ordinary shared boundary forfind_shared_edges(),find_internal_boundaries()and quotient arcs. The period is never inferred from the crs. Coordinates are not moved, so geometry emitted from a fused pool jumps by one period across the seam. With a tolerance, x is wrapped, snapped, then wrapped again (#3).
Constructors and naming
wkpool(vertices, segments)is now exported and documented: the user-level constructor from a vertex table and a segment table, as returned bypool_vertices()andpool_segments()(#2).New
pool_holes(), the same function ashole_points()under a name that matches the otherpool_*()accessors.hole_points()is kept (#1).
Performance
cycles_to_wkb()/cycles_to_wkt()build all polygons in onewk::wk_polygon()call and all multipolygons in onewk::wk_collection()call, instead of looping over features and parts (10,000 hexagons: 5.7 s to 0.16 s). The fallback for pools without path provenance matches cycle edges to segments with onevctrs::vec_match()instead of a scan per cycle. Output is unchanged.establish_topology()andmerge_coincident()no longer build hash tables to look up vertex ids: pools they mint have.vx = 1..n, where an id is its own position, so validation innew_wkpool()and the segment remap use it directly (other pools fall back tomatch()). On 1M line coordinatesmerge_coincident()drops from 0.35 s to 0.12 s, and the vertex pipeline is now faster than silicate’sPATH0()on every benchmark input (seebench/README.md).find_cycles(),classify_cycles(),hole_points()andfind_neighbours()no longer scale quadratically: on 10,000 hexagonsfind_neighbours(type = "edge")drops from 19 s (13 GB allocated) to 0.03 s andclassify_cycles()from 8.3 s to 0.23 s.find_shared_edges(),find_internal_boundaries()andtopology_report()key edges with vctrs instead ofpaste(). Results are unchanged, except thatfind_neighbours()output no longer carries a strayout.attrsattribute and thefeaturescolumn offind_shared_edges()is a plain named list.bench/holds a benchmark against silicate (not part of the built package).
Quotient boundary semantics
vertex_degree(),find_nodes(),find_arcs(),as_arcs(),arc_node_summary(),arcs_to_wkt()andarcs_to_wkb()gain aquotientargument (default FALSE, behaviour unchanged). In the quotient graph, duplicated shared boundaries - the same undirected vertex pair carried by more than one feature - count once: the first occurrence keeps its direction, multiplicity is discarded whatever its count. A vertex interior to a shared boundary has quotient degree 2 (the degree-2 invariant holds at this level, including for vertices minted by densification), true junctions remain nodes, andfind_arcs(x, quotient = TRUE)yields the TopoJSON-style arc decomposition of a polygon layer: each shared border once, not once per feature.Characterization guards pin the default duplicated-boundary and antimeridian-seam semantics as explicit contracts: boundary-interior vertices are degree-4 by default, seams of a +-180 split stay distinct through merge, and merge keys are exact coordinates with no wrapping of any kind.
Compiled arc kernel (first src/)
find_arcs()now runs on a cpp11 kernel (wkpool’s first compiled code, LinkingTo: cpp11 only, no system requirements). The algorithm, adjacency ordering, node ordering and output are identical to the pure-R implementation, which is retained as the executable specification (find_arcs_walk_r, internal) and pinned to the kernel by conformance tests across nodes, loops, open lines, holes, grids, subsets and shuffled pools.Scale: the pathological all-nodes grid at 2.56 million segments runs in about 1 second (was ~10x that in R after the linear rewrite, and unboundedly worse before it); 2 million segments of long arcs walk in under a second, with remaining time dominated by the R-side dense-id mapping. Real-world effect: the CGAZ world admin boundaries (~10M segments) drop from ~44s to a few seconds.
Exact coordinate identity and linear internals
merge_coincident()now groups vertices on the double-precision bit pattern (viavctrs::vec_group_id()) instead of formatted text keys. This fixes a silent correctness bug: the old keys went throughas.character()at ~15 significant digits, so distinct doubles differing only beyond printable precision merged even attolerance = 0. Exact now means exact.When the pool carries
z, vertices are keyed on all coordinate columns: equal x/y with different z no longer merge silently. Tolerance is documented honestly as a snap grid (cell sizetolerance), not a distance guarantee, and canonical vertices keep their original coordinates.topology_report()computes its counts directly from vertex groups instead of performing a full internalmerge_coincident()plusfind_shared_edges().The quadratic idioms are gone:
find_arcs()uses dense integer adjacency with amortized cursors and a preallocated walk buffer (was: character-keyed lists grown withc()in loops);pool_combine()remaps by position with per-input offsets (was: character-named lookup tables and growing vectors);as_arcs()andvertex_degree()are vectorized. On a 160x160 grid of squares (102400 segments)find_arcs()drops from minutes to under half a second, and all core verbs now scale linearly.pool_compact()remap corrected for pools whose segment order does not follow pool order (latent indexing assumption).
Path provenance (.path)
establish_topology()now mints a.pathid per segment - one id per input ring or linestring - with a sidecarpathstable (.path,.feature,.part,.ring, captured fromwk::wk_coords()identifiers). Provenance survives subsetting,merge_coincident(),pool_compact()andpool_combine()(which offsets path ids to keep them unique), and is exposed via the new accessorspool_path()andpool_paths().new_wkpool()gainspathandpathsarguments.find_cycles()uses provenance when present: a cycle is a path whose segment chain closes. Rings are recovered exactly, in input order and input winding, robust to segment reordering (chains are rebuilt by connectivity), and a broken ring after subsetting is dropped rather than mis-walked. The result carries apathattribute mapping cycles to path ids. Pools without provenance keep the legacy storage-order walk.The winding-convention issue is resolved structurally: with provenance, ring roles are known (the first ring of each part is the exterior, later rings are its holes), so
classify_cycles()andhole_points()no longer guess from winding and theconventionargument is only consulted for provenance-free pools.hole_points()on a simple polygon now returnsNULLas intended.cycles_to_wkb(feature = TRUE)reconstructs original feature structure exactly when provenance is present: rings grouped into their original part (exterior first, then that part’s own holes), parts grouped into POLYGON or MULTIPOLYGON features. This replaces the heuristics (all holes with the first outer; holes dropped for multi-part features) for established pools; provenance-free pools keep the old behaviour.
Native emitters
All round-trip emitters (
arcs_to_*(),cycles_to_*(),segments_to_*()) are rebuilt on wk’s C-level construction filters (wk::wk_linestring(),wk::wk_polygon(),wk::wk_collection()). WKB is now the primary product and WKT is derived from it; no coordinate text is assembled anywhere in wkpool.Consequences: WKB output preserves coordinates at full double precision (previously text formatting perturbed doubles at around the 15th significant digit); z vertex values are carried through all emitters (previously dropped); rings are closed by wk; crs and geodesic ride along by construction. WKT output uses wk’s writer (16 significant digits, trimmed) - use the WKB emitters when bit-exact coordinates matter.
Behaviour is otherwise unchanged. (The documented simplifications in
cycles_to_wkb(feature = TRUE)were retained at this stage and have since been superseded by path provenance for established pools - see above; they still describe the provenance-free fallback.)
wk_handle: a wkpool is wk-handleable
wk::wk_handle()is implemented for wkpool: a pool presents as one LINESTRING per segment (length preserved), with crs and geodesic attached. The wk ecosystem’s generic conversions now work directly on a pool:wk::as_wkb(),wk::as_wkt(),wk::wk_coords(),wk::wk_meta(),geos::as_geos_geometry(), and anything else built onwk_handle(). Arcs and cycles remain explicit conversions.The handled path is built natively from the pool (
wk::xy()indexed by the segment table, through wk’s C-level linestring filter): no text round trip, coordinates at full double precision, XYZ when the pool carries z.
CRS round trip
A wkpool now participates in wk’s CRS propagation model. The CRS and geodesic flag of the input are captured by
establish_topology()(viawk::wk_crs()andwk::wk_is_geodesic()), carried through subsetting,merge_coincident(),pool_compact(),as_arcs()andpool_combine(), and restored onto the output ofsegments_to_wkt(),arcs_to_wkt(),cycles_to_wkt()and their WKB counterparts.Methods for
wk::wk_crs(),wk::wk_set_crs(),wk::wk_is_geodesic()andwk::wk_set_geodesic()are provided for wkpool objects. The CRS is opaque and carried, never interpreted, per wk convention.pool_combine()resolves the CRS across inputs withwk::wk_crs_output(): inherit values give way to concrete values, and combining pools with unequal concrete CRS is an error.new_wkpool()is now exported, so downstream packages that construct pools directly (e.g. bigcurve) can do so through a supported constructor that checks invariants and carries crs/geodesic.
wkpool 0.3.0
CRAN release: 2026-03-05
Removed
as_decido()which wasn’t fully worked out.Fixed
wkpool()which never worked before.Added check_wkpool helper function.
wkpool 0.2.0
Round-trip to wk
-
arcs_to_wkt()/arcs_to_wkb()— arcs become linestrings -
cycles_to_wkt()/cycles_to_wkb()— cycles become polygons, with hole nesting -
segments_to_wkt()/segments_to_wkb()— raw segments as linestrings, multilinestring, or points
Arc-node topology
-
vertex_degree()counts segments touching each vertex -
find_nodes()identifies branch points and endpoints (degree ≠ 2) -
find_arcs()extracts maximal segment sequences between nodes -
as_arcs()returns wkpool with.arccolumn for arc membership -
arc_node_summary()reports arc/node structure statistics
Arcs are the “uninterrupted lines” — sequences of segments passing only through degree-2 vertices. Useful for line simplification, network extraction, and efficient storage.
wkpool 0.1.0
Initial release. Vertex pool topology for wk-handleable geometry.
Core functions
-
establish_topology()converts any wk-handleable geometry to segments + vertex pool -
merge_coincident()discovers shared vertices (exact or tolerance-based) -
pool_vertices(),pool_segments(),pool_feature()access the structure -
pool_combine()merges multiple pools with proper index remapping -
pool_compact()removes unreferenced vertices
Topology discovery
-
find_shared_edges()finds edges shared by multiple features -
find_internal_boundaries()finds edges traversed in opposite directions (true shared boundaries) -
find_neighbours()builds adjacency from shared edges or vertices -
topology_report()summarizes vertex/edge sharing
Cycle and winding analysis
-
find_cycles()discovers closed rings from segment connectivity -
cycle_signed_area()computes signed area (shoelace formula) -
classify_cycles()identifies outer rings vs holes by winding -
reverse_cycle()flips winding direction -
hole_points()extracts hole centroids for triangulation - Convention support:
"sf"(default) or"ogc"winding rules
Triangulation export
-
as_pslg()exports to RTriangle format (P, S matrices) -
as_decido()exports to decido format (0-indexed)
vctrs integration
- wkpool is a vctrs rcrd: subset, combine operations work
-
plot.wkpool()for quick visualization