deoverlap 3.2.2__tar.gz → 4.1.0__tar.gz
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.
- deoverlap-4.1.0/Cargo.lock +210 -0
- deoverlap-4.1.0/Cargo.toml +6 -0
- deoverlap-4.1.0/PKG-INFO +336 -0
- deoverlap-4.1.0/README.md +305 -0
- deoverlap-4.1.0/bindings/python/Cargo.toml +16 -0
- deoverlap-4.1.0/bindings/python/src/lib.rs +119 -0
- deoverlap-4.1.0/core/Cargo.toml +10 -0
- deoverlap-4.1.0/core/src/geom.rs +175 -0
- deoverlap-4.1.0/core/src/lib.rs +635 -0
- deoverlap-4.1.0/core/src/mask.rs +139 -0
- deoverlap-4.1.0/core/src/merge.rs +109 -0
- deoverlap-4.1.0/core/tests/behaviour.rs +288 -0
- {deoverlap-3.2.2 → deoverlap-4.1.0}/pyproject.toml +11 -5
- {deoverlap-3.2.2 → deoverlap-4.1.0}/src/deoverlap/__init__.py +3 -5
- deoverlap-4.1.0/src/deoverlap/deoverlap.py +247 -0
- {deoverlap-3.2.2 → deoverlap-4.1.0}/src/deoverlap/vpype_plugin.py +47 -72
- deoverlap-3.2.2/PKG-INFO +0 -353
- deoverlap-3.2.2/README.md +0 -304
- deoverlap-3.2.2/setup.cfg +0 -4
- deoverlap-3.2.2/src/deoverlap/deoverlap.py +0 -743
- deoverlap-3.2.2/src/deoverlap.egg-info/PKG-INFO +0 -353
- deoverlap-3.2.2/src/deoverlap.egg-info/SOURCES.txt +0 -14
- deoverlap-3.2.2/src/deoverlap.egg-info/dependency_links.txt +0 -1
- deoverlap-3.2.2/src/deoverlap.egg-info/entry_points.txt +0 -2
- deoverlap-3.2.2/src/deoverlap.egg-info/requires.txt +0 -10
- deoverlap-3.2.2/src/deoverlap.egg-info/top_level.txt +0 -1
- deoverlap-3.2.2/tests/test_deoverlap.py +0 -245
- deoverlap-3.2.2/tests/test_vpype_plugin.py +0 -48
- {deoverlap-3.2.2 → deoverlap-4.1.0}/LICENSE +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "autocfg"
|
|
7
|
+
version = "1.5.1"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "byteorder"
|
|
13
|
+
version = "1.5.0"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "deoverlap-core"
|
|
19
|
+
version = "4.1.0"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"rstar",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[[package]]
|
|
25
|
+
name = "deoverlap-python"
|
|
26
|
+
version = "4.1.0"
|
|
27
|
+
dependencies = [
|
|
28
|
+
"deoverlap-core",
|
|
29
|
+
"pyo3",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "hash32"
|
|
34
|
+
version = "0.3.1"
|
|
35
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
+
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
|
37
|
+
dependencies = [
|
|
38
|
+
"byteorder",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "heapless"
|
|
43
|
+
version = "0.8.0"
|
|
44
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
45
|
+
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
|
46
|
+
dependencies = [
|
|
47
|
+
"hash32",
|
|
48
|
+
"stable_deref_trait",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[[package]]
|
|
52
|
+
name = "heck"
|
|
53
|
+
version = "0.5.0"
|
|
54
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
55
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
56
|
+
|
|
57
|
+
[[package]]
|
|
58
|
+
name = "libc"
|
|
59
|
+
version = "0.2.189"
|
|
60
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
61
|
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|
62
|
+
|
|
63
|
+
[[package]]
|
|
64
|
+
name = "libm"
|
|
65
|
+
version = "0.2.16"
|
|
66
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
67
|
+
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
|
68
|
+
|
|
69
|
+
[[package]]
|
|
70
|
+
name = "num-traits"
|
|
71
|
+
version = "0.2.19"
|
|
72
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
73
|
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
74
|
+
dependencies = [
|
|
75
|
+
"autocfg",
|
|
76
|
+
"libm",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[[package]]
|
|
80
|
+
name = "once_cell"
|
|
81
|
+
version = "1.21.4"
|
|
82
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
83
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
84
|
+
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "portable-atomic"
|
|
87
|
+
version = "1.15.0"
|
|
88
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
90
|
+
|
|
91
|
+
[[package]]
|
|
92
|
+
name = "proc-macro2"
|
|
93
|
+
version = "1.0.107"
|
|
94
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
95
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
96
|
+
dependencies = [
|
|
97
|
+
"unicode-ident",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[[package]]
|
|
101
|
+
name = "pyo3"
|
|
102
|
+
version = "0.29.2"
|
|
103
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
104
|
+
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
|
105
|
+
dependencies = [
|
|
106
|
+
"libc",
|
|
107
|
+
"once_cell",
|
|
108
|
+
"portable-atomic",
|
|
109
|
+
"pyo3-build-config",
|
|
110
|
+
"pyo3-ffi",
|
|
111
|
+
"pyo3-macros",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[[package]]
|
|
115
|
+
name = "pyo3-build-config"
|
|
116
|
+
version = "0.29.2"
|
|
117
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
118
|
+
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
|
119
|
+
dependencies = [
|
|
120
|
+
"target-lexicon",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[[package]]
|
|
124
|
+
name = "pyo3-ffi"
|
|
125
|
+
version = "0.29.2"
|
|
126
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
127
|
+
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
|
128
|
+
dependencies = [
|
|
129
|
+
"libc",
|
|
130
|
+
"pyo3-build-config",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
[[package]]
|
|
134
|
+
name = "pyo3-macros"
|
|
135
|
+
version = "0.29.2"
|
|
136
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
137
|
+
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
|
138
|
+
dependencies = [
|
|
139
|
+
"proc-macro2",
|
|
140
|
+
"pyo3-macros-backend",
|
|
141
|
+
"quote",
|
|
142
|
+
"syn",
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
[[package]]
|
|
146
|
+
name = "pyo3-macros-backend"
|
|
147
|
+
version = "0.29.2"
|
|
148
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
149
|
+
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
|
150
|
+
dependencies = [
|
|
151
|
+
"heck",
|
|
152
|
+
"proc-macro2",
|
|
153
|
+
"quote",
|
|
154
|
+
"syn",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
[[package]]
|
|
158
|
+
name = "quote"
|
|
159
|
+
version = "1.0.47"
|
|
160
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
161
|
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
|
162
|
+
dependencies = [
|
|
163
|
+
"proc-macro2",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
[[package]]
|
|
167
|
+
name = "rstar"
|
|
168
|
+
version = "0.12.2"
|
|
169
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
170
|
+
checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb"
|
|
171
|
+
dependencies = [
|
|
172
|
+
"heapless",
|
|
173
|
+
"num-traits",
|
|
174
|
+
"smallvec",
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
[[package]]
|
|
178
|
+
name = "smallvec"
|
|
179
|
+
version = "1.16.2"
|
|
180
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
181
|
+
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
|
182
|
+
|
|
183
|
+
[[package]]
|
|
184
|
+
name = "stable_deref_trait"
|
|
185
|
+
version = "1.2.1"
|
|
186
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
187
|
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|
188
|
+
|
|
189
|
+
[[package]]
|
|
190
|
+
name = "syn"
|
|
191
|
+
version = "2.0.119"
|
|
192
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
193
|
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
|
194
|
+
dependencies = [
|
|
195
|
+
"proc-macro2",
|
|
196
|
+
"quote",
|
|
197
|
+
"unicode-ident",
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
[[package]]
|
|
201
|
+
name = "target-lexicon"
|
|
202
|
+
version = "0.13.5"
|
|
203
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
204
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
205
|
+
|
|
206
|
+
[[package]]
|
|
207
|
+
name = "unicode-ident"
|
|
208
|
+
version = "1.0.26"
|
|
209
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
210
|
+
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
deoverlap-4.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deoverlap
|
|
3
|
+
Version: 4.1.0
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Dist: shapely>=2.0
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
18
|
+
Requires-Dist: matplotlib ; extra == 'test'
|
|
19
|
+
Requires-Dist: vpype>=1.13 ; extra == 'test'
|
|
20
|
+
Requires-Dist: vpype>=1.13 ; extra == 'vpype'
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Provides-Extra: vpype
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Summary: De-overlap Shapely geometries that sit within a tolerance of each other.
|
|
25
|
+
Author-email: Pietro Leoni <pietro.leoni@gmail.com>
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
28
|
+
Project-URL: Bug Tracker, https://github.com/piLeoni/deoverlap/issues
|
|
29
|
+
Project-URL: Homepage, https://github.com/piLeoni/deoverlap
|
|
30
|
+
|
|
31
|
+
# Deoverlap
|
|
32
|
+
|
|
33
|
+
De-overlap Shapely geometries that sit within a tolerance of each other.
|
|
34
|
+
|
|
35
|
+
Source, examples and issues: [github.com/piLeoni/deoverlap](https://github.com/piLeoni/deoverlap)
|
|
36
|
+
|
|
37
|
+
The common case is pen-plotter work: two strokes closer than a pen width
|
|
38
|
+
visually merge on paper, so only one of them should keep the ink. Unlike
|
|
39
|
+
endpoint-only “deduplicate” tools, this library builds a **corridor** around
|
|
40
|
+
each kept stroke and crops (or drops) later strokes that fall inside it.
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
Oakland's MacArthur Maze drawn with a 0.5 mm pen. In the zoom (the dashed box
|
|
47
|
+
on the card), doubled carriageways and junctions darken where the pen passes
|
|
48
|
+
twice (left); deoverlap removes the red strokes (middle) and the ink becomes
|
|
49
|
+
one even layer (right). See [A real map](#a-real-map-the-macarthur-maze) below.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install deoverlap
|
|
53
|
+
# with the vpype command:
|
|
54
|
+
pip install "deoverlap[vpype]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from shapely.geometry import LineString
|
|
61
|
+
from deoverlap import deoverlap
|
|
62
|
+
|
|
63
|
+
geoms = [
|
|
64
|
+
LineString([(0, 0), (2, 0)]),
|
|
65
|
+
LineString([(1, 0.05), (3, 0.05)]), # parallel, 0.05 away
|
|
66
|
+
]
|
|
67
|
+
result = deoverlap(geoms, tolerance=0.1)
|
|
68
|
+
|
|
69
|
+
print(len(result.kept), "surviving geometries")
|
|
70
|
+
print("wholly removed:", result.wholly_removed)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`tolerance` is a distance in the same units as the geometries: strokes closer
|
|
74
|
+
than this to a kept stroke are cut. For a plotter, use the pen width.
|
|
75
|
+
|
|
76
|
+
The Python function and the vpype command take the same options with the same
|
|
77
|
+
defaults; `--self-overlap` on the command line is `self_overlap=True` in Python.
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
Geometries are processed in priority order. Each kept stroke gets a corridor
|
|
82
|
+
of radius `tolerance` around it, and every later stroke loses the parts that
|
|
83
|
+
fall inside a corridor.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
In every figure, blue is kept, red is removed and the thin orange outline is
|
|
88
|
+
the corridor mask. They are rendered by `examples/make_figures.py`.
|
|
89
|
+
|
|
90
|
+
## Prefer — which stroke wins
|
|
91
|
+
|
|
92
|
+
When two corridors collide, priority is explicit:
|
|
93
|
+
|
|
94
|
+
| `prefer=` | Behaviour |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `longest` (default) | The stroke that covers more ground wins |
|
|
97
|
+
| `first` | Input order |
|
|
98
|
+
| `shortest` | Short marks / detail win |
|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
102
|
+
Original indices are preserved in `result.kept_parts` / `removed_parts` even
|
|
103
|
+
when processing order changes.
|
|
104
|
+
|
|
105
|
+
## Angle — which strokes count as overlapping
|
|
106
|
+
|
|
107
|
+
Two strokes overlap only where their directions differ by at most `angle`
|
|
108
|
+
degrees (default 30). A plotter redrawing a crossing is usually fine; the pain
|
|
109
|
+
is parallel near-coincident runs. `angle=90` counts every nearby stroke, so
|
|
110
|
+
crossings get cut too.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
result = deoverlap(geoms, tolerance=0.1, angle=30)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Directions are compared **locally, edge by edge**, so a curving ramp is cropped
|
|
117
|
+
only where it actually runs alongside another road, whatever direction its two
|
|
118
|
+
ends point in.
|
|
119
|
+
|
|
120
|
+
Where two strokes meet at an angle θ, they overlap for roughly `pen / sin θ`:
|
|
121
|
+
about 2 pen widths at 30° and 4 at 15°. The default of 30° catches parallel
|
|
122
|
+
runs and trims shallow merges; 45° or 60° trim steeper merges too.
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+
## Crop or drop
|
|
127
|
+
|
|
128
|
+
By default the overlap is cut away and the rest of the stroke is kept. With
|
|
129
|
+
`drop=0.5`, a stroke that would lose more than half its length is discarded
|
|
130
|
+
whole instead of leaving stubs.
|
|
131
|
+
|
|
132
|
+

|
|
133
|
+
|
|
134
|
+
`min_length=` drops pieces shorter than that after cutting (points are never
|
|
135
|
+
removed by it).
|
|
136
|
+
|
|
137
|
+
## Split pieces stay one object
|
|
138
|
+
|
|
139
|
+
When a ring is cut by a corridor it becomes two arcs. Those arcs are returned
|
|
140
|
+
as **one** multipart geometry under the original input index — a logical
|
|
141
|
+
stroke, not two anonymous objects:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
ring = LineString([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
|
|
145
|
+
cutter = LineString([(1, -1), (1, 3)])
|
|
146
|
+
result = deoverlap([cutter, ring], 0.15, prefer="first", angle=90)
|
|
147
|
+
|
|
148
|
+
result.kept_parts[1] # MultiLineString of both arcs
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use `flatten_geometries(result.kept)` for a flat list of primitive pieces.
|
|
152
|
+
|
|
153
|
+

|
|
154
|
+
|
|
155
|
+
## Self-overlap
|
|
156
|
+
|
|
157
|
+
A thin road outline is often **one** LineString (left kerb → end cap → right
|
|
158
|
+
kerb). Normally deoverlap never compares a geometry to itself, so the two sides
|
|
159
|
+
stay as a heavy double stroke. With `self_overlap=True` every edge is its own
|
|
160
|
+
corridor, so opposite sides can suppress each other. Neighbouring edges never
|
|
161
|
+
cut each other, so joints are not nibbled, unless the path folds back on itself
|
|
162
|
+
like a hairpin.
|
|
163
|
+
|
|
164
|
+

|
|
165
|
+
|
|
166
|
+
## Multi-stage with a carried mask
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
r1 = deoverlap(batch1, 0.1)
|
|
170
|
+
r2 = deoverlap(batch2, 0.1, mask=r1.mask)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Stage 2 is clipped against everything stage 1 kept.
|
|
174
|
+
|
|
175
|
+
## Result
|
|
176
|
+
|
|
177
|
+
`deoverlap(...)` returns a `DeoverlapResult`:
|
|
178
|
+
|
|
179
|
+
| Field | Meaning |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `kept` | Surviving geometries, one per input that kept some ink |
|
|
182
|
+
| `removed` | Cut pieces, flattened (if `keep_duplicates=True`) |
|
|
183
|
+
| `kept_parts` | `{original_index: geometry}` |
|
|
184
|
+
| `removed_parts` | `{original_index: geometry}` (if `keep_duplicates=True`) |
|
|
185
|
+
| `wholly_removed` | Indices removed entirely |
|
|
186
|
+
| `mask` | Corridor polygons (for the next stage) |
|
|
187
|
+
|
|
188
|
+
## Performance
|
|
189
|
+
|
|
190
|
+
The engine is written in Rust and ships as a compiled extension, so
|
|
191
|
+
`pip install` needs no Rust toolchain on the supported platforms. It builds no
|
|
192
|
+
polygons: the corridor of a straight edge is a capsule, and the part of
|
|
193
|
+
another edge inside it is computed exactly. On the MacArthur Maze map below
|
|
194
|
+
(1473 paths) a run takes about 30 ms.
|
|
195
|
+
|
|
196
|
+
## vpype plugin
|
|
197
|
+
|
|
198
|
+
Installing `deoverlap[vpype]` registers a `deoverlap` command that is
|
|
199
|
+
**layer-safe** (honours `-l`) and uses corridor proximity rather than
|
|
200
|
+
endpoint-only matching:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
pip install "deoverlap[vpype]"
|
|
204
|
+
vpype read map.svg deoverlap -t 0.1mm -l 1,2,3 write out.svg
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| Flag | Default | Meaning |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `-t`, `--tolerance` | `0.1mm` | Corridor radius, usually the pen width |
|
|
210
|
+
| `--prefer` | `longest` | `longest` \| `first` \| `shortest` |
|
|
211
|
+
| `--angle` | `30` | Max direction difference in degrees; `90` cuts crossings too |
|
|
212
|
+
| `--self-overlap` | off | Let a path overlap itself |
|
|
213
|
+
| `-m`, `--min-length` | `0mm` | Drop pieces shorter than this after cutting |
|
|
214
|
+
| `--drop` | off | Discard a stroke when more than this fraction would be cut |
|
|
215
|
+
| `-k`, `--keep-duplicates` | off | Keep removed pieces in a separate layer |
|
|
216
|
+
| `-p`, `--progress-bar` | off | Display a progress bar |
|
|
217
|
+
| `-l`, `--layer` | `all` | Target layer(s) |
|
|
218
|
+
|
|
219
|
+
`-t`, `-l`, `-k` and `-p` mean the same as in vpype's own commands and the
|
|
220
|
+
`deduplicate` plugin; `-m` matches `vpype filter`.
|
|
221
|
+
|
|
222
|
+
### Walkthrough with the bundled example
|
|
223
|
+
|
|
224
|
+
`examples/parallel_strokes.svg` is a tiny card with two near-parallel pairs
|
|
225
|
+
(0.05 mm apart), one deliberate cross, and a dual-kerb rectangle drawn as a
|
|
226
|
+
**single** polyline. No OSM dump required — open it, run the commands, open the
|
|
227
|
+
outputs side by side.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Inspect path count / drawn length
|
|
231
|
+
vpype read examples/parallel_strokes.svg stat
|
|
232
|
+
|
|
233
|
+
# Crop near-parallel duplicates; leave the perpendicular cross alone
|
|
234
|
+
vpype read examples/parallel_strokes.svg \
|
|
235
|
+
deoverlap -t 0.1mm -l 1 \
|
|
236
|
+
write examples/out_parallel.svg
|
|
237
|
+
|
|
238
|
+
# Same, but also let the dual-kerb ring collapse against itself
|
|
239
|
+
vpype read examples/parallel_strokes.svg \
|
|
240
|
+
deoverlap -t 0.12mm --self-overlap -l 1 \
|
|
241
|
+
write examples/out_self_overlap.svg
|
|
242
|
+
|
|
243
|
+
vpype read examples/out_parallel.svg stat
|
|
244
|
+
vpype read examples/out_self_overlap.svg stat
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
What to look for (in the SVG or in `stat`’s drawn length):
|
|
248
|
+
|
|
249
|
+
1. **Before** — each parallel pair is a double stroke; the ring reads as a heavy
|
|
250
|
+
double outline.
|
|
251
|
+
2. **`out_parallel.svg`** — each pair reduced to one survivor; the cross still
|
|
252
|
+
there; the ring still double (one geometry cannot overlap itself).
|
|
253
|
+
3. **`out_self_overlap.svg`** — the ring’s opposite sides suppress each other,
|
|
254
|
+
so the dual kerb thins toward a single outline.
|
|
255
|
+
|
|
256
|
+
### A real map: the MacArthur Maze
|
|
257
|
+
|
|
258
|
+
`examples/macarthur_maze.svg` is a 3 km square of Oakland's MacArthur Maze
|
|
259
|
+
interchange from OpenStreetMap, scaled onto a 100 mm card (1 mm ≈ 33 m). At
|
|
260
|
+
that scale dual carriageways, stacked ramps and frontage roads sit a fraction
|
|
261
|
+
of a millimetre apart, so a 0.5 mm pen draws the same paper two or three
|
|
262
|
+
times. Set the tolerance to the pen width: anything closer than that would
|
|
263
|
+
overlap on paper.
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
vpype read examples/macarthur_maze.svg stat
|
|
267
|
+
vpype read examples/macarthur_maze.svg \
|
|
268
|
+
deoverlap -t 0.5mm --angle 90 -m 1mm \
|
|
269
|
+
write examples/out_maze.svg
|
|
270
|
+
vpype read examples/out_maze.svg stat
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`--angle 90` is the maximum: every nearby stroke counts, whatever its
|
|
274
|
+
direction. Side streets stop where the main road's ink begins, and at crossings
|
|
275
|
+
the shorter path is split around the longer one. The gap is exactly the other
|
|
276
|
+
road's ink width, so on paper the crossing still looks whole, just without the
|
|
277
|
+
dark spot of doubled ink.
|
|
278
|
+
|
|
279
|
+
Cutting leaves fragments; almost every piece under 1 mm on this card is one.
|
|
280
|
+
`-m 1mm` (two pen widths) drops them. The path count goes from 1473 to 860 and
|
|
281
|
+
the drawn length drops by 35%.
|
|
282
|
+
|
|
283
|
+
The images at the top of this page show this run. Their ink panels are blended
|
|
284
|
+
like real ink: every pass multiplies the colour, so the darker the blue, the
|
|
285
|
+
more times the pen went over the same spot. Before, the dark bands are doubled
|
|
286
|
+
carriageways and the dark dots are junctions, where a round pen tip lands on
|
|
287
|
+
ink that is already there. After, the ink is one even layer.
|
|
288
|
+
|
|
289
|
+
With the default `--angle 30` crossings are left alone: parallel runs are
|
|
290
|
+
still merged, but both roads are drawn through every junction, and the card
|
|
291
|
+
loses about 23% instead of 35%. The same zoom, crossings kept and cut:
|
|
292
|
+
|
|
293
|
+

|
|
294
|
+
|
|
295
|
+
Keep crossings when every stroke must stay continuous (e.g. for later
|
|
296
|
+
editing); cut them when only the ink on paper matters.
|
|
297
|
+
|
|
298
|
+
To try another place (needs `pip install osmnx`):
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
python examples/fetch_osm_example.py --lat 51.5074 --lon -0.1278 --radius 1500 \
|
|
302
|
+
--out examples/my_place.svg
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Map data © OpenStreetMap contributors, available under the
|
|
306
|
+
[ODbL](https://www.openstreetmap.org/copyright).
|
|
307
|
+
|
|
308
|
+
### Different settings per layer
|
|
309
|
+
|
|
310
|
+
Chain the command to give each layer its own settings:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
vpype read map.svg \
|
|
314
|
+
deoverlap -t 0.1mm -l 3,4 \
|
|
315
|
+
deoverlap -t 0.15mm --self-overlap -l 5 \
|
|
316
|
+
write opt.svg
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## API
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
deoverlap(
|
|
323
|
+
geometries,
|
|
324
|
+
tolerance,
|
|
325
|
+
*,
|
|
326
|
+
prefer="longest", # longest | first | shortest
|
|
327
|
+
angle=30.0, # degrees, 0–90; 90 cuts crossings too
|
|
328
|
+
self_overlap=False,
|
|
329
|
+
min_length=0.0,
|
|
330
|
+
drop=None, # fraction 0–1; None always crops
|
|
331
|
+
keep_duplicates=False,
|
|
332
|
+
progress_bar=False,
|
|
333
|
+
mask=None,
|
|
334
|
+
) -> DeoverlapResult
|
|
335
|
+
```
|
|
336
|
+
|