nooverlap 0.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.
@@ -0,0 +1,169 @@
1
+ # This file is autogenerated by maturin v1.7.0
2
+ # To update, run
3
+ #
4
+ # maturin generate-ci github
5
+ #
6
+ name: CI
7
+
8
+ on:
9
+ push:
10
+ branches:
11
+ - main
12
+ - master
13
+ tags:
14
+ - '*'
15
+ pull_request:
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ linux:
23
+ runs-on: ${{ matrix.platform.runner }}
24
+ strategy:
25
+ matrix:
26
+ platform:
27
+ - runner: ubuntu-latest
28
+ target: x86_64
29
+ - runner: ubuntu-latest
30
+ target: x86
31
+ - runner: ubuntu-latest
32
+ target: aarch64
33
+ - runner: ubuntu-latest
34
+ target: armv7
35
+ - runner: ubuntu-latest
36
+ target: s390x
37
+ - runner: ubuntu-latest
38
+ target: ppc64le
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: 3.x
44
+ - name: Build wheels
45
+ uses: PyO3/maturin-action@v1
46
+ with:
47
+ target: ${{ matrix.platform.target }}
48
+ args: --release --out dist --find-interpreter
49
+ sccache: 'true'
50
+ manylinux: auto
51
+ - name: Upload wheels
52
+ uses: actions/upload-artifact@v4
53
+ with:
54
+ name: wheels-linux-${{ matrix.platform.target }}
55
+ path: dist
56
+
57
+ musllinux:
58
+ runs-on: ${{ matrix.platform.runner }}
59
+ strategy:
60
+ matrix:
61
+ platform:
62
+ - runner: ubuntu-latest
63
+ target: x86_64
64
+ - runner: ubuntu-latest
65
+ target: x86
66
+ - runner: ubuntu-latest
67
+ target: aarch64
68
+ - runner: ubuntu-latest
69
+ target: armv7
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - uses: actions/setup-python@v5
73
+ with:
74
+ python-version: 3.x
75
+ - name: Build wheels
76
+ uses: PyO3/maturin-action@v1
77
+ with:
78
+ target: ${{ matrix.platform.target }}
79
+ args: --release --out dist --find-interpreter
80
+ sccache: 'true'
81
+ manylinux: musllinux_1_2
82
+ - name: Upload wheels
83
+ uses: actions/upload-artifact@v4
84
+ with:
85
+ name: wheels-musllinux-${{ matrix.platform.target }}
86
+ path: dist
87
+
88
+ windows:
89
+ runs-on: ${{ matrix.platform.runner }}
90
+ strategy:
91
+ matrix:
92
+ platform:
93
+ - runner: windows-latest
94
+ target: x64
95
+ - runner: windows-latest
96
+ target: x86
97
+ steps:
98
+ - uses: actions/checkout@v4
99
+ - uses: actions/setup-python@v5
100
+ with:
101
+ python-version: 3.x
102
+ architecture: ${{ matrix.platform.target }}
103
+ - name: Build wheels
104
+ uses: PyO3/maturin-action@v1
105
+ with:
106
+ target: ${{ matrix.platform.target }}
107
+ args: --release --out dist --find-interpreter
108
+ sccache: 'true'
109
+ - name: Upload wheels
110
+ uses: actions/upload-artifact@v4
111
+ with:
112
+ name: wheels-windows-${{ matrix.platform.target }}
113
+ path: dist
114
+
115
+ macos:
116
+ runs-on: ${{ matrix.platform.runner }}
117
+ strategy:
118
+ matrix:
119
+ platform:
120
+ - runner: macos-12
121
+ target: x86_64
122
+ - runner: macos-14
123
+ target: aarch64
124
+ steps:
125
+ - uses: actions/checkout@v4
126
+ - uses: actions/setup-python@v5
127
+ with:
128
+ python-version: 3.x
129
+ - name: Build wheels
130
+ uses: PyO3/maturin-action@v1
131
+ with:
132
+ target: ${{ matrix.platform.target }}
133
+ args: --release --out dist --find-interpreter
134
+ sccache: 'true'
135
+ - name: Upload wheels
136
+ uses: actions/upload-artifact@v4
137
+ with:
138
+ name: wheels-macos-${{ matrix.platform.target }}
139
+ path: dist
140
+
141
+ sdist:
142
+ runs-on: ubuntu-latest
143
+ steps:
144
+ - uses: actions/checkout@v4
145
+ - name: Build sdist
146
+ uses: PyO3/maturin-action@v1
147
+ with:
148
+ command: sdist
149
+ args: --out dist
150
+ - name: Upload sdist
151
+ uses: actions/upload-artifact@v4
152
+ with:
153
+ name: wheels-sdist
154
+ path: dist
155
+
156
+ release:
157
+ name: Release
158
+ runs-on: ubuntu-latest
159
+ permissions:
160
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
161
+ if: "startsWith(github.ref, 'refs/tags/')"
162
+ needs: [linux, musllinux, windows, macos, sdist]
163
+ steps:
164
+ - uses: actions/download-artifact@v4
165
+ - name: Publish to PyPI
166
+ uses: PyO3/maturin-action@v1
167
+ with:
168
+ command: upload
169
+ args: --non-interactive --skip-existing wheels-*/*
@@ -0,0 +1,72 @@
1
+ /target
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ .pytest_cache/
6
+ *.py[cod]
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ .venv/
14
+ env/
15
+ bin/
16
+ build/
17
+ develop-eggs/
18
+ dist/
19
+ eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ include/
26
+ man/
27
+ venv/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+ pip-selfcheck.json
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .coverage
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+
45
+ # Translations
46
+ *.mo
47
+
48
+ # Mr Developer
49
+ .mr.developer.cfg
50
+ .project
51
+ .pydevproject
52
+
53
+ # Rope
54
+ .ropeproject
55
+
56
+ # Django stuff:
57
+ *.log
58
+ *.pot
59
+
60
+ .DS_Store
61
+
62
+ # Sphinx documentation
63
+ docs/_build/
64
+
65
+ # PyCharm
66
+ .idea/
67
+
68
+ # VSCode
69
+ .vscode/
70
+
71
+ # Pyenv
72
+ .python-version
@@ -0,0 +1,171 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 3
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.3.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
10
+
11
+ [[package]]
12
+ name = "cfg-if"
13
+ version = "1.0.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
16
+
17
+ [[package]]
18
+ name = "heck"
19
+ version = "0.5.0"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
22
+
23
+ [[package]]
24
+ name = "indoc"
25
+ version = "2.0.5"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
28
+
29
+ [[package]]
30
+ name = "libc"
31
+ version = "0.2.158"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
34
+
35
+ [[package]]
36
+ name = "memoffset"
37
+ version = "0.9.1"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
40
+ dependencies = [
41
+ "autocfg",
42
+ ]
43
+
44
+ [[package]]
45
+ name = "nooverlap"
46
+ version = "0.1.0"
47
+ dependencies = [
48
+ "pyo3",
49
+ ]
50
+
51
+ [[package]]
52
+ name = "once_cell"
53
+ version = "1.19.0"
54
+ source = "registry+https://github.com/rust-lang/crates.io-index"
55
+ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
56
+
57
+ [[package]]
58
+ name = "portable-atomic"
59
+ version = "1.7.0"
60
+ source = "registry+https://github.com/rust-lang/crates.io-index"
61
+ checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265"
62
+
63
+ [[package]]
64
+ name = "proc-macro2"
65
+ version = "1.0.86"
66
+ source = "registry+https://github.com/rust-lang/crates.io-index"
67
+ checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
68
+ dependencies = [
69
+ "unicode-ident",
70
+ ]
71
+
72
+ [[package]]
73
+ name = "pyo3"
74
+ version = "0.22.2"
75
+ source = "registry+https://github.com/rust-lang/crates.io-index"
76
+ checksum = "831e8e819a138c36e212f3af3fd9eeffed6bf1510a805af35b0edee5ffa59433"
77
+ dependencies = [
78
+ "cfg-if",
79
+ "indoc",
80
+ "libc",
81
+ "memoffset",
82
+ "once_cell",
83
+ "portable-atomic",
84
+ "pyo3-build-config",
85
+ "pyo3-ffi",
86
+ "pyo3-macros",
87
+ "unindent",
88
+ ]
89
+
90
+ [[package]]
91
+ name = "pyo3-build-config"
92
+ version = "0.22.2"
93
+ source = "registry+https://github.com/rust-lang/crates.io-index"
94
+ checksum = "1e8730e591b14492a8945cdff32f089250b05f5accecf74aeddf9e8272ce1fa8"
95
+ dependencies = [
96
+ "once_cell",
97
+ "target-lexicon",
98
+ ]
99
+
100
+ [[package]]
101
+ name = "pyo3-ffi"
102
+ version = "0.22.2"
103
+ source = "registry+https://github.com/rust-lang/crates.io-index"
104
+ checksum = "5e97e919d2df92eb88ca80a037969f44e5e70356559654962cbb3316d00300c6"
105
+ dependencies = [
106
+ "libc",
107
+ "pyo3-build-config",
108
+ ]
109
+
110
+ [[package]]
111
+ name = "pyo3-macros"
112
+ version = "0.22.2"
113
+ source = "registry+https://github.com/rust-lang/crates.io-index"
114
+ checksum = "eb57983022ad41f9e683a599f2fd13c3664d7063a3ac5714cae4b7bee7d3f206"
115
+ dependencies = [
116
+ "proc-macro2",
117
+ "pyo3-macros-backend",
118
+ "quote",
119
+ "syn",
120
+ ]
121
+
122
+ [[package]]
123
+ name = "pyo3-macros-backend"
124
+ version = "0.22.2"
125
+ source = "registry+https://github.com/rust-lang/crates.io-index"
126
+ checksum = "ec480c0c51ddec81019531705acac51bcdbeae563557c982aa8263bb96880372"
127
+ dependencies = [
128
+ "heck",
129
+ "proc-macro2",
130
+ "pyo3-build-config",
131
+ "quote",
132
+ "syn",
133
+ ]
134
+
135
+ [[package]]
136
+ name = "quote"
137
+ version = "1.0.37"
138
+ source = "registry+https://github.com/rust-lang/crates.io-index"
139
+ checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
140
+ dependencies = [
141
+ "proc-macro2",
142
+ ]
143
+
144
+ [[package]]
145
+ name = "syn"
146
+ version = "2.0.77"
147
+ source = "registry+https://github.com/rust-lang/crates.io-index"
148
+ checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
149
+ dependencies = [
150
+ "proc-macro2",
151
+ "quote",
152
+ "unicode-ident",
153
+ ]
154
+
155
+ [[package]]
156
+ name = "target-lexicon"
157
+ version = "0.12.16"
158
+ source = "registry+https://github.com/rust-lang/crates.io-index"
159
+ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
160
+
161
+ [[package]]
162
+ name = "unicode-ident"
163
+ version = "1.0.12"
164
+ source = "registry+https://github.com/rust-lang/crates.io-index"
165
+ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
166
+
167
+ [[package]]
168
+ name = "unindent"
169
+ version = "0.2.3"
170
+ source = "registry+https://github.com/rust-lang/crates.io-index"
171
+ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "nooverlap"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7
+ [lib]
8
+ name = "nooverlap"
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ pyo3 = "0.22.0"
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.3
2
+ Name: nooverlap
3
+ Version: 0.1.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Requires-Python: >=3.8
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.7,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "nooverlap"
7
+ requires-python = ">=3.8"
8
+ classifiers = [
9
+ "Programming Language :: Rust",
10
+ "Programming Language :: Python :: Implementation :: CPython",
11
+ "Programming Language :: Python :: Implementation :: PyPy",
12
+ ]
13
+ dynamic = ["version"]
14
+ [tool.maturin]
15
+ features = ["pyo3/extension-module"]
@@ -0,0 +1,47 @@
1
+ import nooverlap
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.animation import FuncAnimation
5
+
6
+ np.random.seed(0)
7
+
8
+ # make some random box data
9
+ n = 70
10
+ x = 5*np.random.rand(n)
11
+ y = 2*np.random.rand(n)
12
+ w = np.random.rand(n)
13
+ h = 0.3*np.random.rand(n)
14
+
15
+ # create a nooverlap object
16
+ no = nooverlap.Pusher()
17
+
18
+ # add the boxes
19
+ for i in range(n):
20
+ no.add_box(x[i], y[i], w[i]/2, w[i]/2, h[i]/2, h[i]/2)
21
+
22
+ # draw the boxes
23
+ fig, ax = plt.subplots()
24
+
25
+ def step(i):
26
+
27
+
28
+ no.push_elements(0.01, 0.1)
29
+
30
+ ax.clear()
31
+ ax.set_xlim(-1,6)
32
+ ax.set_ylim(-0.5,2.5)
33
+ for i in range(n):
34
+ _x,_y = no.get_position(i)
35
+ _w = w[i]
36
+ _h = h[i]
37
+ ax.add_patch(plt.Rectangle((_x-0.5*_w, _y-0.5*_h), width=_w, height=_h , facecolor='lightblue', edgecolor='black'))
38
+ ax.plot([_x,x[i]],[_y,y[i]])
39
+
40
+
41
+
42
+ anim = FuncAnimation(fig,
43
+ step, frames=100, interval=1)
44
+
45
+
46
+ plt.show()
47
+
@@ -0,0 +1,180 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ np.random.seed(0)
5
+ x, y = np.random.random((2,80))
6
+
7
+ # x = [0.5488135039273248,0.7151893663724195,0.6027633760716439,0.5448831829968969,0.4236547993389047,0.6458941130666561,0.4375872112626925,0.8917730007820798,0.9636627605010293,0.3834415188257777,0.7917250380826646,0.5288949197529045,0.5680445610939323,0.925596638292661,0.07103605819788694,0.08712929970154071,0.02021839744032572,0.832619845547938,0.7781567509498505,0.8700121482468192,0.978618342232764,0.7991585642167236,0.46147936225293185,0.7805291762864555,0.11827442586893322,0.6399210213275238,0.1433532874090464,0.9446689170495839,0.5218483217500717,0.4146619399905236]
8
+ # y = [0.26455561210462697,0.7742336894342167,0.45615033221654855,0.5684339488686485,0.018789800436355142,0.6176354970758771,0.6120957227224214,0.6169339968747569,0.9437480785146242,0.6818202991034834,0.359507900573786,0.43703195379934145,0.6976311959272649,0.06022547162926983,0.6667667154456677,0.6706378696181594,0.2103825610738409,0.1289262976548533,0.31542835092418386,0.3637107709426226,0.5701967704178796,0.43860151346232035,0.9883738380592262,0.10204481074802807,0.2088767560948347,0.16130951788499626,0.6531083254653984,0.2532916025397821,0.4663107728563063,0.24442559200160274]
9
+
10
+ # print(','.join([str(n) for n in x]))
11
+ # print(','.join([str(n) for n in y]))
12
+
13
+ fig, ax = plt.subplots()
14
+ plt.plot(x, y, 'bo')
15
+ texts = [plt.text(x[i], y[i], 'Text%s' %i, ha='center', va='center') for i in range(len(x))]
16
+
17
+ # get text sizes
18
+ plt.draw()
19
+ r = fig.canvas.get_renderer()
20
+ expand = (1.0, 1.0)
21
+
22
+ class Box():
23
+ def __init__(self, position, left, right, bottom, top):
24
+ self.x = position[0]
25
+ self.y = position[1]
26
+
27
+ self.x0 = self.x
28
+ self.y0 = self.y
29
+
30
+ self.dl = self.x - left
31
+ self.dr = right - self.x
32
+ self.du = top - self.y
33
+ self.dd = self.y - bottom
34
+ self.to_be_moved = np.array((0,0), dtype=float)
35
+
36
+ def move(self, dx, dy):
37
+ self.x += dx
38
+ self.y += dy
39
+
40
+ def reset(self):
41
+ self.to_be_moved = np.array((0,0), dtype=float)
42
+
43
+ def do_move(self):
44
+ self.move(*self.to_be_moved)
45
+
46
+ @property
47
+ def cx(self):
48
+ return self.x + 0.5 * self.dr - 0.5*self.dl
49
+
50
+ @property
51
+ def cy(self):
52
+ return self.y + 0.5 * self.du - 0.5*self.dd
53
+
54
+ @property
55
+ def width(self):
56
+ return self.dr + self.dl
57
+
58
+ @property
59
+ def height(self):
60
+ return self.du + self.dd
61
+
62
+ @property
63
+ def r(self):
64
+ return ((0.5*self.width)**2 + (0.5*self.height)**2 ) ** (0.5)
65
+
66
+ @property
67
+ def left(self):
68
+ return self.x - self.dl
69
+
70
+ @property
71
+ def right(self):
72
+ return self.x + self.dr
73
+
74
+ @property
75
+ def top(self):
76
+ return self.y + self.du
77
+
78
+ @property
79
+ def bottom(self):
80
+ return self.y - self.dd
81
+
82
+ def plot_home(self, ax):
83
+ ax.plot((self.x, self.x0),
84
+ (self.y, self.y0))
85
+
86
+ def plot_box(self, ax):
87
+
88
+ if self.width == 0 or self.height == 0:
89
+ ax.plot(self.cx, self.cy, 'r.')
90
+ else:
91
+ ax.plot([self.left, self.left, self.right, self.right, self.left],
92
+ [self.bottom, self.top, self.top, self.bottom, self.bottom])
93
+
94
+ def add_force_to_home(self, factor = 0.1):
95
+ dx = self.cx - self.x0
96
+ dy = self.cy - self.y0
97
+
98
+ self.to_be_moved[0] = self.to_be_moved[0] - factor * dx
99
+ self.to_be_moved[1] = self.to_be_moved[1] - factor * dy
100
+
101
+ def add_force_from(self, other, factor = 1.2):
102
+ if other.left > self.right:
103
+ return
104
+ if other.right < self.left:
105
+ return
106
+ if other.top < self.bottom:
107
+ return
108
+ if other.bottom > self.top:
109
+ return
110
+
111
+ self_to_other = (other.cx - self.cx, other.cy - self.cy)
112
+ distance = np.linalg.norm(self_to_other)
113
+
114
+ if distance > 1e-10:
115
+ D = np.array(self_to_other) / np.linalg.norm(self_to_other)
116
+ else:
117
+ D = np.array((0,1),dtype=float)
118
+
119
+ overlap = distance - factor*self.r - factor*other.r
120
+
121
+ self.to_be_moved = self.to_be_moved + 0.5 * overlap * D
122
+
123
+
124
+
125
+
126
+
127
+ boxes = []
128
+ points = []
129
+
130
+ for i in texts:
131
+ ext = i.get_window_extent(r).expanded(*expand).transformed(ax.transData.inverted())
132
+ #
133
+ #
134
+ # plt.plot([ext.xmin, ext.xmax, ext.xmax, ext.xmin, ext.xmin],[ext.ymax, ext.ymax, ext.ymin, ext.ymin, ext.ymax])
135
+ # print(i.get_position())
136
+ # print(ext)
137
+ boxes.append(Box(i.get_position(), left = ext.xmin, right = ext.xmax, top = ext.ymax, bottom = ext.ymin))
138
+
139
+ x,y = i.get_position()
140
+ #
141
+ points.append(Box(i.get_position(), x,x,y,y))
142
+
143
+
144
+ # Position iteratively
145
+ for i in range(100):
146
+
147
+ for b in boxes:
148
+ dx = b.cx - b.x0
149
+ dy = b.cy - b.y0
150
+ b.move(-0.1*dx,-0.1*dy)
151
+
152
+ for b in boxes:
153
+ for bb in boxes:
154
+ if b == bb:
155
+ continue
156
+ b.add_force_from(bb)
157
+
158
+ for p in points:
159
+ b.add_force_from(p)
160
+
161
+ b.do_move()
162
+ b.reset()
163
+
164
+
165
+
166
+ for b in boxes:
167
+ b.plot_box(ax=ax)
168
+ b.plot_home(ax=ax)
169
+
170
+ for p in points:
171
+ p.plot_box(ax=ax)
172
+
173
+
174
+
175
+ for t,b in zip(texts, boxes):
176
+ t.set_position((b.x, b.y))
177
+
178
+
179
+
180
+ plt.show()
@@ -0,0 +1,48 @@
1
+ import nooverlap
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.animation import FuncAnimation
5
+
6
+ np.random.seed(0)
7
+ n = 50
8
+ x = 5*np.random.rand(n)
9
+ y = 2*np.random.rand(n)
10
+ w = np.random.rand(n)
11
+ h = 0.3*np.random.rand(n)
12
+
13
+
14
+ # make some random box data
15
+ def make_data(fac):
16
+
17
+ # create a nooverlap object
18
+ no = nooverlap.Pusher()
19
+
20
+ # add the boxes
21
+ for i in range(n):
22
+ no.add_box(x[i], y[i], w[i]/2, w[i]/2, h[i]/2, h[i]/2)
23
+
24
+ no.push_free(fac, fac)
25
+
26
+ return no
27
+
28
+ # draw the boxes
29
+ fig, ax = plt.subplots()
30
+
31
+
32
+
33
+ ax.set_xlim(-1,6)
34
+ ax.set_ylim(-0.5,2.5)
35
+
36
+ for fac in (0.001, 0.3):
37
+ no = make_data(fac)
38
+ for i in range(n):
39
+ _x,_y = no.get_position(i)
40
+ _w = w[i]
41
+ _h = h[i]
42
+ ax.add_patch(plt.Rectangle((_x-0.5*_w, _y-0.5*_h), width=_w, height=_h , facecolor=(3*fac,3*fac,3*fac), edgecolor='black'))
43
+ ax.plot([_x,x[i]],[_y,y[i]])
44
+
45
+
46
+
47
+ plt.show()
48
+
@@ -0,0 +1,45 @@
1
+ import nooverlap
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.animation import FuncAnimation
5
+
6
+ np.random.seed(0)
7
+
8
+ # make some random box data
9
+ for n in range(100):
10
+
11
+ x = 5*np.random.rand(n)
12
+ y = 2*np.random.rand(n)
13
+ w = np.random.rand(n)
14
+ h = 0.3*np.random.rand(n)
15
+
16
+ # create a nooverlap object
17
+ no = nooverlap.Pusher()
18
+
19
+ # add the boxes
20
+ for i in range(n):
21
+ no.add_box(x[i], y[i], w[i]/2, w[i]/2, h[i]/2, h[i]/2)
22
+
23
+
24
+ from time import time
25
+ start = time()
26
+ no.push_free(0,(n+1)/1000)
27
+ elapsed = time() - start
28
+ print(f'number {n/1000} took {elapsed*1000} ms')
29
+
30
+ # draw the boxes
31
+ fig, ax = plt.subplots()
32
+
33
+ ax.set_xlim(-1,6)
34
+ ax.set_ylim(-0.5,2.5)
35
+ for i in range(n):
36
+ _x,_y = no.get_position(i)
37
+ _w = w[i]
38
+ _h = h[i]
39
+ ax.add_patch(plt.Rectangle((_x-0.5*_w, _y-0.5*_h), width=_w, height=_h , facecolor='lightblue', edgecolor='black'))
40
+ ax.plot([_x,x[i]],[_y,y[i]])
41
+
42
+
43
+
44
+ plt.show()
45
+
@@ -0,0 +1,255 @@
1
+ use pyo3::prelude::*;
2
+
3
+ /// Define a class "Box" with a position (x,y) and a side (d_left, d_right, d_top, d_bottom)
4
+ /// The class has a method "overlap" that returns True if the box overlaps with another box
5
+ ///
6
+ /// x- x+
7
+ /// y+
8
+ /// ------ top -------
9
+ /// left right
10
+ /// ----- bottom -----
11
+ /// y-
12
+
13
+ #[pyclass]
14
+ struct Box {
15
+ x0 : f32,
16
+ y0 : f32,
17
+ x: f32,
18
+ y: f32,
19
+ d_left: f32,
20
+ d_right: f32,
21
+ d_top: f32,
22
+ d_bottom: f32,
23
+ }
24
+
25
+ #[pymethods]
26
+ impl Box {
27
+
28
+ #[new]
29
+ fn new(x0: f32, y0: f32, d_left: f32, d_right: f32, d_top: f32, d_bottom: f32) -> Self {
30
+ let x = x0;
31
+ let y = y0;
32
+ Box { x,y,x0,y0,d_left, d_right, d_top, d_bottom }
33
+ }
34
+
35
+ fn width(&self) -> f32 {
36
+ self.d_left + self.d_right
37
+ }
38
+
39
+ fn height(&self) -> f32 {
40
+ self.d_top + self.d_bottom
41
+ }
42
+
43
+ fn left(&self) -> f32 {
44
+ self.x - self.d_left
45
+ }
46
+
47
+ fn right(&self) -> f32 {
48
+ self.x + self.d_right
49
+ }
50
+
51
+ fn top(&self) -> f32 {
52
+ self.y + self.d_top
53
+ }
54
+
55
+ fn bottom(&self) -> f32 {
56
+ self.y - self.d_bottom
57
+ }
58
+
59
+ fn overlap(&self, other: &Box) -> bool {
60
+ if self.right() < other.left()
61
+ {
62
+ return false;
63
+ }
64
+ if self.left() > other.right()
65
+ {
66
+ return false;
67
+ }
68
+ if self.top() < other.bottom()
69
+ {
70
+ return false;
71
+ }
72
+ if self.bottom() > other.top()
73
+ {
74
+ return false;
75
+ }
76
+
77
+ return true;
78
+
79
+ }
80
+
81
+ fn move_towards_origin(&mut self, distance : f32)
82
+ {
83
+ if self.left() > self.x0
84
+ {
85
+ self.x -= distance;
86
+ }
87
+ if self.right() < self.x0
88
+ {
89
+ self.x += distance;
90
+ }
91
+ if self.top() < self.y0
92
+ {
93
+ self.y += distance;
94
+ }
95
+ if self.bottom() > self.y0
96
+ {
97
+ self.y -= distance;
98
+ }
99
+
100
+ }
101
+
102
+ fn get_overlapping_distance(&self, other: &Box) -> f32 {
103
+ // overlap in x-direction is the minimum of the right side of the first box and the left side of the second box
104
+ let mut overlap_x = other.left() - self.right();
105
+ if overlap_x < 0.0 {
106
+ overlap_x = self.left() - other.right();
107
+ }
108
+
109
+ if overlap_x < 0.0 {
110
+ overlap_x = 0.0;
111
+ }
112
+
113
+
114
+ let mut overlap_y = other.top() - self.bottom();
115
+ if overlap_y < 0.0 {
116
+ overlap_y = self.top() - other.bottom();
117
+ }
118
+
119
+ if overlap_y < 0.0 {
120
+ overlap_y = 0.0;
121
+ }
122
+
123
+ return (overlap_x*overlap_x + overlap_y*overlap_y).sqrt();
124
+
125
+
126
+ }
127
+
128
+ fn line_to_center(&self, other: &Box) -> (f32, f32) {
129
+ let dx = self.x - other.x;
130
+ let dy = self.y - other.y;
131
+ return (dx, dy)
132
+ }
133
+
134
+ fn distance_to_center(&self, other: &Box) -> f32 {
135
+ let (dx, dy) = self.line_to_center(other);
136
+ return (dx*dx + dy*dy).sqrt();
137
+ }
138
+
139
+ }
140
+
141
+ #[pyclass]
142
+ struct Pusher {
143
+ boxes : Vec<Box>,
144
+
145
+ }
146
+
147
+ #[pymethods]
148
+ impl Pusher {
149
+
150
+ #[new]
151
+ fn new() -> Self {
152
+ Pusher { boxes: Vec::new()}
153
+ }
154
+
155
+ /// Add a box to the pusher,
156
+ /// returns the index of the newly added box
157
+
158
+ fn add_box(&mut self, x0: f32, y0: f32, d_left: f32, d_right: f32, d_top: f32, d_bottom: f32) -> usize {
159
+ let new_box = Box::new(x0, y0, d_left, d_right, d_top, d_bottom);
160
+ self.boxes.push(new_box);
161
+
162
+ return self.boxes.len() - 1;
163
+ }
164
+
165
+ /// Push the boxes so that they don't overlap
166
+ /// returns true if the boxes were pushed
167
+ /// returns false if the boxes don't overlap
168
+ fn push_elements(&mut self, push_factor_horizontal:f32, push_factor_vertical:f32) -> bool{
169
+ let mut push = false;
170
+ for i in 0..self.boxes.len() {
171
+
172
+ for j in 0..self.boxes.len() {
173
+ if i == j {
174
+ continue;
175
+ }
176
+ if self.boxes[i].overlap(&self.boxes[j]) {
177
+
178
+ let (mut dx, mut dy) = self.boxes[i].line_to_center(&self.boxes[j]);
179
+
180
+ let overlap = self.boxes[i].get_overlapping_distance(&self.boxes[j]);
181
+
182
+ dx = dx * overlap;
183
+ dy = dy * overlap;
184
+
185
+ // maximize the move distance to the push factor
186
+ // times the size of the box
187
+
188
+ if dx.abs() > push_factor_horizontal * self.boxes[i].width()
189
+ {
190
+ dx = dx.signum() * push_factor_horizontal * self.boxes[i].width();
191
+ }
192
+
193
+ if dy.abs() > push_factor_vertical * self.boxes[i].height()
194
+ {
195
+ dy = dy.signum() * push_factor_vertical * self.boxes[i].height();
196
+ }
197
+
198
+
199
+ self.boxes[i].x += dx;
200
+ self.boxes[i].y += dy;
201
+ self.boxes[j].x -= dx;
202
+ self.boxes[j].y -= dy;
203
+
204
+ push = true
205
+ }
206
+ }
207
+ }
208
+ push
209
+ }
210
+
211
+ /// Pull all elements towards their original position
212
+ /// if they so not overlap it
213
+ fn pull_elements(&mut self, distance : f32)
214
+ {
215
+ // loop over all boxes
216
+
217
+ for b in self.boxes.iter_mut()
218
+ {
219
+ b.move_towards_origin(distance);
220
+ }
221
+
222
+ }
223
+
224
+ /// Get the position of the box
225
+ /// returns the position of the box
226
+ fn get_position(&self, index: usize) -> (f32, f32) {
227
+ (self.boxes[index].x, self.boxes[index].y)
228
+ }
229
+
230
+ fn push_free(&mut self, push_factor_horizontal : f32, push_factor_vertical : f32)
231
+
232
+ {
233
+ if push_factor_horizontal <= 0.0 && push_factor_vertical <= 0.0
234
+ {
235
+ panic!("At least one of the push factors should be larger than 0.0");
236
+ }
237
+
238
+ loop {
239
+ let pushed = self.push_elements(push_factor_horizontal, push_factor_vertical);
240
+ if !pushed
241
+ {
242
+ break;
243
+ }
244
+ }
245
+ }
246
+
247
+ }
248
+
249
+ /// A Python module implemented in Rust.
250
+ #[pymodule]
251
+ fn nooverlap(m: &Bound<'_, PyModule>) -> PyResult<()> {
252
+ m.add_class::<Box>()?;
253
+ m.add_class::<Pusher>()?;
254
+ Ok(())
255
+ }