Deoverlap
De-overlap Shapely geometries that sit within a tolerance of each other.
The common case is pen-plotter work: two strokes closer than a pen width visually merge on paper, so only one of them should keep the ink. Unlike endpoint-only “deduplicate” tools, this library builds a corridor around each kept stroke and crops (or drops) later strokes that fall inside it.
pip install deoverlap
# with the vpype command:
pip install "deoverlap[vpype]"
Quick start
from shapely.geometry import LineString
from deoverlap import deoverlap, KeepPolicy
geoms = [
LineString([(0, 0), (2, 0)]),
LineString([(1, 0.05), (3, 0.05)]), # parallel, 0.05 away
]
result = deoverlap(geoms, tolerance=0.1, keep=KeepPolicy.LONGEST)
print(len(result.kept), "surviving geometries")
print("wholly removed:", result.wholly_removed)
tolerance is a distance in the same units as the geometries. For a plotter
with a 0.1 mm pen, start around 0.08–0.15 (page mm).
Keep policy — which stroke wins
When two corridors collide, priority is explicit:
keep= |
Behaviour |
|---|---|
first (default) |
Input order — stable, historical behaviour |
longest |
Prefer the stroke that covers more ground |
shortest |
Prefer short marks / detail |
Original indices are preserved in result.kept_parts / removed_parts even
when processing order changes.
Crop vs drop
mode="crop"(default) — subtract the overlap, keep the rest.mode="drop"— if more thandrop_fraction(default 0.5) of a geometry’s length is covered, discard the whole thing instead of leaving stubs.
min_length= drops lineal fragments shorter than that threshold after clipping
(points are never removed by it).
Parallel only — keep crossings
By default a corridor punches a hole in any later geometry, including a perpendicular cross. For plotters, overdraw at a cross is usually fine; the pain is parallel near-coincident runs:
result = deoverlap(
geoms,
tolerance=0.1,
parallel_only=True,
parallel_angle=30, # degrees
)
Only corridors whose bearing is within parallel_angle of the candidate are
used as clip masks.
Groups — split pieces stay one object
When a ring is cut by a corridor it becomes two arcs. With group=True
(default) those arcs are returned as one multipart geometry under the
original input index — a logical stroke, not two anonymous objects:
ring = LineString([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
cutter = LineString([(1, -1), (1, 3)])
result = deoverlap([cutter, ring], 0.15, group=True)
result.kept_parts[1] # MultiLineString of both arcs
Set group=False for a flat list of primitive pieces (origins still recorded
in kept_parts).
This is the right model for layer-aware pipelines too: treat each input
geometry as a group, run deoverlap per layer, and never let one layer’s
corridor eat another layer’s groups unless you pass a shared mask.
Multi-stage with a carried mask
r1 = deoverlap(batch1, 0.1)
r2 = deoverlap(batch2, 0.1, mask=r1.mask)
Stage 2 is clipped against everything stage 1 kept.
Result
deoverlap(...) returns a DeoverlapResult:
| Field | Meaning |
|---|---|
kept |
Drawable geometries (grouped or flat) |
removed |
Flattened cut pieces (if keep_duplicates=True) |
kept_parts |
{original_index: geometry} |
removed_parts |
{original_index: geometry} |
wholly_removed |
Indices removed entirely |
mask |
Corridor polygons (for the next stage) |
Iterating the result still yields the legacy
(kept, kept_map, removed, mask) tuple.
Performance notes
The heavy lifting is GEOS (via Shapely). The Python loop avoids rebuilding the
spatial index on every insert (tree_rebuild_every) and periodically dissolves
the mask when bearings are not needed (mask_union_every). Do not run this
on stipple / dab layers — a dot grid is meant to sit closer than a pen width.
vpype plugin
Installing deoverlap[vpype] registers a deoverlap command that is
layer-safe (honours -l) and uses corridor proximity rather than
endpoint-only matching:
vpype read map.svg deoverlap -t 0.1mm --keep longest -l 1,2,3 write out.svg
| Flag | Default | Meaning |
|---|---|---|
-t / --tolerance |
0.1mm |
Corridor half-width |
--keep |
longest |
first | longest | shortest |
--mode |
crop |
crop | drop |
--min-length |
0 |
Drop stubs shorter than this |
--parallel-only |
on | Do not punch perpendicular crossings |
--parallel-angle |
30 |
Bearing window in degrees |
-l |
all |
Target layer(s) |
-k |
off | Keep removed pieces on a new layer |
Do not run it on stipple / dab layers (park dots): they are meant to sit closer than a pen width.
Self-overlap (--segments)
A thin road outline is often one LineString (left kerb → end cap → right
kerb). Without --segments, deoverlap never compares a geometry to itself, so
the two sides stay as a heavy double stroke. With --segments, every edge is
its own corridor unit; opposite sides can suppress each other, while immediate
neighbours on the same chain (--segment-adjacency, default 1) stay intact so
joints are not nibbled.
vpype read map.svg deoverlap -t 0.15mm --keep longest --segments -l 1 write out.svg
API
deoverlap(
geometries,
tolerance,
*,
keep="first", # first | longest | shortest
mode="crop", # crop | drop
min_length=0.0,
drop_fraction=0.5,
parallel_only=False,
parallel_angle=30.0,
segments=False, # self-overlap via edge explosion
segment_adjacency=1,
group=True,
keep_duplicates=False,
progress_bar=False,
mask=None,
tree_rebuild_every=32,
mask_union_every=64,
) -> DeoverlapResult
Release files for deoverlap 3.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deoverlap-3.1.0.tar.gz | 18.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deoverlap-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.0 kB
Release files / deoverlap-3.1.0.tar.gz
| Download URL | deoverlap-3.1.0.tar.gz |
|---|---|
| Size | 18.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d1d384e2c6ea7dd4a0ab2de5f492d89e51028e28555b856c5680cd574403172f
|
|
BLAKE2b-256 checksum How to use checksums |
a9c8274e6edcef28380d2b19f1d64bdabdd0710d24cc2c44f8f9e028e90d9e3c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / deoverlap-3.1.0-py3-none-any.whl
| Download URL | deoverlap-3.1.0-py3-none-any.whl |
|---|---|
| Size | 13.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8e7ff60e99376f813328cbbc28afe21e349676b456702dd4f581f111520e6a3a
|
|
BLAKE2b-256 checksum How to use checksums |
6d306cd4f0602ababafb0676ca5cc6ca613e527816466b3f165366a2acbb2431
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|