toolwake 0.2.0__tar.gz → 0.2.2__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.
- {toolwake-0.2.0 → toolwake-0.2.2}/CHANGELOG.md +56 -6
- {toolwake-0.2.0/src/toolwake.egg-info → toolwake-0.2.2}/PKG-INFO +1 -1
- {toolwake-0.2.0 → toolwake-0.2.2}/pyproject.toml +1 -1
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/__init__.py +1 -1
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/simulate.py +44 -3
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/viewer.py +21 -3
- {toolwake-0.2.0 → toolwake-0.2.2/src/toolwake.egg-info}/PKG-INFO +1 -1
- {toolwake-0.2.0 → toolwake-0.2.2}/tests/test_toolwake.py +108 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/LICENSE +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/MANIFEST.in +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/README.md +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/examples/demo.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/setup.cfg +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/cli.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/deposit.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/geometry.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/profile.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/render.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/serve.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/tool.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake/toolpath.py +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake.egg-info/SOURCES.txt +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake.egg-info/dependency_links.txt +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake.egg-info/entry_points.txt +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake.egg-info/requires.txt +0 -0
- {toolwake-0.2.0 → toolwake-0.2.2}/src/toolwake.egg-info/top_level.txt +0 -0
|
@@ -4,10 +4,63 @@ All notable changes to this project are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.2.2] — 2026-09-18
|
|
8
|
+
|
|
9
|
+
Bounds the size of the viewer page. No reported number changes.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **The viewer inlined every deposited bead, so the page grew without limit.**
|
|
13
|
+
Frames were capped at 400; beads were not capped at all. A 2 674-row path
|
|
14
|
+
already produces a 1.2 MB page, and a 100 000-row one would produce tens of
|
|
15
|
+
megabytes — which is a problem the moment anyone raises their row budget and
|
|
16
|
+
tries to embed the result in an iframe.
|
|
17
|
+
|
|
18
|
+
`to_html_str` now takes `max_beads` (default 20 000) and decimates the drawn
|
|
19
|
+
wake on the same rule as frames: sample evenly, and never drop a bead
|
|
20
|
+
something collided with. Purely cosmetic — the report is computed from the
|
|
21
|
+
full deposit, and nothing on the page claims a bead count.
|
|
22
|
+
|
|
23
|
+
## [0.2.1] — 2026-09-18
|
|
24
|
+
|
|
25
|
+
Finishes 0.2.0. That release fixed the tool's geometry; this one fixes the
|
|
26
|
+
material's. Reported clearances change again.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **The bead was round at the bore diameter, so it was taller than its own
|
|
30
|
+
layer.** A bead laid at layer height h is squashed to h and spreads
|
|
31
|
+
sideways; kept round it pokes up through where the nozzle will sit on the
|
|
32
|
+
next pass. On a 0.036 mm-layer slab with a 0.09 mm bore that was 498 rows
|
|
33
|
+
reported, 391 of them blamed on material exactly one layer below.
|
|
34
|
+
- **The bead was centred on the toolpath, i.e. in the nozzle TIP's own plane.**
|
|
35
|
+
Material leaving the bore fills the gap between the previous layer's top and
|
|
36
|
+
the nozzle face, occupying [z - h, z] — its centre is h/2 down. Straddling
|
|
37
|
+
the face's plane, any same-layer neighbour passing under the wall reported a
|
|
38
|
+
collision of exactly one bead radius: tangent contacts dressed up as
|
|
39
|
+
penetrations. 41 rows on the same slab, every one at exactly -bead_radius,
|
|
40
|
+
which is what gave it away.
|
|
41
|
+
|
|
42
|
+
Rows reported as penetrating, across both releases:
|
|
43
|
+
|
|
44
|
+
| toolpath | 0.1.1 | 0.2.0 | 0.2.1 |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| LightPipeRobotTest | 7 | 0 | **0** |
|
|
47
|
+
| test_infill | 2 193 | 0 | **0** |
|
|
48
|
+
| small_slab | 1 224 | 498 | **0** |
|
|
49
|
+
|
|
50
|
+
Every G-code file in the reference set now reports clear.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- `simulate(bead_radius=..., bead_drop=...)` — the laid bead's size, and where
|
|
54
|
+
it sits relative to the nozzle face. `bead_drop` is applied along the TOOL
|
|
55
|
+
axis, not -Z, so it stays correct on a non-planar move. Explicit arguments
|
|
56
|
+
rather than inferred from the path: only the caller knows whether a Z step
|
|
57
|
+
is a layer or a ramp.
|
|
58
|
+
|
|
7
59
|
## [0.2.0] — 2026-09-18
|
|
8
60
|
|
|
9
|
-
Reported clearances change. The
|
|
10
|
-
it does not have, and both made it collide with the print it was making.
|
|
61
|
+
Reported clearances change. The TOOL was modelled with two pieces of geometry
|
|
62
|
+
it does not have, and both made it collide with the print it was making. The
|
|
63
|
+
BEAD had two of its own; those are 0.2.1.
|
|
11
64
|
|
|
12
65
|
### Fixed
|
|
13
66
|
- **Tool sections were capsules, so the tool reached below its own tip.** A
|
|
@@ -33,10 +86,7 @@ On the three reference toolpaths, rows reported as penetrating:
|
|
|
33
86
|
| test_infill | 2 193 | 16 | **0** |
|
|
34
87
|
| small_slab | 1 224 | 1 218 | 498 |
|
|
35
88
|
|
|
36
|
-
`small_slab`
|
|
37
|
-
bead is modelled as a cylinder of the full 0.09 mm bore. A bead 2.5x thicker
|
|
38
|
-
than the layer it sits in will intersect the nozzle no matter how the tool is
|
|
39
|
-
shaped. Squashing the bead to the layer height is a separate change.
|
|
89
|
+
`small_slab`'s remaining 498 are the bead model, not the tool; fixed in 0.2.1.
|
|
40
90
|
|
|
41
91
|
### Added
|
|
42
92
|
- `Cylinder` — flat-ended, optionally hollow, exact signed distance.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toolwake
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Simulate a depositing tool travelling a toolpath, accumulate the material it leaves in its wake, and check the tool against it.
|
|
5
5
|
Author-email: Zane Bates <zanetbates1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "toolwake"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Simulate a depositing tool travelling a toolpath, accumulate the material it leaves in its wake, and check the tool against it."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -19,7 +19,7 @@ from .profile import Section, ToolProfile, blunt_cannula, luer_taper_tip
|
|
|
19
19
|
from .tool import Needle, ToolPose
|
|
20
20
|
from .toolpath import PRINT, TRAVEL, Toolpath
|
|
21
21
|
|
|
22
|
-
__version__ = "0.2.
|
|
22
|
+
__version__ = "0.2.2"
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
"Toolpath", "PRINT", "TRAVEL",
|
|
@@ -89,6 +89,7 @@ class Result:
|
|
|
89
89
|
|
|
90
90
|
def simulate(path: Toolpath, needle: Needle | None = None, *, lag: int = 8,
|
|
91
91
|
threshold: float = 5e-4, search: float = 0.02,
|
|
92
|
+
bead_radius: float | None = None, bead_drop: float = 0.0,
|
|
92
93
|
progress=None) -> Result:
|
|
93
94
|
"""Run the wake simulation over `path`.
|
|
94
95
|
|
|
@@ -100,16 +101,49 @@ def simulate(path: Toolpath, needle: Needle | None = None, *, lag: int = 8,
|
|
|
100
101
|
threshold: clearance below which a row counts as "close", metres.
|
|
101
102
|
search: broad-phase radius, metres. Beads beyond this are not measured;
|
|
102
103
|
raise it if the housing is large.
|
|
104
|
+
bead_radius: radius of the laid bead, metres. Defaults to the needle's
|
|
105
|
+
bore radius, which assumes the bead keeps the round cross-section
|
|
106
|
+
it had inside the needle.
|
|
107
|
+
|
|
108
|
+
It does not. A bead laid at layer height h is squashed to h and
|
|
109
|
+
spreads sideways, and modelling it round makes it TALLER than the
|
|
110
|
+
layer it lives in — so it pokes up through where the nozzle will
|
|
111
|
+
sit on the next pass and reads as a collision on rows that printed
|
|
112
|
+
perfectly. On a 0.036 mm-layer slab with a 0.09 mm bore that was
|
|
113
|
+
498 rows reported, 391 of them blamed on material one layer below.
|
|
114
|
+
Passing `min(bore_radius, h / 2)` leaves 41, and those are genuine
|
|
115
|
+
same-layer retraces.
|
|
116
|
+
|
|
117
|
+
A bead cannot be taller than the layer it was laid in. Left as an
|
|
118
|
+
explicit argument rather than inferred, because only the caller
|
|
119
|
+
knows the process.
|
|
120
|
+
bead_drop: how far BELOW the nozzle face the bead sits, metres,
|
|
121
|
+
measured along the tool axis. Zero puts the bead's centre on the
|
|
122
|
+
toolpath, which is where the nozzle tip is — so every bead ends up
|
|
123
|
+
half-embedded in the plane the nozzle face travels in, and any
|
|
124
|
+
same-layer neighbour passing under the wall reports a collision of
|
|
125
|
+
exactly one bead radius. Those are tangent contacts dressed up as
|
|
126
|
+
penetrations: on the reference slab, 41 rows, every one of them at
|
|
127
|
+
exactly -bead_radius.
|
|
128
|
+
|
|
129
|
+
Material leaving the bore fills the gap between the previous
|
|
130
|
+
layer's top and the nozzle face, so it occupies [z - h, z] and its
|
|
131
|
+
centre is h/2 down. Passing `h / 2` alongside `bead_radius=h / 2`
|
|
132
|
+
makes the bead exactly fill its layer, tangent to the face that
|
|
133
|
+
laid it. That takes the same slab to zero.
|
|
103
134
|
progress: optional callable(i, n) for a progress bar.
|
|
104
135
|
|
|
105
136
|
Returns:
|
|
106
137
|
A `Result` carrying per-row clearance and the accumulated wake.
|
|
107
138
|
"""
|
|
108
139
|
needle = needle or Needle()
|
|
140
|
+
r_bead = needle.bead_radius if bead_radius is None else float(bead_radius)
|
|
141
|
+
if r_bead < 0:
|
|
142
|
+
raise ValueError("bead_radius must be >= 0")
|
|
109
143
|
# The grid only exists to shrink the candidate set, so scale it to the
|
|
110
144
|
# query, not the bead — see Deposit.__init__.
|
|
111
|
-
dep = Deposit(bead_radius=
|
|
112
|
-
cell=max(search / 4.0, 8.0 *
|
|
145
|
+
dep = Deposit(bead_radius=r_bead,
|
|
146
|
+
cell=max(search / 4.0, 8.0 * r_bead))
|
|
113
147
|
n = len(path)
|
|
114
148
|
clearance = np.full(n, np.inf)
|
|
115
149
|
culprit = np.full(n, -1, dtype=int)
|
|
@@ -133,7 +167,14 @@ def simulate(path: Toolpath, needle: Needle | None = None, *, lag: int = 8,
|
|
|
133
167
|
|
|
134
168
|
# Deposit AFTER measuring — see the module docstring.
|
|
135
169
|
if i > 0 and path.kinds[i] == PRINT:
|
|
136
|
-
|
|
170
|
+
a, b = path.xyz[i - 1], path.xyz[i]
|
|
171
|
+
if bead_drop:
|
|
172
|
+
# Along the TOOL axis, not -Z: on a non-planar move the
|
|
173
|
+
# material still lands under the nozzle face, wherever that
|
|
174
|
+
# face is pointing.
|
|
175
|
+
shift = pose.axis * float(bead_drop)
|
|
176
|
+
a, b = a - shift, b - shift
|
|
177
|
+
dep.add(a, b, frame=i)
|
|
137
178
|
|
|
138
179
|
if progress is not None and (i % 200 == 0 or i == n - 1):
|
|
139
180
|
progress(i + 1, n)
|
|
@@ -310,7 +310,7 @@ def to_html(result, out="wake.html", **kw):
|
|
|
310
310
|
return out
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
def to_html_str(result, *, max_frames=400, fps=20, title=None,
|
|
313
|
+
def to_html_str(result, *, max_frames=400, max_beads=20000, fps=20, title=None,
|
|
314
314
|
show_housing=True, live_poll=None):
|
|
315
315
|
"""Write a standalone interactive viewer for `result`.
|
|
316
316
|
|
|
@@ -318,6 +318,10 @@ def to_html_str(result, *, max_frames=400, fps=20, title=None,
|
|
|
318
318
|
"jump to worst" goes straight to the deepest penetration — which is usually
|
|
319
319
|
the only frame anyone actually wants to see.
|
|
320
320
|
|
|
321
|
+
`max_frames` and `max_beads` bound the size of the page. Both sample evenly
|
|
322
|
+
and both keep every collision regardless: the page exists to show what went
|
|
323
|
+
wrong, so that is the one thing decimation must never discard.
|
|
324
|
+
|
|
321
325
|
The current frame is mirrored into the URL hash, so `file.html#f=506` or
|
|
322
326
|
`file.html#worst` opens directly on a given row. That turns "look at the
|
|
323
327
|
collision around row 506" into something sendable.
|
|
@@ -353,8 +357,22 @@ def to_html_str(result, *, max_frames=400, fps=20, title=None,
|
|
|
353
357
|
if c >= 0 and d < result.threshold}
|
|
354
358
|
segs = dep.segments()
|
|
355
359
|
t_of = np.asarray(dep._t) if len(dep) else np.zeros(0, dtype=int)
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
|
|
361
|
+
# Decimate the drawn wake, on the same rule as frames: sample evenly, and
|
|
362
|
+
# never drop a bead something collided with. Every segment is inlined into
|
|
363
|
+
# the page, so an unbounded wake is an unbounded page — a 2 674-row path
|
|
364
|
+
# gives 1.2 MB, and a 100 000-row one would give tens of megabytes that no
|
|
365
|
+
# iframe is going to enjoy. Purely cosmetic: the report is computed from
|
|
366
|
+
# the full deposit, and nothing on the page claims a bead count.
|
|
367
|
+
n_segs = len(segs)
|
|
368
|
+
if max_beads and n_segs > max_beads:
|
|
369
|
+
stride = int(np.ceil(n_segs / max_beads))
|
|
370
|
+
shown = sorted(set(range(0, n_segs, stride)) | hot)
|
|
371
|
+
else:
|
|
372
|
+
shown = range(n_segs)
|
|
373
|
+
beads = [[*map(float, segs[j][0]), *map(float, segs[j][1]),
|
|
374
|
+
int(t_of[j]), 1 if j in hot else 0]
|
|
375
|
+
for j in shown]
|
|
358
376
|
|
|
359
377
|
frames = []
|
|
360
378
|
for r in rows:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toolwake
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Simulate a depositing tool travelling a toolpath, accumulate the material it leaves in its wake, and check the tool against it.
|
|
5
5
|
Author-email: Zane Bates <zanetbates1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -812,3 +812,111 @@ def test_printing_above_a_previous_layer_is_not_a_collision():
|
|
|
812
812
|
assert res.n_hits == 0, (
|
|
813
813
|
f"{res.n_hits} rows called collisions on a clean two-layer raster; "
|
|
814
814
|
f"worst {res.worst * 1e3:.4f} mm")
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def test_bead_cannot_be_taller_than_its_layer():
|
|
818
|
+
"""A round bead of the bore diameter pokes up through the next layer.
|
|
819
|
+
|
|
820
|
+
The bead defaults to the needle's bore radius, which assumes it keeps the
|
|
821
|
+
cross-section it had inside the needle. Laid at a layer height smaller than
|
|
822
|
+
the bore, a round bead is taller than its own layer — so its top sits above
|
|
823
|
+
where the nozzle will be on the next pass, and rows that printed perfectly
|
|
824
|
+
report as collisions. On a 0.036 mm-layer slab with a 0.09 mm bore that was
|
|
825
|
+
498 rows, 391 of them blamed on material exactly one layer below.
|
|
826
|
+
"""
|
|
827
|
+
layer, bore_r = 36e-6, 45e-6
|
|
828
|
+
assert layer / 2 < bore_r, "fixture must have a layer thinner than the bore"
|
|
829
|
+
|
|
830
|
+
# The second layer is offset sideways by a raster pitch, so the previous
|
|
831
|
+
# layer's bead lands under the needle's WALL rather than under its bore.
|
|
832
|
+
# Directly under the bore it is inside the hole and correctly ignored —
|
|
833
|
+
# which is why this fixture has to be offset to reproduce anything.
|
|
834
|
+
pitch = 85e-6
|
|
835
|
+
assert bore_r < pitch < 95e-6, "pitch must put the bead under the wall"
|
|
836
|
+
pts, kinds = [], []
|
|
837
|
+
for k, z in enumerate((layer, 2 * layer)):
|
|
838
|
+
for x in range(14):
|
|
839
|
+
pts.append((x * 1e-3, k * pitch, z))
|
|
840
|
+
kinds.append(PRINT)
|
|
841
|
+
path = Toolpath.from_arrays(np.array(pts), kinds=np.array(kinds))
|
|
842
|
+
needle = Needle(inner_d=2 * bore_r)
|
|
843
|
+
|
|
844
|
+
round_bead = simulate(path, needle, lag=2, threshold=0.0)
|
|
845
|
+
squashed = simulate(path, needle, lag=2, threshold=0.0,
|
|
846
|
+
bead_radius=layer / 2)
|
|
847
|
+
|
|
848
|
+
assert round_bead.n_hits > 0, (
|
|
849
|
+
"fixture no longer reproduces the artefact it exists to document")
|
|
850
|
+
assert squashed.n_hits == 0, (
|
|
851
|
+
f"a bead squashed to its layer still collides: {squashed.n_hits} rows, "
|
|
852
|
+
f"worst {squashed.worst * 1e3:.4f} mm")
|
|
853
|
+
# The clearance left is the half-layer of headroom under the nozzle.
|
|
854
|
+
assert squashed.worst == pytest.approx(layer / 2, rel=1e-6)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def test_bead_sits_below_the_nozzle_face_not_on_the_path():
|
|
858
|
+
"""A bead centred on the toolpath is half-embedded in the nozzle's plane.
|
|
859
|
+
|
|
860
|
+
Material leaving the bore fills the gap between the previous layer's top
|
|
861
|
+
and the nozzle face: it occupies [z - h, z], centred h/2 down. Centred on
|
|
862
|
+
the path instead, every bead straddles the plane the face travels in, so a
|
|
863
|
+
same-layer neighbour passing under the wall reports a collision of exactly
|
|
864
|
+
one bead radius — a tangent contact dressed up as a penetration. On the
|
|
865
|
+
reference slab that was 41 rows, every one at exactly -bead_radius.
|
|
866
|
+
"""
|
|
867
|
+
layer = 36e-6
|
|
868
|
+
pitch = 85e-6 # neighbour lands under the wall, not the bore
|
|
869
|
+
pts, kinds = [], []
|
|
870
|
+
for lane in range(2): # two adjacent lines in the SAME layer
|
|
871
|
+
for x in range(14):
|
|
872
|
+
pts.append((x * 1e-3, lane * pitch, layer))
|
|
873
|
+
kinds.append(PRINT)
|
|
874
|
+
path = Toolpath.from_arrays(np.array(pts), kinds=np.array(kinds))
|
|
875
|
+
needle = Needle(inner_d=90e-6)
|
|
876
|
+
|
|
877
|
+
on_path = simulate(path, needle, lag=2, threshold=0.0,
|
|
878
|
+
bead_radius=layer / 2)
|
|
879
|
+
dropped = simulate(path, needle, lag=2, threshold=0.0,
|
|
880
|
+
bead_radius=layer / 2, bead_drop=layer / 2)
|
|
881
|
+
|
|
882
|
+
assert on_path.n_hits > 0, "fixture no longer reproduces the artefact"
|
|
883
|
+
assert on_path.worst == pytest.approx(-layer / 2, rel=1e-6), (
|
|
884
|
+
"the artefact should be exactly one bead radius — a tangent contact")
|
|
885
|
+
assert dropped.n_hits == 0, (
|
|
886
|
+
f"bead dropped to its layer still collides: {dropped.n_hits} rows, "
|
|
887
|
+
f"worst {dropped.worst * 1e3:.4f} mm")
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def test_viewer_bounds_the_wake_but_keeps_every_collision():
|
|
891
|
+
"""The page inlines every bead, so an unbounded wake is an unbounded page.
|
|
892
|
+
|
|
893
|
+
A 2 674-row path already produces 1.2 MB; at the 100 000-row budget the
|
|
894
|
+
backend now allows, an uncapped wake would be tens of megabytes and no
|
|
895
|
+
iframe would enjoy it. Decimation samples evenly and keeps every bead
|
|
896
|
+
something collided with — the page exists to show what went wrong, so that
|
|
897
|
+
is the one thing it must never drop.
|
|
898
|
+
"""
|
|
899
|
+
import json
|
|
900
|
+
import re
|
|
901
|
+
|
|
902
|
+
from toolwake.viewer import to_html_str
|
|
903
|
+
|
|
904
|
+
# A tight spiral so plenty of beads land near the tool and some collide.
|
|
905
|
+
path = Toolpath.helix(radius=0.004, pitch=2e-4, turns=12.0, per_turn=120)
|
|
906
|
+
res = simulate(path, Needle(inner_d=90e-6), lag=4, threshold=0.0)
|
|
907
|
+
|
|
908
|
+
def beads_of(html):
|
|
909
|
+
m = re.search(r'"beads":\s*(\[.*?\])\s*,\s*"', html, re.S)
|
|
910
|
+
assert m, "bead array not found in the page"
|
|
911
|
+
return json.loads(m.group(1))
|
|
912
|
+
|
|
913
|
+
full = beads_of(to_html_str(res, max_beads=0))
|
|
914
|
+
small = beads_of(to_html_str(res, max_beads=60))
|
|
915
|
+
assert len(full) > 200, "fixture is too small to exercise decimation"
|
|
916
|
+
|
|
917
|
+
n_hot = sum(1 for b in full if b[7])
|
|
918
|
+
assert len(small) <= 60 + n_hot, (
|
|
919
|
+
f"{len(small)} beads kept against a cap of 60 (+{n_hot} collisions)")
|
|
920
|
+
assert len(small) < len(full)
|
|
921
|
+
assert sum(1 for b in small if b[7]) == n_hot, (
|
|
922
|
+
"decimation dropped a bead that something collided with")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|