From 20156bee2b1b56d959aa6990820a13e7c4d45968 Mon Sep 17 00:00:00 2001
From: Padraig Gleeson
Date: Mon, 5 Oct 2026 13:36:11 +0100
Subject: [PATCH 1/2] Remove -f requirement for wcon file argument; to v0.2.0
---
README.md | 2 +-
pyproject.toml | 2 +-
test_all.sh | 8 +++---
wconviewer/WormView.py | 62 +++++++++++++++++++++++++++++++-----------
4 files changed, 52 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index 0791e53..45b19f2 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Saving animations with `-nogui` requires the `ffmpeg` binary to be installed on
## Usage
```
-wconviewer -f examples/simdata.wcon
+wconviewer examples/simdata.wcon
```
Run `wconviewer -h` for all options (e.g. `-nogui` to save an `.mp4` instead of showing a window).
diff --git a/pyproject.toml b/pyproject.toml
index b36d89a..f56ee78 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "WCONViewer"
-version = "0.1.0"
+version = "0.2.0"
description = "Viewer for WCON (Worm tracker Commons Object Notation) files in 2D"
readme = "README.md"
license = { file = "LICENSE" }
diff --git a/test_all.sh b/test_all.sh
index 751bef4..dc031e4 100755
--- a/test_all.sh
+++ b/test_all.sh
@@ -6,8 +6,8 @@ ruff check wconviewer app.py
pip install .
-wconviewer -f examples/simdata.wcon -nogui -z # Test reloading WCON in Player
-wconviewer -f examples/worm_motion_log.wcon -nogui -g
-wconviewer -f examples/output_W2D.wcon -nogui -g -z
+wconviewer examples/simdata.wcon -nogui -z # Test reloading WCON in Player
+wconviewer examples/worm_motion_log.wcon -nogui -g
+wconviewer examples/output_W2D.wcon -nogui -g -z
-#wconviewer -f examples/N2\ on\ food\ L_2009_09_15__11_01_01___8___1.wcon.zip -nogui
+#wconviewer examples/N2\ on\ food\ L_2009_09_15__11_01_01___8___1.wcon.zip -nogui
diff --git a/wconviewer/WormView.py b/wconviewer/WormView.py
index 641a0f5..7f9a063 100644
--- a/wconviewer/WormView.py
+++ b/wconviewer/WormView.py
@@ -143,7 +143,13 @@ def set_show_grid(self, value):
self.ax.grid(False, which="major")
self.ax.grid(False, which="minor")
- def get_plot(self, args):
+ def get_plot(
+ self,
+ wcon_file,
+ ignore_wcon_perimeter=False,
+ suppress_automatic_generation=False,
+ minor_radius=40e-3,
+ ):
# global times, t_units, x, y, px, py, ax
self.fig, self.ax = plt.subplots()
@@ -151,7 +157,7 @@ def get_plot(self, args):
plt.get_current_fig_manager().set_window_title("WCON replay")
self.ax.set_aspect("equal")
- self.wcon = SimpleWCON(args.wcon_file)
+ self.wcon = SimpleWCON(wcon_file)
self.ax.set_xlabel("x (%s)" % self.wcon.x_units_used)
self.ax.set_ylabel("y (%s)" % self.wcon.y_units_used)
@@ -181,20 +187,20 @@ def get_plot(self, args):
print("No objects found")
if self.wcon.px is not None and self.wcon.py is not None:
- if args.ignore_wcon_perimeter:
+ if ignore_wcon_perimeter:
print(
"Ignoring (px, py) values in WCON file and computing perimeter from midline."
)
- self.px, self.py = self.get_perimeter(self.x, self.y, args.minor_radius)
+ self.px, self.py = self.get_perimeter(self.x, self.y, minor_radius)
else:
print("Using (px, py) from WCON file")
self.px = np.array(self.wcon.px).T
self.py = np.array(self.wcon.py).T
else:
- if not args.suppress_automatic_generation and self.wcon.x.shape[0] >= 2:
+ if not suppress_automatic_generation and self.wcon.x.shape[0] >= 2:
print("Computing perimeter from midline")
self.px, self.py = self.get_perimeter(
- self.wcon.x, self.wcon.y, args.minor_radius
+ self.wcon.x, self.wcon.y, minor_radius
)
else:
print("Not computing perimeter from midline")
@@ -273,11 +279,10 @@ def parse_args():
description="Open a player for the worm behaviour."
)
parser.add_argument(
- "-f",
- "--wcon_file",
+ "wcon_file",
type=validate_file,
+ metavar="WCON_FILE",
help="WCON file path",
- required=True,
)
parser.add_argument(
"-nogui", action="store_true", help="Just load file, don't show GUI"
@@ -333,13 +338,38 @@ def main():
print(" - Arguments parsed: %s" % args)
+ show_worm_view(
+ args.wcon_file,
+ args.show_head,
+ args.zoom_to_worm,
+ args.show_grid,
+ args.nogui,
+ args.ignore_wcon_perimeter,
+ args.suppress_automatic_generation,
+ args.minor_radius,
+ )
+
+
+def show_worm_view(
+ wcon_file: str,
+ show_head: bool = False,
+ zoom_to_worm: bool = False,
+ show_grid: bool = False,
+ nogui: bool = False,
+ ignore_wcon_perimeter: bool = False,
+ suppress_automatic_generation: bool = False,
+ minor_radius: float = 40e-3,
+):
+
wv = WormView(
- show_head=args.show_head,
- zoom_to_worm=args.zoom_to_worm,
- show_grid=args.show_grid,
+ show_head=show_head,
+ zoom_to_worm=zoom_to_worm,
+ show_grid=show_grid,
)
- fig, ax = wv.get_plot(args)
+ fig, ax = wv.get_plot(
+ wcon_file, ignore_wcon_perimeter, suppress_automatic_generation, minor_radius
+ )
def update(ti):
print(" ------ Animating the plot for time index: %d" % ti)
@@ -357,12 +387,12 @@ def update(ti):
on_toggle_zoom=wv.set_zoom_to_worm,
)
- if args.nogui:
+ if nogui:
# Checkboxes are only useful for interactive control; don't bake them
# into the frames used for the saved movie.
anim.check_buttons.ax.set_visible(False)
- if not args.nogui:
+ if not nogui:
plt.show()
else:
print("GUI suppressed, exiting without showing %s." % anim)
@@ -370,7 +400,7 @@ def update(ti):
from matplotlib.animation import FFMpegWriter
FFwriter = FFMpegWriter(fps=10)
- mp4_file = args.wcon_file.replace(".wcon.zip", ".wcon").replace(".wcon", ".mp4")
+ mp4_file = wcon_file.replace(".wcon.zip", ".wcon").replace(".wcon", ".mp4")
print(f"Saving animation to: {mp4_file}")
anim.save(mp4_file, writer=FFwriter)
From c28f4e95a4ac012e19f8a86ffb1362697e023fc3 Mon Sep 17 00:00:00 2001
From: Padraig Gleeson
Date: Mon, 5 Oct 2026 14:04:46 +0100
Subject: [PATCH 2/2] Add start play option
---
.gitignore | 1 +
wconviewer/Player.py | 10 ++++++++++
wconviewer/WormView.py | 3 +++
3 files changed, 14 insertions(+)
diff --git a/.gitignore b/.gitignore
index 2d24004..7f22cdf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
/old
/build
/*.egg-info
+/wconviewer/__pycache__
diff --git a/wconviewer/Player.py b/wconviewer/Player.py
index b42e928..5ff804e 100644
--- a/wconviewer/Player.py
+++ b/wconviewer/Player.py
@@ -25,6 +25,7 @@ def __init__(
zoom_state=False,
on_toggle_grid=None,
on_toggle_zoom=None,
+ start_play=True,
**kwargs,
):
self.i = 0
@@ -53,6 +54,15 @@ def __init__(
self.times = times
self.t_units = t_units
+ if not start_play:
+ # The animation timer is started by matplotlib on the first draw, so
+ # pause it from a draw callback registered after matplotlib's own.
+ def pause_after_first_draw(event):
+ self.fig.canvas.mpl_disconnect(cid)
+ self.stop()
+
+ cid = self.fig.canvas.mpl_connect("draw_event", pause_after_first_draw)
+
def play(self):
while self.runs:
self.i = self.i + self.step * (1 if self.forwards else -1)
diff --git a/wconviewer/WormView.py b/wconviewer/WormView.py
index 7f9a063..77c34f5 100644
--- a/wconviewer/WormView.py
+++ b/wconviewer/WormView.py
@@ -359,6 +359,7 @@ def show_worm_view(
ignore_wcon_perimeter: bool = False,
suppress_automatic_generation: bool = False,
minor_radius: float = 40e-3,
+ start_play: bool = True,
):
wv = WormView(
@@ -385,6 +386,8 @@ def update(ti):
zoom_state=wv.zoom_to_worm,
on_toggle_grid=wv.set_show_grid,
on_toggle_zoom=wv.set_zoom_to_worm,
+ # When saving a movie (nogui) the animation always runs through
+ start_play=start_play or nogui,
)
if nogui: