Skip to content

Commit

Permalink
Merge pull request #21 from Ledger-Donjon/20-add-feature-to-manage-ma…
Browse files Browse the repository at this point in the history
…rkers

Add feature to manage markers
  • Loading branch information
mmouchous-ledger authored Nov 27, 2024
2 parents 976d755 + e06b7ed commit f681e8a
Show file tree
Hide file tree
Showing 15 changed files with 256 additions and 30 deletions.
2 changes: 1 addition & 1 deletion laserstudio/instruments/camera_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, config: dict):
f"Camera's resolution {self.width}px; {self.height}px"
)
logging.getLogger("laserstudio").info(
f"Image's dimension {self.width_um}µm; {self.height_um}µm (without considering any magnifier)"
f"Image's dimension {self.width_um}\xa0µm; {self.height_um}\xa0µm (without considering any magnifier)"
)

def __del__(self):
Expand Down
2 changes: 1 addition & 1 deletion laserstudio/instruments/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def move_to(self, position: Vector, wait: bool):
for i, displacement in enumerate(displacement.data):
if abs(displacement) > self.guardrail:
logging.getLogger("laserstudio").error(
f"Do not move!! One axis ({i}) moves further than {self.guardrail}µm: {displacement}µm"
f"Do not move!! One axis ({i}) moves further than {self.guardrail}\xa0µm: {displacement}\xa0µm"
)
return
# Move to actual destination
Expand Down
24 changes: 21 additions & 3 deletions laserstudio/laserstudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
)
from typing import Optional, Any, Union

from .widgets.viewer import Viewer
from .widgets.viewer import Viewer, IdMarker
from .instruments.instruments import (
Instruments,
PDMInstrument,
Expand Down Expand Up @@ -83,6 +83,7 @@ def __init__(self, config: Optional[dict]):
# Toolbar: Markers
toolbar = MarkersToolbar(self.viewer)
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)
self.addToolBar(Qt.ToolBarArea.RightToolBarArea, toolbar.markers_list_toolbar)

# Toolbar: Stage positioning
if self.instruments.stage is not None:
Expand Down Expand Up @@ -175,11 +176,11 @@ def __init__(self, config: Optional[dict]):
if window_state is not None:
self.restoreState(window_state)

def closeEvent(self, event):
def closeEvent(self, a0):
"""Saves user settings before closing the application."""
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("window-state", self.saveState())
super().closeEvent(event)
super().closeEvent(a0)

def handle_go_next(self) -> dict:
"""Go Next operation.
Expand Down Expand Up @@ -236,6 +237,23 @@ def handle_position(self, pos: Optional[list[float]]) -> dict:
self.instruments.stage.move_to(Vector(*pos), wait=True)
return {"pos": self.instruments.stage.position.data}

def handle_markers(self) -> list[dict]:
"""Handle a Markers API request to get the list of markers."""

return [
{
"id": marker.id if isinstance(marker, IdMarker) else -1,
"pos": [marker.pos().x(), marker.pos().y()],
"color": [
marker.qfillcolor.redF(),
marker.qfillcolor.greenF(),
marker.qfillcolor.blueF(),
marker.qfillcolor.alphaF(),
],
}
for marker in self.viewer.markers
]

def handle_add_markers(
self, positions: Optional[list[list[float]]], color: Optional[list[float]]
) -> dict:
Expand Down
8 changes: 8 additions & 0 deletions laserstudio/lsapi/lsapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ def autofocus(self) -> List[float]:
"""
return self.send("motion/autofocus").json()

def markers(self) -> List[Dict[str, Union[int, Tuple[float, float]]]]:
"""
Get the list of markers in the scene.
:return: A list of dictionaries, each containing the marker's id, position and RGBA color.
"""
return self.send("annotation/markers").json()

