Skip to main content

Deoverlap

De-overlap vector strokes to prevent overdrawing.

Source, examples and issues: github.com/piLeoni/deoverlap

Where strokes run on top of each other, deoverlap keeps one and cuts the others back, so no area gets drawn twice. One Rust engine, three front ends: a Python library (Shapely geometries), a vpype command for SVG plotter pipelines, and Node.js bindings over flat coordinate buffers.

Before, removed, and after on a real map at pen width (OpenStreetMap)

A street map drawn with a 0.5 mm pen: before (left), what was cut away (middle), after (right). Blue is kept, red is removed, orange is the corridor mask — the same colours in every figure below.

Quick start

Python

pip install deoverlap
from shapely.geometry import LineString
from deoverlap import deoverlap

geoms = [
    LineString([(0, 0), (2, 0)]),
    LineString([(1, 0.05), (3, 0.05)]),  # parallel, 0.05 away
]
result = deoverlap(geoms, tolerance=0.1)

print(len(result.kept), "surviving geometries")  # 2: the second is cropped
print("wholly removed:", result.wholly_removed)  # []

tolerance is a distance in the same units as the geometries: strokes closer than this to a kept stroke are cut. For a plotter, use the pen width.

vpype

pip install "deoverlap[vpype]"
vpype read map.svg deoverlap -t 0.1mm -l 1,2,3 write out.svg

Same options as the Python function (--self-overlap ↔ self_overlap=True); only the tolerance has a default here, 0.1mm. Full flag list: vpype plugin.

Node.js

npm install deoverlap

Flat buffers, API and building from source: bindings/node/README.md, docs/WIRE_FORMAT.md.

How it works

Geometries are processed one at a time, in priority order. Each kept stroke gets a corridor of radius tolerance around it, and every later stroke loses the parts that fall inside a corridor. The options below decide which stroke goes first (prefer), which nearby strokes count as overlapping (angle), and what to do with the leftovers of a cut stroke (drop, min_length).

Tangent circles and a line: the corridor mask (orange) around kept strokes (blue) crops the overlapping arcs (red)

Prefer — which stroke wins

The processing order decides which of two overlapping strokes is kept whole:

prefer= Behaviour
longest (default) The stroke that covers more ground wins
first Input order
shortest Short marks / detail win

The same three strokes under prefer=longest, first and shortest

Original indices are preserved in result.kept_parts / removed_parts even when processing order changes.

Angle — which strokes count as overlapping

Two strokes overlap only where their directions differ by at most angle degrees (default 30). Parallel runs and shallow merges are cropped; crossings steeper than that are left alone. angle=90 counts every nearby stroke, so crossings get cut too.

result = deoverlap(geoms, tolerance=0.1, angle=90)  # cut crossings too

Directions are compared locally, edge by edge, so a curving ramp is cropped only where it actually runs alongside another road, whatever direction its two ends point in.

Where two strokes meet at an angle θ, they overlap for roughly pen / sin θ: about 2 pen widths at 30° and 4 at 15°. The default of 30° catches parallel runs and trims shallow merges; 45° or 60° trim steeper merges too.

angle=90 cuts the crossings too; angle=30 only crops the parallel duplicate

Crop or drop

By default the overlap is cut away and the rest of the stroke is kept. With drop=0.5, a stroke that would lose more than half its length is discarded whole instead of leaving stubs.

drop=None keeps the protruding stub, drop=0.5 discards the mostly covered stroke

A dropped stroke has no entry in kept_parts and its index is listed in wholly_removed. With keep_duplicates=True, removed_parts holds the original geometry unchanged.

min_length= drops pieces shorter than that after cutting (points are never removed by it).

Split pieces stay one object

A ring crossed by another stroke is cut into two arcs. They come back as one MultiLineString under the ring's input index, not as two anonymous pieces:

