copick-shared-ui 2.0.0a2__tar.gz → 2.0.0a3__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.
Files changed (91) hide show
  1. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/release-please.yml +1 -3
  2. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/tests.yml +0 -3
  3. copick_shared_ui-2.0.0a3/.release-please.manifest.json +3 -0
  4. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/CHANGELOG.md +7 -0
  5. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/PKG-INFO +2 -2
  6. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/pyproject.toml +1 -1
  7. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/__init__.py +1 -1
  8. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/click_schema.py +1 -1
  9. copick_shared_ui-2.0.0a3/src/copick_shared_ui/core/types.py +132 -0
  10. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/ui/edit_object_types_dialog.py +115 -3
  11. copick_shared_ui-2.0.0a3/src/copick_shared_ui/util/filament_session.py +567 -0
  12. copick_shared_ui-2.0.0a3/src/copick_shared_ui/util/filaments.py +341 -0
  13. copick_shared_ui-2.0.0a3/src/copick_shared_ui/util/instances.py +332 -0
  14. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/actions_bar.py +3 -14
  15. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/command_form.py +3 -12
  16. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/uri_widget.py +26 -4
  17. copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/compact_delegate.py +466 -0
  18. copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/flow_layout.py +105 -0
  19. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/info/info_widget.py +44 -4
  20. copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/instances/__init__.py +5 -0
  21. copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/instances/instance_browser.py +490 -0
  22. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/data_worker.py +12 -0
  23. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/conftest.py +16 -0
  24. copick_shared_ui-2.0.0a3/tests/test_compact_rows.py +203 -0
  25. copick_shared_ui-2.0.0a3/tests/test_filament_session.py +302 -0
  26. copick_shared_ui-2.0.0a3/tests/test_filaments.py +131 -0
  27. copick_shared_ui-2.0.0a3/tests/test_instances.py +99 -0
  28. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_package_metadata.py +1 -1
  29. copick_shared_ui-2.0.0a3/tests/test_types.py +67 -0
  30. copick_shared_ui-2.0.0a3/tests/test_uri_and_dialog.py +110 -0
  31. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/uv.lock +52 -52
  32. copick_shared_ui-2.0.0a2/.release-please.manifest.json +0 -3
  33. copick_shared_ui-2.0.0a2/tests/inspect_wheel_metadata.py +0 -30
  34. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/dependabot.yml +0 -0
  35. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/conventional-commits.yml +0 -0
  36. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/py-formatting.yml +0 -0
  37. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.gitignore +0 -0
  38. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.pre-commit-config.yaml +0 -0
  39. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/LICENSE +0 -0
  40. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/README.md +0 -0
  41. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/SECURITY.md +0 -0
  42. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/benchmarks/__init__.py +0 -0
  43. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/benchmarks/results/2026-08-18-local-alpha-baseline.md +0 -0
  44. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/benchmarks/thumbnail_io.py +0 -0
  45. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/release-please.config.json +0 -0
  46. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/release-please.prerelease.config.json +0 -0
  47. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/__init__.py +0 -0
  48. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/image_interface.py +0 -0
  49. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/models.py +0 -0
  50. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/thumbnail_cache.py +0 -0
  51. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/icons/__init__.py +0 -0
  52. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/platform/__init__.py +0 -0
  53. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/platform/chimerax_integration.py +0 -0
  54. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/platform/napari_integration.py +0 -0
  55. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/storage.py +0 -0
  56. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/__init__.py +0 -0
  57. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/colors.py +0 -0
  58. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/styles.py +0 -0
  59. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/theme_detection.py +0 -0
  60. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/ui/__init__.py +0 -0
  61. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/util/__init__.py +0 -0
  62. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/util/validation.py +0 -0
  63. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/__init__.py +0 -0
  64. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/__init__.py +0 -0
  65. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/command_browser.py +0 -0
  66. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/command_runner.py +0 -0
  67. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/param_widgets.py +0 -0
  68. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/gallery/__init__.py +0 -0
  69. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/gallery/gallery_widget.py +0 -0
  70. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/gallery/run_card.py +0 -0
  71. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/info/__init__.py +0 -0
  72. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/__init__.py +0 -0
  73. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/base.py +0 -0
  74. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/base_manager.py +0 -0
  75. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/chimerax.py +0 -0
  76. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/napari.py +0 -0
  77. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/unified_workers.py +0 -0
  78. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/__init__.py +0 -0
  79. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/docker-compose.yml +0 -0
  80. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/helpers.py +0 -0
  81. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/ssh_mount/.gitignore +0 -0
  82. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_core_direct_reader_fixture.py +0 -0
  83. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_core_entity_backends.py +0 -0
  84. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_imports.py +0 -0
  85. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_live_portal_integration.py +0 -0
  86. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_ssh_backend_integration.py +0 -0
  87. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_storage.py +0 -0
  88. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_thumbnail_baseline.py +0 -0
  89. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_thumbnail_benchmark.py +0 -0
  90. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_thumbnail_errors.py +0 -0
  91. {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_worker_callbacks.py +0 -0
@@ -78,9 +78,7 @@ jobs:
78
78
  run: uv lock --check
79
79
 
80
80
  - name: build
81
- run: |
82
- uv build
83
- uv run --no-project --with packaging python tests/inspect_wheel_metadata.py dist/*.whl
81
+ run: uv build
84
82
 
85
83
  # release-please's own Python updater misses a version with a dotted prerelease suffix (2.0.0-alpha.1), so
86
84
  # src/copick_shared_ui/__init__.py carries an x-release-please-version marker; refuse to upload if it was still missed.
@@ -64,6 +64,3 @@ jobs:
64
64
 
65
65
  - name: Build artifacts
66
66
  run: uv build
67
-
68
- - name: Inspect wheel metadata
69
- run: uv run --no-sync python tests/inspect_wheel_metadata.py dist/*.whl
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "2.0.0-alpha.3"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0-alpha.3](https://github.com/copick/copick-shared-ui/compare/copick-shared-ui-v2.0.0-alpha.2...copick-shared-ui-v2.0.0-alpha.3) (2026-10-06)
4
+
5
+
6
+ ### ✨ Features
7
+
8
+ * filaments, instance segmentations and compact instance browsing (v2.0) ([#74](https://github.com/copick/copick-shared-ui/issues/74)) ([763861b](https://github.com/copick/copick-shared-ui/commit/763861bc00651eb5db6a68d4605df0671ec76317))
9
+
3
10
  ## [2.0.0-alpha.2](https://github.com/copick/copick-shared-ui/compare/copick-shared-ui-v2.0.0-alpha.1...copick-shared-ui-v2.0.0-alpha.2) (2026-10-06)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: copick-shared-ui
3
- Version: 2.0.0-alpha.2
3
+ Version: 2.0.0-alpha.3
4
4
  Summary: Shared UI components for copick visualization plugins
5
5
  Project-URL: Repository, https://github.com/copick/copick-shared-ui
6
6
  Project-URL: Issues, https://github.com/copick/copick-shared-ui/issues
@@ -46,7 +46,7 @@ Classifier: Programming Language :: Python :: 3.14
46
46
  Classifier: Programming Language :: Python :: Implementation :: CPython
47
47
  Classifier: Topic :: Scientific/Engineering :: Image Processing
48
48
  Requires-Python: >=3.11
49
- Requires-Dist: copick<3,>=2.0.0a1
49
+ Requires-Dist: copick<3,>=2.0.0a3
50
50
  Requires-Dist: numpy>=2.0.2
51
51
  Requires-Dist: pydantic>=2
52
52
  Requires-Dist: qtpy
@@ -24,7 +24,7 @@ dynamic = ["version"]
24
24
  dependencies = [
25
25
  "qtpy",
26
26
  "pydantic>=2",
27
- "copick>=2.0.0a1,<3",
27
+ "copick>=2.0.0a3,<3",
28
28
  "zarr>=3.1.6,<4",
29
29
  "numpy>=2.0.2",
30
30
  "superqt"
@@ -1,6 +1,6 @@
1
1
  """Copick shared UI components for cross-platform visualization."""
2
2
 
3
- __version__ = "2.0.0-alpha.2" # x-release-please-version
3
+ __version__ = "2.0.0-alpha.3" # x-release-please-version
4
4
 
5
5
  # Core components
6
6
  from copick_shared_ui.core import (
@@ -40,7 +40,7 @@ UI_EXCLUDED_COMMANDS = {"browse", "info", "config", "stats", "deposit"}
40
40
  class URIParamMeta:
41
41
  """Metadata for a URI parameter, extracted from CopickURI type."""
42
42
 
43
- object_type: str # "picks", "mesh", "segmentation", "tomogram", "feature", "any"
43
+ object_type: str # "picks", "filaments", "mesh", "segmentation", "tomogram", "feature", "any"
44
44
  role: str # "input", "output", "reference"
45
45
 
46
46
 
@@ -0,0 +1,132 @@
1
+ """Copick entity types as the UI sees them, tolerant of copick versions that predate filaments and the newer
2
+ segmentation types. Qt-free."""
3
+
4
+ from typing import Any, Optional
5
+
6
+ SEGMENTATION_TYPES = ("binary", "multilabel", "instance", "panoptic")
7
+
8
+ SEGMENTATION_TYPE_LABELS = {
9
+ "binary": "Binary",
10
+ "multilabel": "Multilabel",
11
+ "instance": "Instance",
12
+ "panoptic": "Panoptic",
13
+ }
14
+
15
+ # Short tags for compact tables and badges (the chips copick-web shows).
16
+ SEGMENTATION_TYPE_TAGS = {
17
+ "binary": "binary",
18
+ "multilabel": "multi",
19
+ "instance": "inst",
20
+ "panoptic": "pan",
21
+ }
22
+
23
+ FILAMENT_METADATA_NAMESPACE = "copick"
24
+ FILAMENT_METADATA_KEY = "filament"
25
+
26
+
27
+ def _class_names(obj: Any):
28
+ return [cls.__name__ for cls in type(obj).__mro__]
29
+
30
+
31
+ def copick_object_type(obj: Any) -> Optional[str]:
32
+ """``"filaments"``, ``"picks"``, ``"mesh"``, ``"segmentation"``, ``"tomogram"``, ``"feature"``, ``"voxel_spacing"``,
33
+ ``"run"`` or None, by class (works for every backend's subclasses)."""
34
+ names = _class_names(obj)
35
+ # Order matters: check the more specific names first.
36
+ for cls_name, kind in (
37
+ ("CopickFilaments", "filaments"),
38
+ ("CopickPicks", "picks"),
39
+ ("CopickMesh", "mesh"),
40
+ ("CopickSegmentation", "segmentation"),
41
+ ("CopickTomogram", "tomogram"),
42
+ ("CopickFeatures", "feature"),
43
+ ("CopickVoxelSpacing", "voxel_spacing"),
44
+ ("CopickRun", "run"),
45
+ ):
46
+ if cls_name in names:
47
+ return kind
48
+ return None
49
+
50
+
51
+ URI_OBJECT_TYPES = ("picks", "filaments", "mesh", "segmentation", "tomogram", "feature")
52
+
53
+
54
+ def uri_object_type(obj: Any) -> Optional[str]:
55
+ """The copick URI object type of an entity (``copick.util.uri``), or None if it has no URI form."""
56
+ if obj is None:
57
+ return None
58
+ kind = copick_object_type(obj)
59
+ return kind if kind in URI_OBJECT_TYPES else None
60
+
61
+
62
+ def segmentation_type_of(seg: Any) -> str:
63
+ """``"binary"``, ``"multilabel"``, ``"instance"`` or ``"panoptic"`` (falls back to ``is_multilabel`` for copick
64
+ versions without instance and panoptic segmentations)."""
65
+ kind = getattr(seg, "segmentation_type", None)
66
+ if isinstance(kind, str) and kind in SEGMENTATION_TYPES:
67
+ return kind
68
+ if getattr(seg, "is_panoptic", False) is True:
69
+ return "panoptic"
70
+ if getattr(seg, "is_instance", False) is True:
71
+ return "instance"
72
+ return "multilabel" if getattr(seg, "is_multilabel", False) else "binary"
73
+
74
+
75
+ def segmentation_type_flags(kind: str, explicit: Optional[bool] = None) -> dict:
76
+ """Keyword flags for ``new_segmentation`` / ``get_segmentations`` / ``delete_segmentations`` selecting exactly one
77
+ type.
78
+
79
+ With a copick that has instance and panoptic segmentations (``explicit``, detected by default), all three flags are
80
+ given, because an omitted flag means "any": ``delete_segmentations(..., is_multilabel=False)`` alone would also
81
+ delete an instance segmentation with the same name, user, session and voxel size. Older copick versions only get
82
+ ``is_multilabel``.
83
+ """
84
+ if kind not in SEGMENTATION_TYPES:
85
+ raise ValueError(f"Unknown segmentation type {kind!r}")
86
+ explicit = supports_segmentation_types() if explicit is None else explicit
87
+ if not explicit:
88
+ if kind in ("instance", "panoptic"):
89
+ raise ValueError(f"This copick version has no {kind} segmentations.")
90
+ return {"is_multilabel": kind == "multilabel"}
91
+ return {"is_multilabel": kind == "multilabel", "is_instance": kind == "instance", "is_panoptic": kind == "panoptic"}
92
+
93
+
94
+ def filament_spec_of(obj: Any) -> Optional[dict]:
95
+ """The filament declaration of a pickable object (``PickableObject`` or ``CopickObject``) as a dict, or None."""
96
+ spec = getattr(obj, "filament", None)
97
+ if spec is not None and not callable(spec):
98
+ return spec.model_dump(exclude_none=True) if hasattr(spec, "model_dump") else dict(spec)
99
+ meta = getattr(obj, "metadata", None)
100
+ if meta is None and hasattr(obj, "_meta"):
101
+ meta = getattr(obj._meta, "metadata", None)
102
+ if isinstance(meta, dict):
103
+ ns = meta.get(FILAMENT_METADATA_NAMESPACE)
104
+ if isinstance(ns, dict) and isinstance(ns.get(FILAMENT_METADATA_KEY), dict):
105
+ return dict(ns[FILAMENT_METADATA_KEY])
106
+ return None
107
+
108
+
109
+ def is_filament_object(obj: Any) -> bool:
110
+ """Whether a pickable object is declared a filament."""
111
+ value = getattr(obj, "is_filament", None)
112
+ if isinstance(value, bool):
113
+ return value
114
+ return filament_spec_of(obj) is not None
115
+
116
+
117
+ def supports_filaments() -> bool:
118
+ """Whether the installed copick has the Filaments entity."""
119
+ try:
120
+ from copick.models import CopickRun
121
+ except ImportError: # pragma: no cover
122
+ return False
123
+ return hasattr(CopickRun, "get_filaments")
124
+
125
+
126
+ def supports_segmentation_types() -> bool:
127
+ """Whether the installed copick has instance and panoptic segmentations."""
128
+ try:
129
+ from copick.models import CopickSegmentation
130
+ except ImportError: # pragma: no cover
131
+ return False
132
+ return hasattr(CopickSegmentation, "segmentation_type")
@@ -2,7 +2,8 @@
2
2
  Dialog for editing and managing PickableObject types in the copick configuration
3
3
  """
4
4
 
5
- from typing import List, Tuple
5
+ import copy
6
+ from typing import List, Optional, Tuple
6
7
 
7
8
  from copick.models import PickableObject
8
9
  from qtpy.QtCore import Qt
@@ -10,6 +11,7 @@ from qtpy.QtGui import QFont
10
11
  from qtpy.QtWidgets import (
11
12
  QCheckBox,
12
13
  QColorDialog,
14
+ QComboBox,
13
15
  QDialog,
14
16
  QDialogButtonBox,
15
17
  QDoubleSpinBox,
@@ -29,8 +31,11 @@ from qtpy.QtWidgets import (
29
31
  QWidget,
30
32
  )
31
33
 
34
+ from copick_shared_ui.core.types import FILAMENT_METADATA_KEY, FILAMENT_METADATA_NAMESPACE, filament_spec_of
32
35
  from copick_shared_ui.util.validation import validate_copick_name
33
36
 
37
+ POLARITY_CHOICES = [("Not stated", None), ("Polar", True), ("Apolar", False)]
38
+
34
39
 
35
40
  class ColorButton(QPushButton):
36
41
  """Button that displays and allows selection of a color"""
@@ -261,6 +266,15 @@ class EditObjectTypesDialog(QDialog):
261
266
  )
262
267
  left_layout.addRow("Type:", self._is_particle_cb)
263
268
 
269
+ # Is filament checkbox (a filament is a particle annotated with ordered points along its axis)
270
+ self._is_filament_cb = QCheckBox("Is Filament")
271
+ self._is_filament_cb.setChecked(False)
272
+ self._is_filament_cb.setToolTip(
273
+ "Check for continuous assemblies (e.g. microtubules, actin) annotated with ordered points along their "
274
+ "axis.\nPicks then carry the filament ID as instance ID. Requires 'Is Particle'.",
275
+ )
276
+ left_layout.addRow("", self._is_filament_cb)
277
+
264
278
  # Label (numeric ID)
265
279
  self._label_spin = QSpinBox()
266
280
  self._label_spin.setRange(1, 9999)
@@ -346,9 +360,39 @@ class EditObjectTypesDialog(QDialog):
346
360
 
347
361
  right_group.setLayout(right_layout)
348
362
 
363
+ # Filament properties (stored as metadata["copick"]["filament"])
364
+ self._filament_group = QGroupBox("Filament Properties")
365
+ filament_layout = QFormLayout()
366
+
367
+ self._polarity_combo = QComboBox()
368
+ for text, _value in POLARITY_CHOICES:
369
+ self._polarity_combo.addItem(text)
370
+ self._polarity_combo.setToolTip("Whether the structure has a polarity (microtubules and actin are polar)")
371
+ filament_layout.addRow("Polarity:", self._polarity_combo)
372
+
373
+ self._rise_spin = QDoubleSpinBox()
374
+ self._rise_spin.setRange(0.0, 10000.0)
375
+ self._rise_spin.setDecimals(2)
376
+ self._rise_spin.setValue(0.0)
377
+ self._rise_spin.setSpecialValueText("Not set")
378
+ self._rise_spin.setToolTip("Axial rise per subunit (descriptive only; never used as a sampling distance)")
379
+ filament_layout.addRow("Helical rise (Å):", self._rise_spin)
380
+
381
+ self._twist_spin = QDoubleSpinBox()
382
+ self._twist_spin.setRange(-361.0, 360.0)
383
+ self._twist_spin.setDecimals(2)
384
+ self._twist_spin.setValue(-361.0)
385
+ self._twist_spin.setSpecialValueText("Not set")
386
+ self._twist_spin.setToolTip("Twist per subunit (descriptive only)")
387
+ filament_layout.addRow("Helical twist (°):", self._twist_spin)
388
+
389
+ self._filament_group.setLayout(filament_layout)
390
+ self._filament_group.setEnabled(False)
391
+
349
392
  # Add groups to grid
350
393
  grid_layout.addWidget(left_group, 0, 0)
351
394
  grid_layout.addWidget(right_group, 0, 1)
395
+ grid_layout.addWidget(self._filament_group, 1, 0, 1, 2)
352
396
 
353
397
  # Form action buttons
354
398
  form_buttons_layout = QHBoxLayout()
@@ -393,6 +437,18 @@ class EditObjectTypesDialog(QDialog):
393
437
  self._name_edit.textChanged.connect(self._validate_form)
394
438
  self._label_spin.valueChanged.connect(self._validate_form)
395
439
 
440
+ # A filament must be a particle
441
+ self._is_particle_cb.toggled.connect(self._on_particle_toggled)
442
+ self._is_filament_cb.toggled.connect(self._on_filament_toggled)
443
+
444
+ def _on_particle_toggled(self, checked: bool):
445
+ self._is_filament_cb.setEnabled(checked)
446
+ if not checked:
447
+ self._is_filament_cb.setChecked(False)
448
+
449
+ def _on_filament_toggled(self, checked: bool):
450
+ self._filament_group.setEnabled(checked)
451
+
396
452
  def _populate_objects_table(self):
397
453
  """Populate the objects table"""
398
454
  self._objects_table.setRowCount(len(self._existing_objects))
@@ -404,7 +460,10 @@ class EditObjectTypesDialog(QDialog):
404
460
  self._objects_table.setItem(row, 0, name_item)
405
461
 
406
462
  # Type
407
- type_text = "Particle" if obj.is_particle else "Segmentation"
463
+ if filament_spec_of(obj) is not None:
464
+ type_text = "Filament"
465
+ else:
466
+ type_text = "Particle" if obj.is_particle else "Segmentation"
408
467
  type_item = QTableWidgetItem(type_text)
409
468
  type_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
410
469
  self._objects_table.setItem(row, 1, type_item)
@@ -626,6 +685,11 @@ class EditObjectTypesDialog(QDialog):
626
685
  self._identifier_edit.clear()
627
686
  self._threshold_spin.setValue(self._threshold_spin.minimum())
628
687
  self._radius_spin.setValue(self._radius_spin.minimum())
688
+ self._is_filament_cb.setEnabled(True)
689
+ self._is_filament_cb.setChecked(False)
690
+ self._polarity_combo.setCurrentIndex(0)
691
+ self._rise_spin.setValue(self._rise_spin.minimum())
692
+ self._twist_spin.setValue(self._twist_spin.minimum())
629
693
 
630
694
  # Hide validation messages
631
695
  self._name_validation.hide()
@@ -658,6 +722,31 @@ class EditObjectTypesDialog(QDialog):
658
722
  else:
659
723
  self._radius_spin.setValue(self._radius_spin.minimum())
660
724
 
725
+ spec = filament_spec_of(obj)
726
+ self._is_filament_cb.setChecked(spec is not None)
727
+ spec = spec or {}
728
+ polarity = spec.get("polar")
729
+ self._polarity_combo.setCurrentIndex(next(i for i, (_t, v) in enumerate(POLARITY_CHOICES) if v == polarity))
730
+ rise = spec.get("helical_rise_a")
731
+ self._rise_spin.setValue(rise if rise is not None else self._rise_spin.minimum())
732
+ twist = spec.get("helical_twist_deg")
733
+ self._twist_spin.setValue(twist if twist is not None else self._twist_spin.minimum())
734
+
735
+ def _filament_spec_from_form(self, previous: Optional[dict]) -> Optional[dict]:
736
+ """The filament spec in the form, keeping any extra keys of the previous spec; None if not a filament."""
737
+ if not (self._is_particle_cb.isChecked() and self._is_filament_cb.isChecked()):
738
+ return None
739
+ spec = dict(previous or {})
740
+ polar = POLARITY_CHOICES[self._polarity_combo.currentIndex()][1]
741
+ rise = self._rise_spin.value() if self._rise_spin.value() != self._rise_spin.minimum() else None
742
+ twist = self._twist_spin.value() if self._twist_spin.value() != self._twist_spin.minimum() else None
743
+ for key, value in (("polar", polar), ("helical_rise_a", rise), ("helical_twist_deg", twist)):
744
+ if value is None:
745
+ spec.pop(key, None)
746
+ else:
747
+ spec[key] = value
748
+ return spec
749
+
661
750
  def _populate_initial_data(self):
662
751
  """Populate initial data for new object"""
663
752
  existing_labels = {obj.label for obj in self._existing_objects if obj.label is not None}
@@ -686,7 +775,10 @@ class EditObjectTypesDialog(QDialog):
686
775
  if self._radius_spin.value() != self._radius_spin.minimum():
687
776
  radius = self._radius_spin.value()
688
777
 
689
- return PickableObject(
778
+ # Start from the edited object so fields the form does not show (metadata, and anything newer copick
779
+ # versions add) survive the edit.
780
+ data = copy.deepcopy(self._selected_object.model_dump()) if self._selected_object is not None else {}
781
+ data.update(
690
782
  name=name,
691
783
  is_particle=is_particle,
692
784
  label=label,
@@ -698,6 +790,25 @@ class EditObjectTypesDialog(QDialog):
698
790
  radius=radius,
699
791
  )
700
792
 
793
+ # Filament declaration: metadata["copick"]["filament"]
794
+ previous = filament_spec_of(self._selected_object) if self._selected_object is not None else None
795
+ spec = self._filament_spec_from_form(previous)
796
+ metadata = dict(data.get("metadata") or {})
797
+ namespace = metadata.get(FILAMENT_METADATA_NAMESPACE)
798
+ namespace = dict(namespace) if isinstance(namespace, dict) else {}
799
+ if spec is None:
800
+ namespace.pop(FILAMENT_METADATA_KEY, None)
801
+ else:
802
+ namespace[FILAMENT_METADATA_KEY] = spec
803
+ if namespace:
804
+ metadata[FILAMENT_METADATA_NAMESPACE] = namespace
805
+ else:
806
+ metadata.pop(FILAMENT_METADATA_NAMESPACE, None)
807
+ if metadata or "metadata" in data:
808
+ data["metadata"] = metadata
809
+
810
+ return PickableObject(**data)
811
+
701
812
  def _validate_form(self):
702
813
  """Validate the current form state"""
703
814
  name_text = self._name_edit.text().strip()
@@ -794,6 +905,7 @@ class EditObjectTypesDialog(QDialog):
794
905
  or current.identifier != original.identifier
795
906
  or current.map_threshold != original.map_threshold
796
907
  or current.radius != original.radius
908
+ or getattr(current, "metadata", None) != getattr(original, "metadata", None)
797
909
  ):
798
910
  return True
799
911