Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"ezdxf>=1.4.3",
"ndv[vispy]>=0.5.0rc4",
"ome-writers[tensorstore,tifffile]>=0.3.0",
"pydantic-settings>=2.7.1",
Expand Down
11 changes: 11 additions & 0 deletions src/pymmcore_gui/_main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ def get_widget(self, key: str, create: bool = True) -> QWidget:
return widget

self._action_widgets[key] = widget
self._link_related_widgets(key, widget)

action = self.get_action(key)
dock = CDockWidget(self.dock_manager, info.text, self)
Expand Down Expand Up @@ -411,6 +412,16 @@ def _on_system_config_loaded(self) -> None:
settings.last_config = None
settings.flush()

def _link_related_widgets(self, key: str, widget: QWidget) -> None:
if key == WidgetAction.MDA_WIDGET:
if explorer := self._action_widgets.get(WidgetAction.STAGE_EXPLORER):
if hasattr(explorer, "set_mda_widget"):
explorer.set_mda_widget(widget)
elif key == WidgetAction.STAGE_EXPLORER:
if mda_widget := self._action_widgets.get(WidgetAction.MDA_WIDGET):
if hasattr(widget, "set_mda_widget"):
widget.set_mda_widget(mda_widget)

def _add_toolbar(self, name: str, tb_entry: ToolDictValue) -> None:
if callable(tb_entry):
tb = tb_entry(self._mmc, self)
Expand Down
5 changes: 2 additions & 3 deletions src/pymmcore_gui/actions/widget_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,9 @@ def create_config_wizard(parent: QWidget) -> pmmw.ConfigWizard:

def create_stage_explorer_widget(parent: QWidget) -> pmmw.StageExplorer:
"""Create the Stage Explorer widget."""
from pymmcore_widgets import StageExplorer
from pymmcore_gui.widgets._mda_stage_explorer import MDALinkedStageExplorer

return StageExplorer(parent=parent, mmcore=_get_core(parent))
return MDALinkedStageExplorer(parent=parent, mmcore=_get_core(parent))


# ######################## WidgetAction Enum #########################
Expand Down Expand Up @@ -317,5 +317,4 @@ def create_about_widget(parent: QWidget) -> QWidget:
icon="mdi:map-search",
create_widget=create_stage_explorer_widget,
dock_area=DockWidgetArea.LeftDockWidgetArea,
floatable=False,
)
162 changes: 162 additions & 0 deletions src/pymmcore_gui/widgets/_chip_dxf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from __future__ import annotations

import warnings
from dataclasses import dataclass
from math import hypot, pi
from pathlib import Path
from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
from collections.abc import Iterable


@dataclass(slots=True)
class ChipCurve:
points: np.ndarray
closed: bool = False


@dataclass(slots=True)
class ChipOverlayData:
curves: list[ChipCurve]
reference_points: list[tuple[float, float]]
source: Path | None


def load_chip_overlay_data(path: str | Path) -> ChipOverlayData:
"""Load a DXF file and extract a 2D XY projection for overlay display."""
src = Path(path)
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message="'.*' deprecated - use '.*'",
category=DeprecationWarning,
module="pyparsing.*",
)
import ezdxf

doc = ezdxf.readfile(src)
curves: list[ChipCurve] = []
ref_points: list[tuple[float, float]] = []

for entity in doc.modelspace():
if entity.dxftype() == "3DSOLID":
sat_lines = getattr(entity, "sat", None) or getattr(entity, "acis_data", ())
c, pts = _curves_from_acis_lines(sat_lines)
curves.extend(c)
ref_points.extend(pts)

return ChipOverlayData(
curves=curves, reference_points=_unique_points(ref_points), source=src
)


def _curves_from_acis_lines(
lines: Iterable[str],
) -> tuple[list[ChipCurve], list[tuple[float, float]]]:
curves: list[ChipCurve] = []
points: list[tuple[float, float]] = []

for line in lines:
text = line.strip()
if text.startswith("straight-curve"):
segment = _parse_straight_curve(text)
if segment is not None:
curves.append(
ChipCurve(points=np.asarray(segment, dtype=float), closed=False)
)
points.extend((tuple(segment[0]), tuple(segment[-1])))
elif text.startswith("ellipse-curve"):
ellipse = _parse_ellipse_curve(text)
if ellipse is not None:
curves.append(ChipCurve(points=ellipse, closed=True))
points.extend(_sample_reference_points(ellipse))
elif text.startswith("point"):
pt = _parse_point(text)
if pt is not None:
points.append(pt)

return curves, points


def _parse_straight_curve(line: str) -> np.ndarray | None:
values = _extract_numeric_tokens(line)
if len(values) < 8:
return None

origin = np.array(values[0:3], dtype=float)
direction = np.array(values[3:6], dtype=float)
scalars = values[6:]

if len(scalars) >= 2:
start = origin + direction * scalars[0]
end = origin + direction * scalars[1]
else:
start = origin
end = origin + direction

return np.array([[start[0], start[1]], [end[0], end[1]]], dtype=float)


def _parse_ellipse_curve(line: str, samples: int = 64) -> np.ndarray | None:
values = _extract_numeric_tokens(line)
if len(values) < 9:
return None

center = np.array(values[0:3], dtype=float)
major = np.array(values[6:9], dtype=float)
radius = hypot(float(major[0]), float(major[1]))
if radius <= 0:
return None

angles = np.linspace(0, 2 * pi, samples, endpoint=False)
pts = np.column_stack(
(
center[0] + radius * np.cos(angles),
center[1] + radius * np.sin(angles),
)
)
return pts.astype(float)


def _parse_point(line: str) -> tuple[float, float] | None:
values = _extract_numeric_tokens(line)
if len(values) < 2:
return None
return float(values[0]), float(values[1])


def _extract_numeric_tokens(text: str) -> list[float]:
if "$-1" in text:
text = text.rsplit("$-1", maxsplit=1)[-1]

out: list[float] = []
for token in text.replace("#", " ").split():
try:
out.append(float(token))
except ValueError:
continue
return out


def _sample_reference_points(points: np.ndarray) -> list[tuple[float, float]]:
if len(points) < 4:
return [tuple(p) for p in points]
idxs = (0, len(points) // 4, len(points) // 2, (3 * len(points)) // 4)
return [tuple(points[i]) for i in idxs]


def _unique_points(
points: Iterable[tuple[float, float]], precision: int = 3
) -> list[tuple[float, float]]:
seen: set[tuple[float, float]] = set()
out: list[tuple[float, float]] = []
for x, y in points:
key = (round(float(x), precision), round(float(y), precision))
if key in seen:
continue
seen.add(key)
out.append((float(x), float(y)))
return out
Loading