cutter = LineString([(1, -1), (1, 3)])
ring = LineString([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
result = deoverlap([cutter, ring], 0.15, prefer="first", angle=90)

result.kept_parts[1]  # MultiLineString with both arcs

One colour per object. On the left, both halves of the ring are one entry of result.kept; on the right, flatten_geometries(result.kept) splits them into separate single parts, when that is what you need:

The cut ring is one object in result.kept (same colour), and two after flatten_geometries

Self-overlap

A thin road outline is often one LineString (left kerb → end cap → right kerb). Normally deoverlap never compares a geometry to itself, so the two sides stay as a heavy double stroke. With self_overlap=True every edge is its own corridor, so opposite sides can suppress each other. Neighbouring edges never cut each other, so joints are not nibbled, unless the path folds back on itself like a hairpin.

A thin ribbon drawn as one polyline: untouched normally, one side suppressed with self_overlap

Multi-stage with a carried mask

To process batches separately (for example, main roads first, then side streets), pass the previous result as mask:

r1 = deoverlap(batch1, 0.1)
r2 = deoverlap(batch2, 0.1, mask=r1)  # or mask=r1.mask

Stage 2 is clipped against everything stage 1 kept.

Result

deoverlap(...) returns a DeoverlapResult:

Field Meaning
kept Surviving geometries, one per input that kept something
removed Cut pieces, flattened (if keep_duplicates=True)
kept_parts {input_index: geometry}
removed_parts {input_index: geometry} (if keep_duplicates=True)
wholly_removed Indices of inputs with nothing kept
mask Corridor polygons, for a next stage

Performance

The Python wheel is a compiled extension (no Rust toolchain at install time). The engine builds no polygons: the corridor of a straight edge is a capsule, and the part of another edge inside it is computed exactly. On the MacArthur Maze map below (1473 paths) a run takes about 30 ms.

vpype plugin

pip install "deoverlap[vpype]" registers a deoverlap command. Each layer is processed on its own; -l picks which ones.

Flag Default Meaning
-t, --tolerance 0.1mm Corridor radius, usually the pen width
--prefer longest longest | first | shortest
--angle 30 Max direction difference in degrees; 90 cuts crossings too
--self-overlap off Let parts of one path cut each other
-m, --min-length 0mm Drop pieces shorter than this after cutting
--drop off Discard a stroke when more than this fraction would be cut
-k, --keep-duplicates off Keep removed pieces in a separate layer
-p, --progress-bar off Display a progress bar
-l, --layer all Target layer(s)

-t, -l, -k and -p follow the usual vpype conventions; -m matches vpype filter.

Chain the command to give each layer its own settings:

vpype read map.svg \
  deoverlap -t 0.1mm -l 3,4 \
  deoverlap -t 0.15mm --self-overlap -l 5 \
  write opt.svg

Walkthrough with the bundled example

examples/parallel_strokes.svg is a tiny card with two near-parallel pairs (0.05 mm apart), one deliberate cross, and a dual-kerb rectangle drawn as a single polyline. Run the commands and open the outputs side by side.

# Inspect path count / drawn length
vpype read examples/parallel_strokes.svg stat

# Crop near-parallel duplicates; leave the perpendicular cross alone
vpype read examples/parallel_strokes.svg \
  deoverlap -t 0.1mm -l 1 \
  write examples/out_parallel.svg

# Same, but also let the dual-kerb ring collapse against itself
vpype read examples/parallel_strokes.svg \
  deoverlap -t 0.12mm --self-overlap -l 1 \
  write examples/out_self_overlap.svg

vpype read examples/out_parallel.svg stat
vpype read examples/out_self_overlap.svg stat

What to look for (in the SVG or in stat’s drawn length):

  1. Before — each parallel pair is a double stroke; the ring reads as a heavy double outline.
  2. out_parallel.svg — each pair reduced to one survivor; the cross still there; the ring still double (one geometry cannot overlap itself).
  3. out_self_overlap.svg — the ring’s opposite sides suppress each other, so the dual kerb thins toward a single outline.

A real map: the MacArthur Maze

examples/macarthur_maze.svg is a 3 km square of Oakland's MacArthur Maze interchange from OpenStreetMap, scaled onto a 100 mm card (1 mm ≈ 33 m). At that scale dual carriageways, stacked ramps and frontage roads sit a fraction of a millimetre apart, so a 0.5 mm pen draws the same paper two or three times. Set the tolerance to the pen width: anything closer than that would overlap on paper.

vpype read examples/macarthur_maze.svg stat
vpype read examples/macarthur_maze.svg \
  deoverlap -t 0.5mm --angle 90 -m 1mm \
  write examples/out_maze.svg
vpype read examples/out_maze.svg stat

The whole card after this run; the dashed box is the zoom shown at the top of this page:

The whole 100 mm card: kept in blue, removed in red; dashed box is the zoom at the top

--angle 90 is the maximum: every nearby stroke counts, whatever its direction. Side streets stop where the main road's ink begins, and at crossings the shorter path is split around the longer one. The gap is exactly the other road's ink width, so on paper the crossing still looks whole, just without the dark spot of doubled ink.

Cutting leaves fragments; almost every piece under 1 mm on this card is one. -m 1mm (two pen widths) drops them. The path count goes from 1473 to 860 and the drawn length drops by 35%.

With the default --angle 30 crossings are left alone: parallel runs are still merged, but both roads are drawn through every junction, and the card loses about 23% instead of 35%. The same zoom, crossings kept and cut:

The zoom at --angle 30 and --angle 90: at 30 the crossings keep both roads and leave dark spots, at 90 they are cut

Keep crossings when every stroke must stay continuous (e.g. for later editing); cut them when only the ink on paper matters.

To try another place (needs pip install osmnx):

python examples/fetch_osm_example.py --lat 51.5074 --lon -0.1278 --radius 1500 \
  --out examples/my_place.svg

All README figures are rendered from the committed files by python examples/make_figures.py (needs matplotlib).

Map data © OpenStreetMap contributors, available under the ODbL.

API

deoverlap(
    geometries,
    tolerance,
    *,
    prefer="longest",      # longest | first | shortest
    angle=30.0,            # degrees, 0–90; 90 cuts crossings too
    self_overlap=False,
    min_length=0.0,
    drop=None,             # fraction 0–1; None always crops
    keep_duplicates=False,
    progress_bar=False,
    mask=None,
) -> DeoverlapResult

Metadata

Release files for deoverlap 4.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for deoverlap 4.2.0
File Size Uploaded Provenance Actions
deoverlap-4.2.0.tar.gz 33.9 kB Without provenance Details for deoverlap-4.2.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for deoverlap 4.2.0
File Actions
deoverlap-4.2.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details for deoverlap-4.2.0-cp310-abi3-win_amd64.whl
deoverlap-4.2.0-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details for deoverlap-4.2.0-cp310-abi3-musllinux_1_2_x86_64.whl
deoverlap-4.2.0-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details for deoverlap-4.2.0-cp310-abi3-musllinux_1_2_aarch64.whl
deoverlap-4.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for deoverlap-4.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
deoverlap-4.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for deoverlap-4.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
deoverlap-4.2.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for deoverlap-4.2.0-cp310-abi3-macosx_11_0_arm64.whl
deoverlap-4.2.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details for deoverlap-4.2.0-cp310-abi3-macosx_10_12_x86_64.whl

Total release size: 3.2 MB

Release files / deoverlap-4.2.0.tar.gz

Download URL deoverlap-4.2.0.tar.gz
Size 33.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d3ec397d79cc5866f698d563345802c4cac4daf9463e7634ac9c209da0c78f2f
BLAKE2b-256 checksum
How to use checksums
5fd5baa2c5a7887a75fe1e486b811aa28090311fbeb097f376930ea515be342a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-win_amd64.whl

Download URL deoverlap-4.2.0-cp310-abi3-win_amd64.whl
Size 279.5 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
72f145c86bf35a71e8197fc03f3c770551b48398d1d41ae92f744cfb0c55abab
BLAKE2b-256 checksum
How to use checksums
2031d27872fa785ce6602434bcc46a303cede728b1e56fd66f92484e90878d1e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL deoverlap-4.2.0-cp310-abi3-musllinux_1_2_x86_64.whl
Size 639.1 kB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
d7fbec2aa52a8423746278e84421653a42c5561121004e4a79f05226736b2f49
BLAKE2b-256 checksum
How to use checksums
9e5aa2b7e49c60c9998b5f0f543e93c33e83df71c7ed454da8d47c989615e805
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL deoverlap-4.2.0-cp310-abi3-musllinux_1_2_aarch64.whl
Size 602.3 kB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
3446c20eb339ee886ded2c231b265467d610ffea13efa7c359a5aa9ecd2ed1d2
BLAKE2b-256 checksum
How to use checksums
d14ecbe8af1eb57f2b6d3cae36e313ed4b316e24a0bc9d371b0614cb37b23e01
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL deoverlap-4.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 433.9 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
82419744fffa0a016325517d834258582612b65b1b5b0fa3bd62b6b205614fd0
BLAKE2b-256 checksum
How to use checksums
aaee3efe7b5b90616a1a468e2c1fe6e1d7f0c512b64feedca0905288abefc95f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL deoverlap-4.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 423.5 kB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
63b05e77a8e89584ba54c0d7764769b91d8f976757cdb63ea0a2cf8c7fc2e4ff
BLAKE2b-256 checksum
How to use checksums
c1359a76cd1f3e6ae57db8c72bfebb6bda7047d96c6fc8971432f535dfb4c6a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL deoverlap-4.2.0-cp310-abi3-macosx_11_0_arm64.whl
Size 385.3 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
fd4d6e472b8c43e5a6e6444df0e66fce60d5176ecffe9a9881d65f1a3cc8305a
BLAKE2b-256 checksum
How to use checksums
fc81d00952c638c4945f3b0e2189208b7586c33d2ca599969fc6868698ba3c01
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / deoverlap-4.2.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL deoverlap-4.2.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 391.2 kB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
2a533e7003a5a7e9477675d9ddb6c7c134811ab24b7f18537cda3c65c99ad619
BLAKE2b-256 checksum
How to use checksums
f22f2f3becc5acc5ae92c126113f8bd4e8fb9e66732af1a198d05dd66c8b557b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Security for deoverlap 4.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page