def marker(
self,
color: Union[Tuple[float, float, float], Tuple[float, float, float, float]] = (
Expand Down
2 changes: 1 addition & 1 deletion laserstudio/lsapi/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

setup(
name="lsapi",
version="1.1",
version="1.2",
install_requires=["requests", "Pillow"],
python_requires=">=3.7",
)
9 changes: 9 additions & 0 deletions laserstudio/restserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ def handle_add_markers(
):
return QVariant(self.laser_studio.handle_add_markers(pos, color))

@pyqtSlot(result="QVariant")
def handle_markers(self):
return QVariant(self.laser_studio.handle_markers())

@pyqtSlot(QVariant, result="QVariant")
def handle_position(self, pos: Optional[List[float]]):
return QVariant(self.laser_studio.handle_position(pos))
Expand Down Expand Up @@ -311,6 +315,11 @@ def post(self):
qvar = RestServer.invoke("handle_add_markers", QVariant(pos), QVariant(color))
return cast(dict, qvar)

@annotations.route("/markers")
class Markers(Resource):
def get(self):
qvar = RestServer.invoke("handle_markers")
return cast(List[dict], qvar)

# instruments = flask_api.namespace("instruments", description="Control instruments")

Expand Down
4 changes: 2 additions & 2 deletions laserstudio/widgets/keyboardbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def __init__(self, stage: StageInstrument, *__args):
w.setDecimals(1)
w.setValue(self.displacement_z)
w.valueChanged.connect(lambda v: self.__setattr__("displacement_xy", v))
w.setSuffix(" µm")
w.setSuffix("\xa0µm")
w.setSingleStep(5)
grid.addWidget(w, 3, 1, 1, 3)

Expand Down Expand Up @@ -94,7 +94,7 @@ def __init__(self, stage: StageInstrument, *__args):
w.setDecimals(1)
w.setValue(self.displacement_z)
w.valueChanged.connect(lambda v: self.__setattr__("displacement_z", v))
w.setSuffix(" µm")
w.setSuffix("\xa0µm")
w.setSingleStep(10)
grid.addWidget(w, 3, 4)

Expand Down
18 changes: 14 additions & 4 deletions laserstudio/widgets/marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ def size(self, value):
self.__update_size()

@property
def color(self):
def qcolor(self) -> QColor:
""":return: Current color, as QColor."""
return QColor(self.__color)

@property
def color(self) -> Union[QColor, Qt.GlobalColor, int]:
""":return: Current color, as QColor, Qt.GlobalColor or int."""
return self.__color

@color.setter
Expand All @@ -86,6 +91,11 @@ def color(self, value: Union[QColor, Qt.GlobalColor, int]):
self.__line2.setPen(self.__pen)
self.update()

@property
def qfillcolor(self) -> QColor:
""":return: Current fill color, as QColor."""
return QColor(self.__fillcolor)

@property
def fillcolor(self):
""":return: Current fill color, as QColor."""
Expand Down Expand Up @@ -128,8 +138,8 @@ def update_pos(self):
else:
self.setVisible(False)

def setToolTip(self, value: str):
self.__ellipse.setToolTip(value)
def setToolTip(self, toolTip: Optional[str]):
self.__ellipse.setToolTip(toolTip)


class IdMarker(Marker):
Expand All @@ -139,7 +149,7 @@ class IdMarker(Marker):
ID = 1

def __init__(self, parent=None, color=QColorConstants.Red) -> None:
super().__init__(parent, color=QColorConstants.Transparent, fillcolor=color)
super().__init__(parent, color=color, fillcolor=color)
self._id = IdMarker.ID
IdMarker.ID += 1

Expand Down
13 changes: 9 additions & 4 deletions laserstudio/widgets/scangeometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,14 @@ def __update(self):
)
else:
for poly in self.__scan_geometry.geoms:
self.__scan_zones_group.addToGroup(
ScanGeometry.__poly_to_path_item(poly)
)
if isinstance(poly, Polygon):
self.__scan_zones_group.addToGroup(
ScanGeometry.__poly_to_path_item(poly)
)
else:
logging.getLogger("laserstudio").error(
"Unsupported geometry type in scan geometry."
)
self.addToGroup(self.__scan_zones_group)

# Also, update the scan path with the new geometry
Expand Down Expand Up @@ -146,7 +151,7 @@ def density(self, value: int):

@staticmethod
def shapely_to_yaml(
geometry: Union[Polygon, MultiPolygon, GeometryCollection]
geometry: Union[Polygon, MultiPolygon, GeometryCollection],
) -> dict:
"""
:return: A dict for YAML serialization.
Expand Down
2 changes: 1 addition & 1 deletion laserstudio/widgets/toolbars/cameratoolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def __init__(self, laser_studio: "LaserStudio"):
w = QWidget()
grid.addWidget(QLabel("Refresh interval:"), 3, 1)
self.refresh_interval = w = ReturnSpinBox()
w.setSuffix("ms")
w.setSuffix("\xa0ms")
w.setMinimum(20)
w.setMaximum(10000)
w.setSingleStep(10)
Expand Down
146 changes: 146 additions & 0 deletions laserstudio/widgets/toolbars/markerslisttoolbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import (
QPushButton,
QToolBar,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QHBoxLayout,
QWidget,
)
from ..viewer import Viewer
from ..marker import Marker


class MarkersGroupListItem(QTreeWidgetItem):
def __init__(self, parent: QTreeWidget):
super().__init__(parent)
self.number_of_checked = 0

def update_checked_state(self):
# To prevent the itemChanged signal from being emitted
tw = self.treeWidget()
assert tw is not None
tw.blockSignals(True)
self.setToolTip(0, f"{self.number_of_checked} shown over {self.childCount()}")
tw.blockSignals(False)
if self.number_of_checked == 0:
self.setCheckState(0, Qt.CheckState.Unchecked)
elif self.number_of_checked == self.childCount():
self.setCheckState(0, Qt.CheckState.Checked)
else:
self.setCheckState(0, Qt.CheckState.PartiallyChecked)


class MarkersListItem(QTreeWidgetItem):
def __init__(self, group: MarkersGroupListItem, marker: Marker):
super().__init__(group)
self.group = group
x, y = marker.pos().x(), marker.pos().y()
self.marker = marker
visible = marker.isVisible()
self.setCheckState(
0, Qt.CheckState.Checked if visible else Qt.CheckState.Unchecked
)
self.setText(0, f"{x:.02f}\xa0µm, {y:.02f}\xa0µm")
self.setForeground(0, marker.fillcolor)
if visible:
group.number_of_checked += 1


class MarkersListToolbar(QToolBar):
def show_selected(self):
for item in self.list.selectedItems():
item.setCheckState(0, Qt.CheckState.Checked)

def hide_selected(self):
for item in self.list.selectedItems():
item.setCheckState(0, Qt.CheckState.Unchecked)

def refresh_list(self):
self.list.clear()
markers_by_colors: dict[str, list[Marker]] = {}
for marker in self.viewer.markers:
if type(marker.fillcolor) is QColor:
name: str = f"{marker.fillcolor.hue():02x}{marker.fillcolor.saturation():02x}{marker.fillcolor.lightness():02x}{marker.fillcolor.alpha():02x}"
else:
name = str(marker.color)
if name not in markers_by_colors:
markers_by_colors[name] = [marker]
else:
markers_by_colors[name].append(marker)
for color in sorted(markers_by_colors.keys()):
markers: list[Marker] = markers_by_colors[color]
group = MarkersGroupListItem(self.list)
group.setForeground(0, markers[0].fillcolor)
group.setText(
0, f"{len(markers)} marker" + ("" if len(markers) == 1 else "s")
)

self.list.itemChanged.disconnect(self.item_changed)
for marker in markers:
MarkersListItem(group, marker)
group.update_checked_state()
self.list.itemChanged.connect(self.item_changed)

def __init__(self, viewer: Viewer):
super().__init__("Markers List")
self.setObjectName("toolbar-markers-list") # For settings save and restore
self.setAllowedAreas(
Qt.ToolBarArea.LeftToolBarArea | Qt.ToolBarArea.RightToolBarArea
)
self.setFloatable(True)

self.viewer = viewer
self.list = QTreeWidget()
self.list.setHeaderHidden(True)
self.list.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
self.list.itemChanged.connect(self.item_changed)

w = QWidget()
self.addWidget(w)
vbox = QVBoxLayout()
w.setLayout(vbox)

w = QPushButton("Refresh")
w.clicked.connect(self.refresh_list)
vbox.addWidget(w)

hbox = QHBoxLayout()
w = QPushButton("Show")
w.clicked.connect(self.show_selected)
hbox.addWidget(w)
w = QPushButton("Hide")
w.clicked.connect(self.hide_selected)
hbox.addWidget(w)

vbox.addLayout(hbox)
vbox.addWidget(self.list)

self.list.itemDoubleClicked.connect(self.show_marker)

def show_marker(self, item: QTreeWidgetItem):
if isinstance(item, MarkersListItem):
self.viewer.follow_stage_sight = False
self.viewer.cam_pos_zoom = item.marker.pos(), self.viewer.cam_pos_zoom[1]

def item_changed(self, item: QTreeWidgetItem):
if isinstance(item, MarkersListItem):
visible = item.checkState(0) == Qt.CheckState.Checked
was_visible = item.marker.isVisible()
if not was_visible and visible:
item.group.number_of_checked += 1
elif was_visible and not visible:
item.group.number_of_checked -= 1
item.marker.setVisible(visible)
item.group.update_checked_state()
if isinstance(item, MarkersGroupListItem):
new_state = item.checkState(0)
if new_state == Qt.CheckState.PartiallyChecked:
return
for i in range(item.childCount()):
child = item.child(i)
if child is None:
continue
child.setCheckState(0, new_state)
Loading

0 comments on commit f681e8a

Please sign in to comment.