A C++ port of GOPack (Collins, Orick, Stephenson, 2017) -- MATLAB software for computing maximal circle packings with very large numbers of circles -- structured to build as a native library for Java (via JNI) and as standalone Windows/macOS executables.
License: GPL-3.0, same as the original GOPack (this is a derivative work; see LICENSE.md).
This is a direct, line-by-line port, not a reimplementation from the
paper's description. The translation was done by reading every .m file in
code/@GOPacker/ and code/ and translating each one into C++ against the
same field names, method names, and loop structure, specifically so the two
can be diffed and checked against each other rather than trusted on faith.
See the module-level comment in core/include/gopack/Packer.h for the
indexing convention (1-indexed, matching MATLAB, to keep every loop bound
identical to the source).
Fully ported and covered by tests (gopack::Packer):
readpack-- reads the*.pFLOWERS format (docs/GO_Formats.txt)loadComplex-- not part of the original MATLAB source; a new entry point that ingests an already-known combinatorial complex directly from memory (nodeCount + per-vertex flower lists + geometry, matching the *.p FLOWERS format's own fields but as arrays instead of parsed text), for callers that already hold the triangulation in memory instead of a file (e.g. the JNI bridge'scomputeMaximalPackingFromComplex, see "Java usage" below). Implemented as the same post-parse logic readpack() itself uses (factored out so the two can't silently diverge), so a complex loaded either way behaves identically from that point on.complex_count,indxMatrices,FarVertparse_triangles-- ingest a bareNx3triangle-list (as produced by a Delaunay/convex-hull step, e.g. therandTriangulationfamily) directly, building flowers/orientation/alpha from scratch, without going through the*.ptext format.pruneComplex-- remove orphan vertices (cut off from the interior component) after such a triangulation, needed for bounded (non-convex) regions.rand_bdry_pts-- pickMpoints uniformly at random by arc length along a closed polygonal path (gopack::geom::randBdryPtsinGeometry.h; the only piece of therand*/Triangulationfamily with no external Delaunay/convex-hull dependency). See "Notes onparse_triangles/pruneComplex/rand_bdry_pts" below for the handful of deliberate deviations from the literal source.- Mode 1 (maximal packing) of the disc/plane, or the sphere if the
complex has no boundary:
setModemode 1,layoutBdry/setHoroCenters - Mode 2 (polygonal / rectangle packing):
setModemode 2 (corner selection -- explicit, fromvlist, or pseudo-random),layoutBdrydispatching tosetPolyCenters(general n-gon) /setRectCenters(automatic special case for 4 right-angle corners), andgetAspect. The sharedcontinueRiffle/layoutCenters/setEffectiveiteration below needed no changes to support this --setEffectivealready branches on the sign ofvAims, whichsetModesets appropriately for either mode. - the core iteration (used by both modes):
continueRiffle/layoutCenters(the sparse linear solve) /setEffective/updateVdata/visualErrors riffle,reapResults,angsumErrors,packStatuswritepack/writeEucl, including hyperbolic and spherical output conversion (e_to_h_data,h_to_e_data,e_to_s_data,s_to_e_data,sph_tangent,affineNormalizer,Centroid,loadTangency)cosAngle,cosCornergopack::geom::delaunayPlane/convexHull3(core/src/RandomGen.cpp) -- not part of the original MATLAB source; thin C++ bindings to the vendored Triangle (plane constrained Delaunay) and Qhull (3D convex hull) libraries (see "Vendored third-party libraries" below), doing for C++ what MATLAB's built-indelaunayTriangulation/convhullndo forrandTriangulation.m. Themselves fully tested (tests/test_random_gen.cpp).- Random triangulation generation --
randTriangulation(asgopack::geom::randTriangulationSphere/randTriangulationPlane,core/src/RandomGen.cpp, built directly ondelaunayPlane/convexHull3above) andrandomDisc/randomSphere/randomRectangle/randomSquare/randomTri(asPacker::randomDisc/randomSphere/randomRectangle/randomSquare/randomTri,core/src/PackerRandom.cpp, built onparse_triangles/pruneComplexabove). Covered bytests/test_random_packers.cpp, and exposed from the standalone CLI (not just JNI, per Ken's request that the standalone executable have this capability too, independent of CirclePack) via--random-disc/--random-sphere/--random-square/--random-rectangle/--random-tri-- see "CLI usage" below.randomTri's arbitrary-polygon-region overload is also exposed via JNI ascomputeRandomTri-- see "Java usage" below. See "Notes on therandTriangulationfamily" below for the deliberate deviations from the literal source (two bug fixes, and one simplification enabled by Triangle's native boundary-carving).
Understood but NOT yet ported:
- The
OFFfile format fallback inreadpack(calling it prints a diagnostic and returns 0, rather than guessing at behavior) - The plotting method (
show, which has no headless equivalent anyway)
If your workflow needs any of the above, say so -- the source for all of
them has already been read and understood (see the corresponding .m
files), so porting them is a bounded follow-up, not new research.
setMode's C++ signature can't distinguish "corner list omitted" from "corner list explicitly passed as empty" the way MATLAB'snargincan; an emptycrnshere always falls through to thevlist-based or pseudo-random corner selection, which is the more useful default for a caller (see the doc comment onPacker::setMode).setMode.m's "putcornangsin counterclockwise order" block recomputes a localcornersvariable using an unsortedbdryIndx(itssort(bdryIndx);call never captures a return value) and that recomputed value is never read again -- it's dead code in the source, and was omitted rather than ported literally, since porting genuinely dead code (with no observable effect either way) would just be noise.setRectCenters.mhas a comment describing corners at "lowerleft (-aspect,-1), upper right (aspect,1)" that doesn't match the actual corner coordinates used in the same function (real part in{+1,-1}, imaginary part in{+aspect,-aspect}); ported literally as written, not as commented, with a note in the code.setMode(2, ...)always resetshesto Euclidean, even if the packing was originally read as hyperbolic or spherical --setMode.mdoesn't do this. GOPack always computes internally in euclidean coordinates regardless ofhes(seeGeometry.h);hesonly controls whetherreadpack()/writepack()convert to/from hyperbolic or spherical circle data at the file boundary. A polygon/rectangle boundary is inherently a euclidean shape, so leavinghesat its original (hyperbolic/spherical) value would makewritepack()apply a conversion to already-euclidean polygon output that was never intended for it.
parse_triangles.m's own alpha-auto-selection fallback (used when no alpha was already set before the call) indexes itsutilFlagboundary-seed array using the new (renumbered) vertex numbers as if they were the old ones -- only actually correct when the input's vertex numbering is already contiguous from 1 (nodeCount == top), which is true for this function's real callers (fresh Delaunay/hull output always numbers its points contiguously). Ported literally, since "fix" here would mean guessing at an unintended generalization rather than correcting a clear bug -- if a future caller passes genuinely sparse vertex numbers, alpha selection may pick a less-than-ideal (but still valid;complex_count()independently re-validates alpha regardless) vertex.pruneComplex.mreadsobj.vlist(j)afterobj.vlistwas just cleared two lines earlier in the source -- real undefined behavior (out-of-bounds access) in a literal C++ port, not just a MATLAB quirk to preserve. Fixed to read from a saved local copy of the pre-clearvlistinstead.pruneComplex.mhasorigCenters(nv)=obj.origCenters(nv), almost certainly a typo forobj.origCenters(v)-- it should read from the OLD numbering, exactly like theorigRadiiline right above it and every other line in that loop. Ported as the evident intent (a literal port would silently read whichever old-numbered vertex happens to sharenv's new index -- wrong data, not just a crash).pruneComplex.m's localv2indx/indx2vvariables are renamedoldToNew/newToOldin the port: an unqualifiedv2indxinside aPackermember function would otherwise mean the realPacker::v2indxmember, which serves an unrelated purpose (sparse-matrix layout indexing, set up byindxMatrices()) -- MATLAB'sobj.v2indxvs. a same-named local are different namespaces, but C++ has no such distinction.rand_bdry_pts.m's auto-close check isabs(diffX)>0.001 AND abs(diffY)>0.001(not OR) -- so a path whose first and last points coincide in exactly one coordinate (e.g. two corners of an axis-aligned rectangle) is not auto-closed. Ported literally as a preserved quirk, not a typo fix, since it's a plausible (if debatable) deliberate choice rather than an unambiguous slip; seetests/test_rand_bdry_pts.cppfor a worked example of what does and doesn't trigger it.randBdryPtsadds one small defensive guard beyond the literal source: the interpolation ratio is taken as 0 (rather than dividing by zero) if two consecutive path points coincide exactly, and the forward arc-length scan is bounds-checked against running past the last segment -- both latent (if practically unreachable) issues in the MATLAB original too.
- Simplification enabled by Triangle's native boundary carving:
randTriangulation.m's plane-region case runs ~90 lines of its own manual post-hoc trimming (corner-convexity checks, then discarding boundary-only and out-of-region faces) afterdelaunayTriangulation(X,Y,C), because MATLAB's triangulator only enforces constraint edges as present -- it doesn't exclude a non-convex region's exterior on its own. Triangle does this natively (passing a closed segment loop without the-cswitch makes it discard everything outside the segment-bounded region, concavities included), so none of that manual trimming logic is ported --gopack::geom::randTriangulationPlane(RandomGen.cpp) relies ondelaunayPlane's constrained mode directly. Verified against a non-convex L-shaped region using its own true vertices as the boundary (checked by total area, confirming the missing corner isn't filled in) in bothtests/test_random_gen.cpp(the rawdelaunayPlanebinding) and ASan/UBSan-instrumented sandbox validation of the fullrandTriangulationPlaneorchestration. - Bug fix:
randTriangulation.m's (andrandomDisc.m's own separate copy of the same loop's) rejection-sampling safety counter is only incremented on a successful hit, never on a rejected attempt -- so a region with low acceptance probability (e.g. a thin sliver) can spin the real MATLAB loop indefinitely, since the cap it's compared against can never actually be reached. Fixed to increment on every attempt; verified in sandbox validation that a deliberately thin sliver polygon with a large point request returns promptly rather than hanging. - Bug fix:
randomTri.msets its localGOPacker'salphato-1before callingrandTriangulation, and only overwrites it ifrandTriangulationreturns a positivealpha(i.e. a validcentwas placed) -- butparse_triangles.m's own alpha-auto-selection only triggers onalpha==0, not on a negative value, so a plain call withoutcent(or with acentoutside the region) hands back aPackerwith an unresolved, invalidalpha=-1.randomRectangle.m, which has the same "centmay land outside the region" situation, avoids this because it never sets itsGOPacker'salphaaway from its constructor default (0) in that case.Packer::randomTri's plane-region overload leavesalphaat itsPacker()-default 0 instead, letting the existing auto-selection produce a valid vertex. - Caveat inherited unchanged from
randTriangulation.m(not a port bug, and not something Triangle's native carving above fixes or could fix): the boundary segmentsrandTriangulationPlanetriangulates against -- and the source's owninpolygonrejection-sampling checks -- are built frombdryNsampled points chord-connected in arc-length order, not fromgraph's own vertices directly, in both the C++ port and the original MATLAB (rand_bdry_pts/randBdryPts's output feeds both). The triangulated region only converges tograph's true shape asbdryNgrows; for a sparsebdryNrelative to a concave shape's feature size, the inscribed chord polygon can visibly shortcut a notch, and any point that ends up outside that chord polygon (despite being inside the truegraph) is correctly excluded from the triangulation -- discovered and quantified during this port's sandbox validation (seeRandomGen.h's doc comment onrandTriangulationPlanefor the full explanation and a worked example). This only matters forPacker::randomTri's generic plane-region overload with a caller-supplied concavegraphand a smallbdryN;randomDisc/randomSphere/randomRectangle/randomSquare(the CLI-exposed generators) never hit it, since a circle and a rectangle have no concave features to shortcut.Packer::randomRectanglealso callspruneComplex()(matchingrandomRectangle.m) specifically to clean up any point this does affect;Packer::randomTri's plane overload does not (matchingrandomTri.m), so a caller who wants that cleanup there should callpruneComplex()/indxMatrices()on the result themselves.
reapResults()now recenterscenters/radiifor Spherical packings (the same affine-normalizationaffineNormalizer/centroidinGeometry.cppcompute, moving the tangency-point centroid to the origin in 3D) -- not part ofreapResults.m, which has no such step.writepack()'s Spherical branch used to be the only place this normalization happened, which meant any caller readingPacker::centers/radiidirectly afterriffle()instead of going throughwritepack()-- notably the JNI bridge'scomputeMaximalPackingFromComplex, which never callswritepack()at all -- got an un-normalized, possibly lopsided packing. Every caller now gets a normalized packing for free as soon asriffle()returns.writepack()still does its ownaffineNormalizercall too, on purpose: it's the only safety net for a caller who callswritepack()on a Spherical packing that was justreadpack()/loadComplex()-loaded but neverriffle()d (soreapResults()never ran).affineNormalizeris idempotent on an already-centered input -- its very first centroid check is already within tolerance, so it returns the identity transform (A=1, B=0) immediately -- so this costs one cheap redundant pass over the tangency points on the normal riffle-then-write path, not a second real optimization.
- Ten regression tests (
tests/test_hex_flower.cpp,tests/test_readpack_roundtrip.cpp,tests/test_polygonal.cpp,tests/test_loadcomplex.cpp,tests/test_sphere_normalize.cpp,tests/test_parse_triangles.cpp,tests/test_prune_complex.cpp,tests/test_rand_bdry_pts.cpp,tests/test_random_gen.cpp,tests/test_random_packers.cpp) exercise the pipeline on the classical "hex flower" complex (one interior vertex of degree 6 ringed by 6 boundary vertices) in both modes, checking angle-sum convergence and symmetric radii for mode 1, a read/riffle/write/re-read round trip, that a 4-corner rectangle layout (mode 2) stays finite, positive, and rectangle-shaped after riffling, thatloadComplex()produces a packing that agrees with a hand-built (readpack()-equivalent)Packerto within 1e-9 -- including its alpha-resolution and optional-radii/vAims-override paths -- thatPacker::centers/radiifor a real 1000-vertex spherical triangulation (tests/data/sphtest1000.p) are already centroid-normalized right afterriffle()with no call towritepack(), thatparse_trianglescorrectly reconstructs a hex-flower fan and a closed tetrahedron (Spherical, with the 3-vertex pseudo-boundary anchor trianglecomplex_count.malways gives a boundary-less complex) from a bare triangle list, thatpruneComplexremoves an orphan vertex deliberately attached via a "flap" face and leaves a still-convergent packing behind, thatrand_bdry_pts's returned points all lie exactly on the source polygon's boundary (including its auto-close path), thatdelaunayPlane/convexHull3(the vendored Triangle/Qhull bindings) produce a correct constrained triangulation of a non-convex L-shaped region (checked by total area, confirming the missing corner wasn't filled in) and a correct 3D convex hull of points on a sphere (checked against Euler's formula,F = 2V-4, for a 200-point case), and that all fivePacker::random*generators produce valid, riffle-convergent packings with the righthes/mode/alpha/corner metadata -- including a specific regression check that therandomTrialpha=-1bug fix above actually produces a valid vertex number, not the source's unresolved-1. - The vendored Triangle/Qhull integration, and the geometric-primitive
layer built directly on it, was actually compiled and run in this
sandbox (unlike most of the rest of this port, which is syntax/logic-
checked in isolation but only really build-tested on Ken's Windows
machine): both libraries' real vendored source, plus
gopack::geom::delaunayPlane/convexHull3/randTriangulationSphere/randTriangulationPlane/pointInPolygon(RandomGen.cpp) calling into them, were built with-fsanitize=address,undefinedand exercised against real geometry (the L-shape and sphere cases above, plus an octahedron, unconstrained square+center-point case, and -- specifically for this session's new orchestration functions -- a dense-boundary-sampling convergence check, acent-placement/rejection check, and a thin-sliver no-hang check for the rejection-sampling bug fix above) with no sanitizer reports. ThePacker-levelrandomDisc/randomSphere/randomRectangle/randomSquare/randomTrigenerators (PackerRandom.cpp) were compile- checked for real against this sandbox's minimal Eigen stand-in (same as the rest ofPacker, see below), and their Eigen-independent inner logic --parseTriangles()/pruneComplex(), which is what these generators actually feed their triangulations through -- was additionally compiled, linked, and run for real against the new orchestration functions' actual output (not synthetic input), which is how the boundary-chord-approximation caveat documented above was discovered and quantified in the first place. This is a materially stronger verification bar than the "compiles cleanly against a minimal Eigen stand-in" level the rest of this port gets in-sandbox, since none of this layer depends on Eigen at all. - Mode 1 has been built and run successfully on real Windows hardware,
including on genuinely large inputs (up to ~500,000 vertices from
GOPack/data/lace500000_K.p), producing packings that loaded correctly in CirclePack. Along the way this caught and fixed two real bugs that static review alone had missed: a Windows text-modeifstreamtellg()/seekg()desync in theALPHA/GAMMA:parser (fixed by opening the file in binary mode -- see the comment onPacker::readpack), and anEigen::SparseLU/BiCGSTABsolver-strategy default that assumed a symmetric system where GOPack'stransMatrixisn't one. - Mode 2 (polygonal/rectangle) has been validated with a standalone ASan/UBSan-instrumented harness against the hex-flower fixture (4-corner rectangle, 6-corner hexagon, 3-corner triangle, and auto-detected-corner cases all produced finite, geometrically sane output with no sanitizer reports), but has not yet been exercised through a real build with a full riffle loop the way mode 1 has -- that's the natural next step once this reaches your machine.
- Numerical agreement with the original MATLAB has not been checked.
That requires running both implementations on the same input and diffing
radii, which needs a MATLAB (or Octave) environment this session doesn't
have. Recommended next step: run
GOPacker.readpack(...); GOPacker.setMode(1); GOPacker.riffle(50);in MATLAB on one of thedata/*.pfiles and compare the resultingobj.radiiagainst this port'sgopack_clioutput on the same file.
GOPack-cpp vendors (commits directly into the repo) two small third-party C
libraries under third_party/, used by the random-triangulation
generation family (gopack::geom::delaunayPlane/convexHull3/
randTriangulationSphere/randTriangulationPlane and
Packer::randomDisc/randomSphere/randomRectangle/randomSquare/
randomTri -- see "Fully ported and covered by tests" above):
- Triangle (Jonathan
Richard Shewchuk, v1.6, 2005) -- plane constrained Delaunay triangulation,
what
randTriangulation.m's plane-region case uses MATLAB'sdelaunayTriangulation(X,Y,C)for. - Qhull (v2020.2,
src/libqhull_ronly) -- 3D convex hull, whatrandTriangulation.m's sphere case uses MATLAB'sconvhullnfor.
See third_party/triangle/README.md and third_party/qhull/README.md for
exactly what's vendored (a deliberately minimal subset of each upstream
project -- not the CLI frontends, GUI tools, or docs), what license terms
apply, and how core/src/RandomGen.cpp calls into each.
Why vendored (committed) rather than fetched at build time, unlike
Eigen above: Eigen is a large, actively-maintained project with a fast git
host, so FetchContent-ing it on demand (falling back from a local
checkout) is a reasonable trade of repo size for staying current. Triangle
and Qhull are the opposite case -- Triangle hasn't been updated since 2005
and has no official git repository at all, and both are small enough
(under 2MB combined) that committing them costs nothing meaningful. Ken's
call: a working executable that builds identically offline, regardless of
whether cs.cmu.edu or github.com happen to be reachable (or whether
either project is still there at all) beats a network dependency on two
old, low-traffic upstream projects, and license terms aren't a concern for
this project's own (research/academic) use either way. Once vendored,
GOPACK_BUILD_RANDOM_GEN=OFF remains available for anyone who'd rather not
build them at all.
Requires CMake 3.18+ and a C compiler (for the vendored Triangle/Qhull
libraries) and C++17 compiler. Eigen is vendored via a local checkout at
./eigen if present (fully offline after that one-time clone), falling
back to CMake FetchContent (needs network access) if ./eigen doesn't
exist -- Triangle and Qhull, by contrast, are committed directly into the
repo under third_party/ and never touch the network at all (see "Vendored
third-party libraries" above). A JDK (JAVA_HOME set) is needed only for
the optional JNI target.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure
Build options (pass as -D<OPTION>=OFF to disable):
GOPACK_BUILD_CLI(default ON) -- thegopackcommand-line executableGOPACK_BUILD_JNI(default ON, skipped automatically if no JDK is found) --gopack_jni.dll/libgopack_jni.dylibfor the Java bridge (JNI.GOPackNative, injni/java/; packageJNI, notorg.kensmath.gopack, since 8/2026 -- see that class's doc comment)GOPACK_BUILD_TESTS(default ON)GOPACK_BUILD_RANDOM_GEN(default ON) -- build the vendored Triangle/Qhull libraries and the whole random-triangulation generation family built on them (gopack::geom::delaunayPlane/convexHull3/randTriangulationSphere/randTriangulationPlaneandPacker::randomDisc/randomSphere/randomRectangle/randomSquare/randomTri-- see "Vendored third-party libraries" above). Disabling this also disablestests/test_random_gen.cpp/tests/test_random_packers.cppand the CLI's--random-*flags (see "CLI usage" below);GOPACK_HAVE_RANDOM_GENis defined for the rest of the codebase to#ifdefaround when this is off.GOPACK_USE_SUITESPARSE(default OFF) -- swap Eigen's built-in sparse solvers for SuiteSparse/CHOLMOD; not wired up yet (Eigen was the chosen default for portability), butcore/include/gopack/SparseLinearSolver.his the seam where that would plug in
gopack input.p -o output.p [--passes 200] [--eucl-out]
gopack input.p -o output.p --polygon [--corners v1,v2,v3,v4] [--angles a1,a2,a3,a4]
gopack --random-disc N -o output.p [--passes 200] [--eucl-out]
gopack --random-sphere N -o output.p [--passes 200]
gopack --random-square N -o output.p [--passes 200] [--eucl-out]
gopack --random-rectangle N[,aspect[,bdryN]] -o output.p [--passes 200] [--eucl-out]
gopack --random-tri intN,bdryN --graph x1,y1,x2,y2,... -o output.p [--cent cx,cy]
[--passes 200] [--eucl-out]
--polygon switches to mode 2 (polygonal/rectangle packing). --corners is
a comma-separated list of 1-indexed boundary vertices, in counterclockwise
order, to use as polygon corners; if omitted, corners are inferred from the
input file's VERT_LIST: or chosen pseudo-randomly (mirroring
setMode.m's own fallback behavior). --angles gives matching target
corner angles in radians; if omitted, all corners get equal angles (e.g.
exactly pi/2 each for a 4-corner input, which is what triggers the
rectangle-specific layout in setRectCenters). With exactly 4 corners, the
CLI also prints the resulting aspect ratio (getAspect).
--random-disc/--random-sphere/--random-square/--random-rectangle
generate a fresh "geometrically random" triangulation (see
Packer::randomDisc/randomSphere/randomSquare/randomRectangle above)
instead of reading <input.p> -- these are mutually exclusive with the
positional <input.p> argument and with each other, and are only available
in a build with GOPACK_BUILD_RANDOM_GEN on (the default). --random-square
and --random-rectangle already leave the generated packing in polygonal
mode with 4 corners chosen automatically, so --polygon/--corners/
--angles are ignored for those two. --random-rectangle takes a single
comma-separated argument: N (interior point count) is required; aspect
(default 1) sets the rectangle to [-aspect,aspect]x[-1,1]; bdryN (default
computed from N and aspect, matching randomRectangle.m) overrides the
boundary point count.
--random-tri intN,bdryN generates a random triangulation of an arbitrary
closed polygonal region -- not just a disc/square/rectangle -- via
Packer::randomTri(intN, bdryN, graph, cent). The boundary polygon is given
with the required --graph x1,y1,x2,y2,... flag: a flat x,y coordinate list,
at least 3 points, in order around the boundary (don't repeat the first
point at the end). --cent cx,cy optionally names a point inside --graph
to use as the packing's alpha (centering) vertex; if omitted, or the point
isn't actually inside the boundary, alpha is chosen automatically. Unlike
--random-square/--random-rectangle, this stays in max-pack mode (mode 1)
-- a generic region has no "corners" concept -- so --polygon/--corners/
--angles still apply afterward if you want polygonal mode on the result.
Two ways to compute a maximal packing (mode 1) from Java, depending on
whether your data starts out in a *.p file or already in memory:
// From a file:
double[] radii = JNI.GOPackNative.computeMaximalPacking(
"input.p", /* geometryHint (reserved) */ 0, /* tolerance (reserved) */ 0.0,
/* maxPasses */ 200);
// From an in-memory complex (e.g. CirclePack's own per-vertex flower data):
int[][] flowers = new int[nodeCount + 1][]; // flowers[0] unused
// ... fill flowers[1..nodeCount], CLOSED (first==last) for interior
// vertices, OPEN (first!=last) for boundary vertices ...
double[] radii2 = JNI.GOPackNative.computeMaximalPackingFromComplex(
nodeCount, flowers, /* geometry: 0=eucl -1=hyp +1=sph */ 0,
/* tolerance (reserved) */ 0.0, /* maxPasses */ 200);A third method, computeRandomTri, generates a random triangulation of an
arbitrary closed polygonal region (the JNI counterpart to the CLI's
--random-tri) and computes its maximal packing in one call, and a fourth,
computeRandomDisc, does the same for the unit disc (the JNI counterpart to
--random-disc):
double[] graphXY = { 0,0, 4,0, 4,3, 0,3 }; // a 4x3 rectangle, as an example
JNI.RandomComplexResult result =
JNI.GOPackNative.computeRandomTri(
/* intN */ 40, /* bdryN */ 20, graphXY,
/* centX, centY, hasCent */ 0.0, 0.0, false,
/* maxPasses */ 200);
// or: JNI.RandomComplexResult result = JNI.GOPackNative.computeRandomDisc(
// /* n */ 200, /* maxPasses */ 200);
// result.nodeCount, result.flowers, result.radii, result.centersRe/centersIm,
// result.geometry, result.alpha, result.gamma are all populated (1-indexed,
// index 0 unused, same convention as radii/radii2 above) -- unlike
// computeMaximalPacking[FromComplex], the caller didn't supply the
// combinatorics, so the whole generated complex comes back, not just radii.radii/radii2 both have length nodeCount+1, and radii[v] is vertex
v's euclidean radius for v = 1..nodeCount -- matching GOPack/CirclePack's
own 1-indexed vertex numbering (the same convention used throughout the C++
core), rather than shifting to a "natural" 0-indexed Java array. radii[0]
is unused. Load gopack_jni.dll / libgopack_jni.dylib via
java.library.path, or bundle both platform binaries and pick one at
runtime based on os.name/os.arch (see the class doc comment in
GOPackNative.java).
Prefer computeMaximalPackingFromComplex over computeMaximalPacking
whenever the triangulation already exists in memory on the Java side (as
it will for a CirclePack caller). computeMaximalPacking still has to open
and parse a *.p file on the C++ side (Packer::readpack), which for large
complexes can easily take longer than the packing computation itself --
that text parsing is real work regardless of whether it happens in a
subprocess or in-process. computeMaximalPackingFromComplex goes straight
to Packer::loadComplex (core/src/PackerIO.cpp), skipping both the
Java-side serialization to text and the C++-side parsing back out of it, so
its cost is close to the packing computation alone. computeMaximalPacking
remains the right choice when the data genuinely starts out as a file (a
*.p on disk with no in-memory representation yet).
Only mode 1 (maximal packing) is exposed through this JNI bridge so far;
mode 2 (polygonal/rectangle) is ported in the C++ core (both readpack()
and loadComplex() load a complex the same way regardless of which mode you
later select with setMode) but only reachable today via the CLI's
--polygon flag -- adding computeMaximalPackingFromComplex's mode-2
counterpart is a small follow-up whenever you need it (same loadComplex
plumbing, just setMode(2, corners, angles) instead of setMode(1) before
riffle).