mapshaper 0.7.34 → 0.7.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mapshaper.js +221 -118
- package/package.json +1 -1
- package/www/index.html +7 -2
- package/www/mapshaper-gui.js +210 -41
- package/www/mapshaper.js +221 -118
package/www/mapshaper.js
CHANGED
|
@@ -31959,12 +31959,12 @@ ${svg}
|
|
|
31959
31959
|
type: 'flag'
|
|
31960
31960
|
})
|
|
31961
31961
|
.option('coarse-bridge', {
|
|
31962
|
-
// Undocumented:
|
|
31963
|
-
//
|
|
31964
|
-
//
|
|
31965
|
-
//
|
|
31966
|
-
//
|
|
31967
|
-
//
|
|
31962
|
+
// Undocumented: force the low-resolution concave bridge at EVERY deep
|
|
31963
|
+
// concave bend, bypassing the default's exposure gate and the loop
|
|
31964
|
+
// remover's clip budget. The guarded form is the default (2026-07-02);
|
|
31965
|
+
// this unguarded form is faster still but NOT area-safe: an exposed
|
|
31966
|
+
// bridge can dent the buffer or create spurious holes (see the caution
|
|
31967
|
+
// at makeCoarseConcaveJoin and "coarse-bridge" in buffer-line-notes.md).
|
|
31968
31968
|
type: 'flag'
|
|
31969
31969
|
})
|
|
31970
31970
|
.option('no-gap-patch', {
|
|
@@ -42059,17 +42059,15 @@ ${svg}
|
|
|
42059
42059
|
return turnPrefix[posHi] - turnPrefix[posLo];
|
|
42060
42060
|
}
|
|
42061
42061
|
|
|
42062
|
-
// Multi-pass collapse
|
|
42063
|
-
//
|
|
42064
|
-
//
|
|
42065
|
-
//
|
|
42066
|
-
//
|
|
42067
|
-
//
|
|
42068
|
-
//
|
|
42069
|
-
//
|
|
42070
|
-
//
|
|
42071
|
-
// turn gate; caps (180-degree arcs) exceed it and so are never collapsed.
|
|
42072
|
-
// @fillFloor (dip+coverage path): max winding-0 area a collapse may swallow; a
|
|
42062
|
+
// Multi-pass collapse. Iterating lets the collapse of tight inner overshoot
|
|
42063
|
+
// loops shorten the spans of the loops that wrap them, so a wrapper whose span
|
|
42064
|
+
// was too long (or too-large-turn, on the gated paths) on one pass can become
|
|
42065
|
+
// collapsible on the next -- the multi-pass "peel simple interior loops first"
|
|
42066
|
+
// idea. With dipTags supplied (clean-outline rings; the tags act as a flag,
|
|
42067
|
+
// see below) each candidate is decided by the exact coverage check plus the
|
|
42068
|
+
// opposite-wound hole guard; otherwise the source-turn or geometric turn gate
|
|
42069
|
+
// applies (maxTurn; caps' 180-degree arcs exceed it and are never collapsed).
|
|
42070
|
+
// @fillFloor (coverage path): max winding-0 area a collapse may swallow; a
|
|
42073
42071
|
// collapse filling a larger hole/notch is refused (see BUFFER_LOOP_FILL_AREA_FRAC).
|
|
42074
42072
|
// Callers pass dist^2 * BUFFER_LOOP_FILL_AREA_FRAC; defaults to the uncovered
|
|
42075
42073
|
// floor when omitted (unit tests without a buffer distance).
|
|
@@ -42078,11 +42076,32 @@ ${svg}
|
|
|
42078
42076
|
if (maxTurn === undefined) maxTurn = BUFFER_LOOP_MAX_TURN;
|
|
42079
42077
|
if (!(maxPasses >= 1)) maxPasses = 12;
|
|
42080
42078
|
if (!(fillFloor >= 0)) fillFloor = BUFFER_LOOP_CHECK_MIN_AREA;
|
|
42081
|
-
|
|
42079
|
+
// dipTags acts purely as a flag here: a ring built by the clean-outline
|
|
42080
|
+
// construction opts into the coverage-checked path. The per-vertex tag
|
|
42081
|
+
// values are not consulted (the tag-excluded turn gate was removed in favor
|
|
42082
|
+
// of the exact coverage check), so the tags are not threaded through passes.
|
|
42083
|
+
var coverage = !!dipTags;
|
|
42084
|
+
// Neighborhood clip budget, shared by all passes over this ring: each
|
|
42085
|
+
// accepted collapse's uncovered (clipped) area accumulates in a coarse
|
|
42086
|
+
// grid, and an accept that would push any touched cell past the per-collapse
|
|
42087
|
+
// floor is refused instead. Individually sub-floor clips are the design's
|
|
42088
|
+
// tolerance, but several of them can land in the SAME neighborhood (dense
|
|
42089
|
+
// fold clusters -- routine with coarse bridge geometry, theoretically
|
|
42090
|
+
// possible with overlapping same-pass collapses) and compound into a
|
|
42091
|
+
// floor-scale dent; the budget bounds the damage per neighborhood to the
|
|
42092
|
+
// same floor that bounds it per collapse.
|
|
42093
|
+
// Cell size scales with the buffer radius (recovered from the disk-relative
|
|
42094
|
+
// fillFloor) so a neighborhood is radius-scale at any latitude/units; clip
|
|
42095
|
+
// attribution is O(1) per accept (center cell), so a dent straddling a cell
|
|
42096
|
+
// edge is bounded by 2x the floor rather than 1x.
|
|
42097
|
+
var budgetCell = Math.max(1000, Math.sqrt(fillFloor / BUFFER_LOOP_FILL_AREA_FRAC));
|
|
42098
|
+
var clipBudget = coverage ? {map: new Map(), cell: budgetCell} : null;
|
|
42099
|
+
var work = ring, workPos = null;
|
|
42082
42100
|
for (var pass = 0; pass < maxPasses; pass++) {
|
|
42083
|
-
var res = collapseRingLoopsPass(work, maxGap, workPos, turnPrefix, maxTurn,
|
|
42101
|
+
var res = collapseRingLoopsPass(work, maxGap, workPos, turnPrefix, maxTurn,
|
|
42102
|
+
coverage, fillFloor, clipBudget);
|
|
42084
42103
|
if (res.ring.length === work.length) return res.ring; // stable
|
|
42085
|
-
work = res.ring; workPos = res.srcPos;
|
|
42104
|
+
work = res.ring; workPos = res.srcPos;
|
|
42086
42105
|
}
|
|
42087
42106
|
return work;
|
|
42088
42107
|
}
|
|
@@ -42090,38 +42109,25 @@ ${svg}
|
|
|
42090
42109
|
// One greedy forward-collapse pass (mirrors removeBufferRingLoops' compaction).
|
|
42091
42110
|
// The span gate is, in order of preference:
|
|
42092
42111
|
// - the reliable source-path turn (getSpanTurn) when srcPos+turnPrefix given;
|
|
42093
|
-
// - else the
|
|
42094
|
-
//
|
|
42095
|
-
//
|
|
42096
|
-
|
|
42097
|
-
function collapseRingLoopsPass(ring, maxGap, srcPos, turnPrefix, maxTurn, dipTags, fillFloor) {
|
|
42112
|
+
// - else the exact coverage check when `coverage` is set (clean-outline rings);
|
|
42113
|
+
// - else the ring's cumulative turn with a geometric cusp threshold.
|
|
42114
|
+
// Returns {ring, srcPos} so the caller can iterate.
|
|
42115
|
+
function collapseRingLoopsPass(ring, maxGap, srcPos, turnPrefix, maxTurn, coverage, fillFloor, clipBudget) {
|
|
42098
42116
|
var gated = !!(srcPos && turnPrefix);
|
|
42099
42117
|
var n = ring.length - 1;
|
|
42100
|
-
// The
|
|
42101
|
-
//
|
|
42118
|
+
// The coverage path defers entirely to the exact coverage check, so it needs
|
|
42119
|
+
// neither a turn gate nor a cumulative-turn prefix; the other two paths
|
|
42102
42120
|
// (source-turn, geometric) have no coverage check and keep the turn gate.
|
|
42103
|
-
var
|
|
42104
|
-
|
|
42105
|
-
//
|
|
42106
|
-
//
|
|
42107
|
-
var covIndex =
|
|
42108
|
-
|
|
42109
|
-
// UNCOVERING area, so it cannot catch a collapse that FILLS a real winding-0
|
|
42110
|
-
// hole (filling adds coverage), and its area pre-filter treats any sub-floor
|
|
42111
|
-
// loop as safe to drop. A dropped sub-loop wound OPPOSITE to the parent ring
|
|
42112
|
-
// bounds such a hole, so refuse the collapse outright -- this keeps real holes
|
|
42113
|
-
// (annulus interiors, self-crossing-line pockets) the coverage check alone
|
|
42114
|
-
// misses, at the cost of only an O(span) signed-area test on the crossings the
|
|
42115
|
-
// collapse actually evaluates (far cheaper than the old O(n*maxGap) per-pass
|
|
42116
|
-
// pre-scan). A same-wound overshoot fold that happens to WRAP a hole is caught
|
|
42117
|
-
// instead by the fill guard in the coverage check (it measures filled area).
|
|
42118
|
-
var parentCCW = coveragePath ? (ringSignedArea(ring) >= 0) : false;
|
|
42121
|
+
var ringTurn = (!gated && !coverage) ? ringAbsTurnPrefix(ring) : null;
|
|
42122
|
+
// The y-band edge index and the parent orientation are built lazily, on the
|
|
42123
|
+
// first candidate that survives to the coverage check: most rings (and most
|
|
42124
|
+
// passes) never get that far, and both are O(ring length) to compute.
|
|
42125
|
+
var covIndex = null;
|
|
42126
|
+
var parentCCW = false, parentKnown = false;
|
|
42119
42127
|
var out = [ring[0]];
|
|
42120
42128
|
var outPos = gated ? [srcPos[0]] : null;
|
|
42121
|
-
var outTags = dipTags ? [dipTags[0]] : null;
|
|
42122
42129
|
var segmentEnd = ring[1];
|
|
42123
42130
|
var segmentEndPos = gated ? srcPos[1] : 0;
|
|
42124
|
-
var segmentEndTag = dipTags ? dipTags[1] : 0;
|
|
42125
42131
|
var nextRingIndex = 2;
|
|
42126
42132
|
while (true) {
|
|
42127
42133
|
var anchor = out[out.length - 1];
|
|
@@ -42133,22 +42139,34 @@ ${svg}
|
|
|
42133
42139
|
var c = ring[s], d = ring[s + 1];
|
|
42134
42140
|
var hit = segHit(ax, ay, bx, by, c[0], c[1], d[0], d[1]);
|
|
42135
42141
|
if (!hit) continue;
|
|
42136
|
-
if (
|
|
42137
|
-
//
|
|
42138
|
-
//
|
|
42139
|
-
//
|
|
42142
|
+
if (coverage) {
|
|
42143
|
+
// Opposite-wound hole protection. The coverage check only guards against
|
|
42144
|
+
// UNCOVERING area, so it cannot catch a collapse that FILLS a real
|
|
42145
|
+
// winding-0 hole (filling adds coverage), and its area pre-filter treats
|
|
42146
|
+
// any sub-floor loop as safe to drop. A dropped sub-loop wound OPPOSITE
|
|
42147
|
+
// to the parent ring bounds such a hole, so refuse the collapse outright
|
|
42148
|
+
// -- this keeps real holes (annulus interiors, self-crossing-line
|
|
42149
|
+
// pockets) the coverage check alone misses, at the cost of only an
|
|
42150
|
+
// O(span) signed-area test on the crossings the collapse actually
|
|
42151
|
+
// evaluates. A same-wound overshoot fold that happens to WRAP a hole is
|
|
42152
|
+
// caught instead by the fill guard in the coverage check.
|
|
42153
|
+
if (!parentKnown) {
|
|
42154
|
+
parentCCW = ringSignedArea(ring) >= 0;
|
|
42155
|
+
parentKnown = true;
|
|
42156
|
+
}
|
|
42140
42157
|
if ((loopAreaSign(hit[0], hit[1], bx, by, ring, nextRingIndex, s) >= 0) !== parentCCW) {
|
|
42141
42158
|
continue;
|
|
42142
42159
|
}
|
|
42143
|
-
// No turn gate on the
|
|
42144
|
-
// It measures how much of the dropped region would become
|
|
42145
|
-
// refuses collapses that would clip a significant lobe
|
|
42146
|
-
// docs/development/buffer-line-notes.md), catching real lobes and
|
|
42147
|
-
// that a cheap turn/area/winding signal cannot separate from
|
|
42148
|
-
// The turn gate was both leaving valid interior loops
|
|
42149
|
-
// hairpins whose source turn exceeds the cap) and
|
|
42150
|
-
// deferring their removal to the dissolve.
|
|
42151
|
-
if (!
|
|
42160
|
+
// No turn gate on the coverage path: the exact coverage check is the
|
|
42161
|
+
// arbiter. It measures how much of the dropped region would become
|
|
42162
|
+
// uncovered and refuses collapses that would clip a significant lobe
|
|
42163
|
+
// (see docs/development/buffer-line-notes.md), catching real lobes and
|
|
42164
|
+
// end caps that a cheap turn/area/winding signal cannot separate from
|
|
42165
|
+
// safe folds. The turn gate was both leaving valid interior loops
|
|
42166
|
+
// uncollapsed (tight hairpins whose source turn exceeds the cap) and
|
|
42167
|
+
// slowing the pipeline by deferring their removal to the dissolve.
|
|
42168
|
+
if (!covIndex) covIndex = buildEdgeYIndex(ring, n);
|
|
42169
|
+
if (!collapseKeepsAreaCovered(ring, n, hit, segmentEnd, nextRingIndex, s, covIndex, fillFloor, clipBudget)) {
|
|
42152
42170
|
continue; // dropping this loop would uncover or fill real area -- leave it
|
|
42153
42171
|
}
|
|
42154
42172
|
} else {
|
|
@@ -42166,24 +42184,20 @@ ${svg}
|
|
|
42166
42184
|
if (crossing) {
|
|
42167
42185
|
segmentEnd = crossing;
|
|
42168
42186
|
if (gated) segmentEndPos = anchorPos;
|
|
42169
|
-
segmentEndTag = 0; // an offset crossing is not a dip vertex
|
|
42170
42187
|
nextRingIndex = crossingEndIndex;
|
|
42171
42188
|
continue;
|
|
42172
42189
|
}
|
|
42173
42190
|
out.push(segmentEnd);
|
|
42174
42191
|
if (gated) outPos.push(segmentEndPos);
|
|
42175
|
-
if (dipTags) outTags.push(segmentEndTag);
|
|
42176
42192
|
if (nextRingIndex > n - 1) break;
|
|
42177
42193
|
segmentEnd = ring[nextRingIndex];
|
|
42178
42194
|
if (gated) segmentEndPos = srcPos[nextRingIndex];
|
|
42179
|
-
if (dipTags) segmentEndTag = dipTags[nextRingIndex];
|
|
42180
42195
|
nextRingIndex++;
|
|
42181
42196
|
}
|
|
42182
|
-
if (out.length < 4) return {ring: ring, srcPos: srcPos
|
|
42197
|
+
if (out.length < 4) return {ring: ring, srcPos: srcPos}; // collapsed away; keep original
|
|
42183
42198
|
out.push(out[0].concat());
|
|
42184
42199
|
if (gated) outPos.push(outPos[0]);
|
|
42185
|
-
|
|
42186
|
-
return {ring: out, srcPos: gated ? outPos : null, dipTags: dipTags ? outTags : null};
|
|
42200
|
+
return {ring: out, srcPos: gated ? outPos : null};
|
|
42187
42201
|
}
|
|
42188
42202
|
|
|
42189
42203
|
// Cumulative absolute turn (degrees) indexed by ring vertex, EXCLUDING fold
|
|
@@ -42191,17 +42205,13 @@ ${svg}
|
|
|
42191
42205
|
// near-180-degree cusps where the offset doubles back on itself (an artifact of
|
|
42192
42206
|
// the self-crossing offset, with no corresponding source bend). A normal offset
|
|
42193
42207
|
// join's turn equals its source bend angle, so the remaining sum reconstructs
|
|
42194
|
-
// the source stretch's cumulative turn -- the
|
|
42195
|
-
//
|
|
42196
|
-
//
|
|
42197
|
-
//
|
|
42198
|
-
//
|
|
42199
|
-
// geometric turn threshold. Tag-gating is the point: a real sharp bend (e.g. a
|
|
42200
|
-
// fjord mouth, emitted as a miter, never tagged) keeps its turn and so is never
|
|
42201
|
-
// mistaken for a fold, which is what makes the geometric-only threshold
|
|
42202
|
-
// over-collapse sharp coastlines.
|
|
42208
|
+
// the source stretch's cumulative turn -- the loop-removal signal -- without
|
|
42209
|
+
// source provenance. Only the provenance-free geometric path uses this (the
|
|
42210
|
+
// clean-outline rings use the exact coverage check instead); the purely
|
|
42211
|
+
// geometric cusp threshold can over-collapse real sharp bends (e.g. a fjord
|
|
42212
|
+
// mouth), which is one reason the coverage check replaced it as the default.
|
|
42203
42213
|
var CUSP_TURN = 135;
|
|
42204
|
-
function ringAbsTurnPrefix(ring
|
|
42214
|
+
function ringAbsTurnPrefix(ring) {
|
|
42205
42215
|
var n = ring.length - 1;
|
|
42206
42216
|
var prefix = new Float64Array(n + 1);
|
|
42207
42217
|
var RAD = 180 / Math.PI;
|
|
@@ -42212,8 +42222,7 @@ ${svg}
|
|
|
42212
42222
|
var cross = e1x * e2y - e1y * e2x;
|
|
42213
42223
|
var dot = e1x * e2x + e1y * e2y;
|
|
42214
42224
|
var ang = Math.abs(Math.atan2(cross, dot)) * RAD;
|
|
42215
|
-
|
|
42216
|
-
prefix[k] = prefix[k - 1] + (isCusp ? 0 : ang);
|
|
42225
|
+
prefix[k] = prefix[k - 1] + (ang > CUSP_TURN ? 0 : ang);
|
|
42217
42226
|
}
|
|
42218
42227
|
prefix[n] = prefix[n - 1];
|
|
42219
42228
|
return prefix;
|
|
@@ -42261,12 +42270,14 @@ ${svg}
|
|
|
42261
42270
|
// The threshold is in ring units; under web Mercator the scale factor is >= 1
|
|
42262
42271
|
// everywhere, so it is an upper bound on the real m^2 area and no genuinely large
|
|
42263
42272
|
// clip is skipped regardless of latitude.
|
|
42264
|
-
function collapseKeepsAreaCovered(ring, n, X, b, nextRingIndex, s, index, fillFloor) {
|
|
42273
|
+
function collapseKeepsAreaCovered(ring, n, X, b, nextRingIndex, s, index, fillFloor, clipBudget) {
|
|
42265
42274
|
// Area pre-filter keeps the scanline off the hot path: a loop can neither clip
|
|
42266
|
-
// nor fill more than its own area, so it is safe to skip
|
|
42267
|
-
//
|
|
42268
|
-
//
|
|
42269
|
-
//
|
|
42275
|
+
// nor fill more than its own (absolute winding) area, so it is safe to skip
|
|
42276
|
+
// whenever the loop is below BOTH thresholds; collapseUncoveredArea guards the
|
|
42277
|
+
// self-crossing case where the net shoelace under-reports that area. (Filling
|
|
42278
|
+
// a winding-0 region never reaches the uncovered floor, so the fill floor must
|
|
42279
|
+
// join the skip test -- otherwise small folds that could fill a small hole
|
|
42280
|
+
// would be skipped, or every collapse would be scanned.)
|
|
42270
42281
|
var floor = fillFloor < BUFFER_LOOP_CHECK_MIN_AREA ? fillFloor : BUFFER_LOOP_CHECK_MIN_AREA;
|
|
42271
42282
|
var u = collapseUncoveredArea(ring, n, X, b, nextRingIndex, s, index, floor);
|
|
42272
42283
|
if (u < 0) return true; // loop below both floors -- too small to clip or fill
|
|
@@ -42275,33 +42286,68 @@ ${svg}
|
|
|
42275
42286
|
// fill floor scales with the buffer disk (dist^2) because a real hole's area is
|
|
42276
42287
|
// a fixed fraction of the disk while a fold sliver is orders of magnitude
|
|
42277
42288
|
// smaller relative to the radius (see BUFFER_LOOP_FILL_AREA_FRAC).
|
|
42278
|
-
|
|
42289
|
+
if (!(u < BUFFER_LOOP_CHECK_MIN_AREA && _lastFillArea < fillFloor)) return false;
|
|
42290
|
+
// Neighborhood budget (see removeBufferRingLoopsIterative): a sub-floor clip
|
|
42291
|
+
// is only accepted while its neighborhood's accumulated clips stay under the
|
|
42292
|
+
// floor. Zero-clip collapses (the common case) skip this entirely.
|
|
42293
|
+
if (u > 0 && clipBudget) {
|
|
42294
|
+
// Key on the clipped region's own centroid (accumulated by the sweep):
|
|
42295
|
+
// overlapping clips from different collapses then share a cell even when
|
|
42296
|
+
// their loops' bboxes differ by kilometers. Two dents straddling a cell
|
|
42297
|
+
// edge can still each spend a budget, so the worst-case neighborhood dent
|
|
42298
|
+
// is 2x the per-collapse floor.
|
|
42299
|
+
var cell = clipBudget.cell;
|
|
42300
|
+
var key = Math.floor(_lastClipX / cell) + ':' + Math.floor(_lastClipY / cell);
|
|
42301
|
+
var spent = clipBudget.map.get(key) || 0;
|
|
42302
|
+
if (spent + u >= BUFFER_LOOP_CHECK_MIN_AREA) return false;
|
|
42303
|
+
clipBudget.map.set(key, spent + u);
|
|
42304
|
+
}
|
|
42305
|
+
return true;
|
|
42279
42306
|
}
|
|
42280
42307
|
|
|
42281
42308
|
// Returns the area of the dropped region a collapse would leave uncovered, or -1
|
|
42282
|
-
// when the loop
|
|
42283
|
-
//
|
|
42309
|
+
// when the loop is provably too small to matter (scanline skipped). See
|
|
42310
|
+
// collapseKeepsAreaCovered for the winding rationale.
|
|
42311
|
+
//
|
|
42312
|
+
// The skip must not trust the net shoelace area alone: a SELF-CROSSING span
|
|
42313
|
+
// (figure-eight) has lobes of opposite winding whose signed areas cancel, so a
|
|
42314
|
+
// near-zero net can hide winding regions far above the floor. |net| bounds the
|
|
42315
|
+
// regions only for a simple loop; the bbox bounds them always. So the scanline
|
|
42316
|
+
// is skipped when the bbox is under the floor, or when the net is under the
|
|
42317
|
+
// floor AND the loop has no self-crossing (O(span^2) pairwise test, span <=
|
|
42318
|
+
// maxGap+2 edges -- run only in the suspicious small-net/large-bbox band).
|
|
42284
42319
|
function collapseUncoveredArea(ring, n, X, b, nextRingIndex, s, index, floor) {
|
|
42285
42320
|
var i, loopLen = s - nextRingIndex + 3; // X, b, ring[next..s]
|
|
42286
42321
|
// Loop area (shoelace over X, b, ring[next..s]) and bounding box.
|
|
42287
42322
|
var area2 = 0, px = X[0], py = X[1];
|
|
42288
42323
|
var minx = X[0], maxx = X[0], miny = X[1], maxy = X[1];
|
|
42289
|
-
|
|
42324
|
+
var qx = b[0], qy = b[1];
|
|
42325
|
+
if (qx < minx) minx = qx; else if (qx > maxx) maxx = qx;
|
|
42326
|
+
if (qy < miny) miny = qy; else if (qy > maxy) maxy = qy;
|
|
42327
|
+
area2 += px * qy - qx * py; px = qx; py = qy;
|
|
42328
|
+
for (i = nextRingIndex; i <= s; i++) {
|
|
42329
|
+
qx = ring[i][0]; qy = ring[i][1];
|
|
42330
|
+
area2 += px * qy - qx * py; px = qx; py = qy;
|
|
42290
42331
|
if (qx < minx) minx = qx; else if (qx > maxx) maxx = qx;
|
|
42291
42332
|
if (qy < miny) miny = qy; else if (qy > maxy) maxy = qy;
|
|
42292
42333
|
}
|
|
42293
|
-
bbox(b[0], b[1]);
|
|
42294
|
-
area2 += px * b[1] - b[0] * py; px = b[0]; py = b[1];
|
|
42295
|
-
for (i = nextRingIndex; i <= s; i++) {
|
|
42296
|
-
area2 += px * ring[i][1] - ring[i][0] * py; px = ring[i][0]; py = ring[i][1];
|
|
42297
|
-
bbox(ring[i][0], ring[i][1]);
|
|
42298
|
-
}
|
|
42299
42334
|
area2 += px * X[1] - X[0] * py;
|
|
42300
|
-
|
|
42301
|
-
|
|
42302
|
-
// that can cross any scanline); reuse module scratch to avoid per-call garbage.
|
|
42335
|
+
var smallNet = Math.abs(area2) / 2 < floor;
|
|
42336
|
+
if (smallNet && (maxx - minx) * (maxy - miny) < floor) return -1;
|
|
42303
42337
|
var scr = coverageScratch(n + loopLen);
|
|
42304
42338
|
var lx0 = scr.lx0, ly0 = scr.ly0, lx1 = scr.lx1, ly1 = scr.ly1;
|
|
42339
|
+
// Loop edges (X->b, b->ring[next..s], ring[s]->X), built before the band
|
|
42340
|
+
// collection so the self-cross test can run first.
|
|
42341
|
+
var lc = 0;
|
|
42342
|
+
lx0[lc] = X[0]; ly0[lc] = X[1]; lx1[lc] = b[0]; ly1[lc] = b[1]; lc++;
|
|
42343
|
+
lx0[lc] = b[0]; ly0[lc] = b[1]; lx1[lc] = ring[nextRingIndex][0]; ly1[lc] = ring[nextRingIndex][1]; lc++;
|
|
42344
|
+
for (i = nextRingIndex; i < s; i++) {
|
|
42345
|
+
lx0[lc] = ring[i][0]; ly0[lc] = ring[i][1]; lx1[lc] = ring[i + 1][0]; ly1[lc] = ring[i + 1][1]; lc++;
|
|
42346
|
+
}
|
|
42347
|
+
lx0[lc] = ring[s][0]; ly0[lc] = ring[s][1]; lx1[lc] = X[0]; ly1[lc] = X[1]; lc++;
|
|
42348
|
+
if (smallNet && !loopEdgesCross(lx0, ly0, lx1, ly1, lc)) return -1; // simple: |net| == area
|
|
42349
|
+
// Collect the ring edges whose y-range meets the loop's band (the only ones
|
|
42350
|
+
// that can cross any scanline); reuse module scratch to avoid per-call garbage.
|
|
42305
42351
|
var band = scr.band, le = 0;
|
|
42306
42352
|
var gen = ++index.gen, stamp = index.stamp, start = index.start, edges = index.edges;
|
|
42307
42353
|
var kb, klo = index.binOf(miny), khi = index.binOf(maxy);
|
|
@@ -42316,17 +42362,23 @@ ${svg}
|
|
|
42316
42362
|
band[le++] = ei;
|
|
42317
42363
|
}
|
|
42318
42364
|
}
|
|
42319
|
-
// Loop edges (X->b, b->ring[next..s], ring[s]->X).
|
|
42320
|
-
var lc = 0;
|
|
42321
|
-
lx0[lc] = X[0]; ly0[lc] = X[1]; lx1[lc] = b[0]; ly1[lc] = b[1]; lc++;
|
|
42322
|
-
lx0[lc] = b[0]; ly0[lc] = b[1]; lx1[lc] = ring[nextRingIndex][0]; ly1[lc] = ring[nextRingIndex][1]; lc++;
|
|
42323
|
-
for (i = nextRingIndex; i < s; i++) {
|
|
42324
|
-
lx0[lc] = ring[i][0]; ly0[lc] = ring[i][1]; lx1[lc] = ring[i + 1][0]; ly1[lc] = ring[i + 1][1]; lc++;
|
|
42325
|
-
}
|
|
42326
|
-
lx0[lc] = ring[s][0]; ly0[lc] = ring[s][1]; lx1[lc] = X[0]; ly1[lc] = X[1]; lc++;
|
|
42327
42365
|
return loopUncoveredArea(ring, band, le, lx0, ly0, lx1, ly1, lc, minx, maxx, miny, maxy, scr);
|
|
42328
42366
|
}
|
|
42329
42367
|
|
|
42368
|
+
// True if any two non-adjacent loop edges cross (strict interior). Adjacent
|
|
42369
|
+
// edges share an endpoint and cannot strictly cross, so they are skipped.
|
|
42370
|
+
function loopEdgesCross(lx0, ly0, lx1, ly1, lc) {
|
|
42371
|
+
for (var i = 0; i < lc - 1; i++) {
|
|
42372
|
+
for (var j = i + 2; j < lc; j++) {
|
|
42373
|
+
if (i === 0 && j === lc - 1) continue; // adjacent via closure at X
|
|
42374
|
+
if (segHit(lx0[i], ly0[i], lx1[i], ly1[i], lx0[j], ly0[j], lx1[j], ly1[j])) {
|
|
42375
|
+
return true;
|
|
42376
|
+
}
|
|
42377
|
+
}
|
|
42378
|
+
}
|
|
42379
|
+
return false;
|
|
42380
|
+
}
|
|
42381
|
+
|
|
42330
42382
|
// y-band index of ring edges: bins the ring's y-range so a scanline query at
|
|
42331
42383
|
// [miny, maxy] returns only edges reaching that band instead of scanning all n.
|
|
42332
42384
|
// start[k]..start[k+1] are indices into `edges` for bin k; `stamp`/`gen` dedup an
|
|
@@ -42383,16 +42435,24 @@ ${svg}
|
|
|
42383
42435
|
// Returns the uncovered area and writes the filled area to _lastFillArea.
|
|
42384
42436
|
// `band` lists the indices of ring edges that can reach the loop's y-band.
|
|
42385
42437
|
var _lastFillArea = 0;
|
|
42438
|
+
var _lastClipX = 0, _lastClipY = 0;
|
|
42386
42439
|
function loopUncoveredArea(ring, band, be, lx0, ly0, lx1, ly1, le, minx, maxx, miny, maxy, scr) {
|
|
42387
42440
|
var h = maxy - miny;
|
|
42388
42441
|
_lastFillArea = 0;
|
|
42389
42442
|
if (h <= 0 || maxx <= minx) return 0;
|
|
42443
|
+
// Known limitation: rows are uniform with a hard cap, so dy scales with the
|
|
42444
|
+
// loop's bbox height and a region thinner than dy in y can fall between row
|
|
42445
|
+
// midpoints unmeasured (only reachable via a loop whose bbox is much taller
|
|
42446
|
+
// than the region -- not observed outside synthetic data). A designed fix
|
|
42447
|
+
// (vertex-guided rows) was implemented, measured at 4-8% of buffer build
|
|
42448
|
+
// time, and reverted; see "Scanline row starvation" in
|
|
42449
|
+
// docs/development/buffer-line-notes.md to re-add it if it is ever needed.
|
|
42390
42450
|
var target = Math.sqrt(BUFFER_LOOP_CHECK_MIN_AREA) / 4;
|
|
42391
42451
|
var rows = Math.round(h / (target > 0 ? target : h));
|
|
42392
42452
|
if (rows < 8) rows = 8; else if (rows > 40) rows = 40;
|
|
42393
42453
|
var dy = h / rows;
|
|
42394
42454
|
var xs = scr.xs, df = scr.df, dl = scr.dl, order = scr.order;
|
|
42395
|
-
var total = 0, fill = 0, r, k, i;
|
|
42455
|
+
var total = 0, fill = 0, momX = 0, momY = 0, r, k, i;
|
|
42396
42456
|
for (r = 0; r < rows; r++) {
|
|
42397
42457
|
var y = miny + (r + 0.5) * dy;
|
|
42398
42458
|
// Winding just left of the loop's x-range (base), plus the crossings that
|
|
@@ -42421,17 +42481,24 @@ ${svg}
|
|
|
42421
42481
|
for (i = 0; i < m; i++) {
|
|
42422
42482
|
var o = order[i], xk = xs[o];
|
|
42423
42483
|
if (wl !== 0 && xk > prevx) {
|
|
42424
|
-
if (wf - wl === 0)
|
|
42484
|
+
if (wf - wl === 0) {
|
|
42485
|
+
total += xk - prevx;
|
|
42486
|
+
momX += (xk + prevx) / 2 * (xk - prevx); momY += y * (xk - prevx);
|
|
42487
|
+
}
|
|
42425
42488
|
else if (wf === 0) fill += xk - prevx;
|
|
42426
42489
|
}
|
|
42427
42490
|
wf += df[o]; wl += dl[o]; prevx = xk;
|
|
42428
42491
|
}
|
|
42429
42492
|
if (wl !== 0 && maxx > prevx) {
|
|
42430
|
-
if (wf - wl === 0)
|
|
42493
|
+
if (wf - wl === 0) {
|
|
42494
|
+
total += maxx - prevx;
|
|
42495
|
+
momX += (maxx + prevx) / 2 * (maxx - prevx); momY += y * (maxx - prevx);
|
|
42496
|
+
}
|
|
42431
42497
|
else if (wf === 0) fill += maxx - prevx;
|
|
42432
42498
|
}
|
|
42433
42499
|
}
|
|
42434
42500
|
_lastFillArea = fill * dy;
|
|
42501
|
+
if (total > 0) { _lastClipX = momX / total; _lastClipY = momY / total; }
|
|
42435
42502
|
return total * dy;
|
|
42436
42503
|
}
|
|
42437
42504
|
|
|
@@ -43151,9 +43218,9 @@ ${svg}
|
|
|
43151
43218
|
var roundJoinSegAngle = 90 / roundJoinSegsPerQuadrant;
|
|
43152
43219
|
// Max arc step (degrees) for the coarse concave bridge (makeCoarseConcaveJoin),
|
|
43153
43220
|
// the optional low-resolution alternative to makeConcaveJoin in
|
|
43154
|
-
// traceCleanOffsetSide. Larger = fewer points = faster dissolve.
|
|
43155
|
-
//
|
|
43156
|
-
//
|
|
43221
|
+
// traceCleanOffsetSide. Larger = fewer points = faster dissolve. NOTE: a
|
|
43222
|
+
// surviving (uncollapsed) bridge CAN become output boundary, where this step
|
|
43223
|
+
// sets the dent depth -- see the caution at makeCoarseConcaveJoin.
|
|
43157
43224
|
var CLEAN_OUTLINE_BRIDGE_STEP = 90;
|
|
43158
43225
|
var capStyle = opts.cap_style || 'round'; // expect 'round' or 'flat'
|
|
43159
43226
|
var pathIter = useMercator ?
|
|
@@ -44159,11 +44226,38 @@ ${svg}
|
|
|
44159
44226
|
} else {
|
|
44160
44227
|
add(p2Prev, segId, 1);
|
|
44161
44228
|
joinPoints = concaveJoin(x1, y1, bearing - 90, -joinAngle, dist);
|
|
44229
|
+
var exposedWedge = false;
|
|
44230
|
+
if (!opts.coarse_bridge) {
|
|
44231
|
+
// Exposure-gated coarse bridge (default): a dip may be emitted
|
|
44232
|
+
// coarsely only when it cannot affect the output.
|
|
44233
|
+
// 1. Exposure gate: probe the full-resolution arc + tips against
|
|
44234
|
+
// the source path (wedgeIsExposed; each probe uses its own
|
|
44235
|
+
// radius with a 0.98 margin, erring toward "exposed" = the
|
|
44236
|
+
// full arc). An uncovered arc point is potential true output
|
|
44237
|
+
// boundary -- an exposed dip that the loop remover refuses to
|
|
44238
|
+
// collapse IS the boundary there, so it must stay at full
|
|
44239
|
+
// resolution (see the caution at makeCoarseConcaveJoin).
|
|
44240
|
+
// Collapsed coarse dips legally clip up to the remover's
|
|
44241
|
+
// per-collapse floor (the chord-to-arc lens is single-covered
|
|
44242
|
+
// where only the fold's own winding spans it); the remover's
|
|
44243
|
+
// neighborhood clip budget keeps clustered dips from
|
|
44244
|
+
// compounding several such clips into one visible dent (a
|
|
44245
|
+
// 102k m^2 dent on the innerlines 2km Sabine River fold
|
|
44246
|
+
// cluster before the budget existed).
|
|
44247
|
+
if (!vertsSegIndex) vertsSegIndex = buildVertsSegmentIndex(verts);
|
|
44248
|
+
exposedWedge = wedgeIsExposed(vertsSegIndex, segId - 1, segId,
|
|
44249
|
+
x1, y1, joinPoints, p2Prev, p1);
|
|
44250
|
+
if (!exposedWedge) {
|
|
44251
|
+
joinPoints = makeCoarseConcaveJoin(x1, y1, bearing - 90, -joinAngle, dist);
|
|
44252
|
+
}
|
|
44253
|
+
}
|
|
44162
44254
|
if (useGapPatch(opts, useMercator) &&
|
|
44163
44255
|
offsetEdgesFanApart(p1Prev, p2Prev, p1, p2)) {
|
|
44164
44256
|
if (!vertsSegIndex) vertsSegIndex = buildVertsSegmentIndex(verts);
|
|
44165
|
-
if (
|
|
44166
|
-
|
|
44257
|
+
if (opts.coarse_bridge ?
|
|
44258
|
+
wedgeIsExposed(vertsSegIndex, segId - 1, segId, x1, y1,
|
|
44259
|
+
joinPoints, p2Prev, p1) :
|
|
44260
|
+
exposedWedge) {
|
|
44167
44261
|
fanApartBends.push(segId);
|
|
44168
44262
|
}
|
|
44169
44263
|
}
|
|
@@ -44367,13 +44461,21 @@ ${svg}
|
|
|
44367
44461
|
return makeRoundJoin(cx, cy, startBearing, arcAngle, dist).reverse();
|
|
44368
44462
|
}
|
|
44369
44463
|
|
|
44370
|
-
// Coarse alternative to makeConcaveJoin
|
|
44371
|
-
//
|
|
44372
|
-
//
|
|
44373
|
-
//
|
|
44374
|
-
//
|
|
44375
|
-
//
|
|
44376
|
-
//
|
|
44464
|
+
// Coarse alternative to makeConcaveJoin: bridges a concave bend with as few
|
|
44465
|
+
// as one reversed arc vertex (CLEAN_OUTLINE_BRIDGE_STEP), producing a
|
|
44466
|
+
// smaller ring for the winding dissolve to chew through. Used by default
|
|
44467
|
+
// only behind the exposure gate above (plus the loop remover's neighborhood
|
|
44468
|
+
// clip budget); opts.coarse_bridge forces it everywhere, unguarded.
|
|
44469
|
+
// CAUTION -- unsound WITHOUT those guards (2026-07-02 eval, see
|
|
44470
|
+
// "coarse-bridge" in docs/development/buffer-line-notes.md): a dip that the
|
|
44471
|
+
// loop remover refuses to collapse can be real output boundary (near-U-turn
|
|
44472
|
+
// bends whose wedge nothing else covers; single-sided polygon grows; hole
|
|
44473
|
+
// boundaries), and there the coarse chords replace the true arc. Chord
|
|
44474
|
+
// vertices stay on the radius-dist circle, so coarse geometry never falls
|
|
44475
|
+
// OUTSIDE the true buffer -- the failures are inward dents (sagitta up to
|
|
44476
|
+
// (1-cos45)*dist ~ 0.29*dist at the 90-degree step) and spurious holes
|
|
44477
|
+
// (chord-triangle slivers whose at-radius vertices defeat the artifact-hole
|
|
44478
|
+
// filter's distance classification).
|
|
44377
44479
|
function makeCoarseConcaveJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
44378
44480
|
var segs = Math.min(roundJoinSegsPerQuadrant,
|
|
44379
44481
|
Math.max(1, Math.ceil(arcAngle / CLEAN_OUTLINE_BRIDGE_STEP)));
|
|
@@ -44385,6 +44487,7 @@ ${svg}
|
|
|
44385
44487
|
return points.reverse();
|
|
44386
44488
|
}
|
|
44387
44489
|
|
|
44490
|
+
|
|
44388
44491
|
// get interior vertices of an interpolated CW arc
|
|
44389
44492
|
function makeRoundJoin(cx, cy, startBearing, arcAngle, dist) {
|
|
44390
44493
|
var points = [];
|
|
@@ -66085,7 +66188,7 @@ ${svg}
|
|
|
66085
66188
|
return name == 'rectangle' || name == 'rectangles' || name == 'filter' && opts.cleanup;
|
|
66086
66189
|
}
|
|
66087
66190
|
|
|
66088
|
-
var version = "0.7.
|
|
66191
|
+
var version = "0.7.35";
|
|
66089
66192
|
|
|
66090
66193
|
// Parse command line args into commands and run them
|
|
66091
66194
|
// Function takes an optional Node-style callback. A Promise is returned if no callback is given.
|