Skip to content
Merged
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
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,12 @@ The experimental browser editor runs from a source checkout only (`chemvas --ui

### Fixed

- Changing bond length now refreshes desktop orbital lobes and handle distances
using the existing renderer. The visible size agrees with saving and reopening
the drawing, including after Undo/Redo.
- Changing bond length now refreshes desktop orbital lobes and visible
scale/rotate handles, including after Undo/Redo. Their size and positions
agree with saving and reopening the drawing.
- Undoing an atom charge shortcut (`+`/`-`) or an atom-bound Mark-tool
addition now restores pre-existing atom annotations, including values
without drawn marks.
- Fixed macOS Ctrl-click editing before the mark context menu, handle priority
over marks, and excessive scrolling in the owner-candidate preview.
- Qt borderless rectangle selection no longer acquires interior holes from
Expand Down
42 changes: 39 additions & 3 deletions app/chemvas/ui/canvas/canvas_mark_scene_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ChangeAtomLabelCommand,
DeleteSceneItemsCommand,
RebindMarkCommand,
SetAtomAnnotationCommand,
)
from chemvas.ui.molecule.atom_label_access import atom_has_visible_label_for
from chemvas.ui.scene.scene_item_access import remove_item_from_canvas_scene
Expand Down Expand Up @@ -70,6 +71,8 @@ def change_charge_for_atom(self, atom_id: int, delta: int) -> None:
atom = self.canvas.model.atom_for_id(atom_id)
if atom is None:
return
before = self.canvas.model.atom_annotations.get(atom_id)
before = dict(before) if before is not None else None
with (
document_transaction(self.canvas, history_service=self.history),
history_transaction_scope(self.history.operations),
Expand Down Expand Up @@ -99,7 +102,21 @@ def change_charge_for_atom(self, atom_id: int, delta: int) -> None:
command = AddSceneItemsCommand.from_items(
item_states=[mark_state_dict_for(self.canvas, item)], items=[item]
)
self.history.push(command)
self._push_atom_mark_edit(atom_id, before, command)

def _push_atom_mark_edit(
self,
atom_id: int,
before: dict[str, int] | None,
command: HistoryCommand,
) -> None:
after = self.canvas.model.atom_annotations.get(atom_id)
# A loaded annotation need not have marks; mark replay alone would
# rebuild it from the marks and lose that value on Undo.
annotation = SetAtomAnnotationCommand(
atom_id, before, dict(after) if after is not None else None
)
self.history.push(CompositeCommand([annotation, command]))

def reveal_unmarked_isolated_carbons(
self, atom_ids: set[int]
Expand Down Expand Up @@ -221,9 +238,28 @@ def add_mark_for_atom(
*,
kind: str | None = None,
):
item = self._add_mark_for_atom(atom_id, click_pos, kind=kind, record=True)
if item is not None:
if (
self.canvas.model.atom_for_id(atom_id) is None
or self.scene_decoration_service is None
):
return None
before = self.canvas.model.atom_annotations.get(atom_id)
before = dict(before) if before is not None else None
with (
document_transaction(self.canvas, history_service=self.history),
history_transaction_scope(self.history.operations),
):
item = self._add_mark_for_atom(atom_id, click_pos, kind=kind, record=False)
if item is None:
return None
self.sync_marks_for_atom(atom_id)
self._push_atom_mark_edit(
atom_id,
before,
AddSceneItemsCommand.from_items(
item_states=[mark_state_dict_for(self.canvas, item)], items=[item]
),
)
return item

def materialize_mark_for_atom(
Expand Down
48 changes: 48 additions & 0 deletions app/chemvas/ui/history/history_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ def restore_mark_ownership(
) -> None: ...


class HistoryAtomAnnotationOperations(Protocol):
def restore_atom_annotation(
self, atom_id: int, annotation: dict[str, int] | None
) -> None: ...


class HistoryCalculationPlanOperations(Protocol):
def set_calculation_plan(self, state: dict[str, object] | None) -> None: ...

Expand Down Expand Up @@ -295,6 +301,46 @@ def redo(self, operations: HistoryMarkOperations) -> None:
self._apply(operations, undo=False)


@dataclass
class SetAtomAnnotationCommand(HistoryCommand):
"""Exact annotation of one atom across a user mark edit.

Mark add/remove replay rebuilds the annotation from the marks alone, which
loses a loaded value that no mark represents. A charge edit lists this
command first so Undo applies the exact prior value after that rebuild.
"""

history_transaction_snapshot_covers_state = True
history_transaction_owns_exact_state = True

atom_id: int
before: dict[str, int] | None
after: dict[str, int] | None

def _apply(
self,
operations: HistoryAtomAnnotationOperations,
state: dict[str, int] | None,
rollback_state: dict[str, int] | None,
) -> None:
with history_command_transaction(
operations,
inverse=lambda: operations.restore_atom_annotation(
self.atom_id, rollback_state
),
inverse_phase="restoring the prior atom annotation",
):
operations.restore_atom_annotation(self.atom_id, state)

@override
def undo(self, operations: HistoryAtomAnnotationOperations) -> None:
self._apply(operations, self.before, self.after)

@override
def redo(self, operations: HistoryAtomAnnotationOperations) -> None:
self._apply(operations, self.after, self.before)


@dataclass
class UpdateSceneItemCommand(HistoryCommand):
history_transaction_snapshot_covers_state = True
Expand Down Expand Up @@ -687,6 +733,7 @@ def redo(self, operations: HistoryAtomLabelOperations) -> None:
"ChangeAtomLabelCommand",
"DeleteSceneItemsCommand",
"GroupSceneItemsCommand",
"HistoryAtomAnnotationOperations",
"HistoryAtomLabelOperations",
"HistoryCalculationPlanOperations",
"HistoryGroupOperations",
Expand All @@ -695,6 +742,7 @@ def redo(self, operations: HistoryAtomLabelOperations) -> None:
"HistorySceneItemOperations",
"HistorySelectionGeometryOperations",
"SetAnnotationSettingsCommand",
"SetAtomAnnotationCommand",
"SetCalculationPlanCommand",
"SetNoteTextCommand",
"SetSceneGeometryCommand",
Expand Down
12 changes: 12 additions & 0 deletions app/chemvas/ui/history/history_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
restore_scene_item,
)
from chemvas.ui.scene.scene_signal_blocking import blocked_scene_signals
from chemvas.ui.selection.selection_info_access import emit_selection_info_for
from chemvas.ui.transactions.document import DocumentSavepoint
from chemvas.ui.transactions.scene_runtime import capture_scene_runtime
from chemvas.ui.transactions.scene_runtime_restore import (
Expand Down Expand Up @@ -428,6 +429,17 @@ def restore_mark_ownership(
else:
model_annotations.pop(atom_id, None)

def restore_atom_annotation(
self, atom_id: int, annotation: dict[str, int] | None
) -> None:
if annotation is None:
self.__canvas.model.atom_annotations.pop(atom_id, None)
else:
self.__canvas.model.atom_annotations[atom_id] = dict(annotation)
# The selection formula readout reflects annotations; mark replay has
# already refreshed it from the value this replaces.
emit_selection_info_for(self.__canvas)

def set_calculation_plan(self, state: dict[str, object] | None) -> None:
set_calculation_plan_for(self.__canvas, state)

Expand Down
7 changes: 7 additions & 0 deletions app/chemvas/ui/molecule/bond_length_graphics_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@ def refresh_bond_length_graphics_for(canvas) -> None:
)
canvas.services.scene_decoration_build_service.set_mark_center(item, center)
canvas.services.selection.update_selection_outline()
# Orbital handles sit at the lobes' bond-length distance; redraw any still
# shown. The selection-outline refresh keeps clearing unselected targets.
handle_state = canvas.runtime_state.handle_state
if handle_state.active_handles and any(
item is handle_state.target for item in canvas.runtime_state.orbital_items()
):
canvas.services.handle_overlay_service.show_orbital_handles(handle_state.target)


__all__ = ["refresh_bond_length_graphics_for"]
2 changes: 1 addition & 1 deletion docs/REFERENCE.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Chemvas는 분자 모델, 주석, 화살표, 설정 정보를 사람이 읽기
- **결합 편집 (결합 위에 마우스 올림)**: 단일선 `1`, 이중선 `2`, 삼중선 `3`, 굵은선 `b`, 쐐기 `w`, 해시 `h`, 점선 `d`, 굵은 이중선 `Shift+B`, 점선 이중선 `Shift+D`, 이중선 위치 `l`/`c`/`r`, 벤젠 융합 `a`, 고리 융합 `4`~`8`
- **변형 및 조작**: 좌우 반전 `Ctrl+Shift+H`, 상하 반전 `Ctrl+Shift+V`, 회전 `Alt+Up/Down`(15°) / `Alt+Left/Right`(1°), 미세 이동 `Shift+방향키`(10 pt)
- **정렬 및 배치**: **Edit ▸ Align** (좌, 우, 상, 하, 중앙) 및 **Edit ▸ Distribute** (가로, 세로)
- **일반**: 저장 `Ctrl+S`, 열기 `Ctrl+O`, 전체 선택 `Ctrl+A`, 그룹화 `Ctrl+G`, 그룹 해제 `Ctrl+Shift+G`, 실행 취소 `Ctrl+Z`, 다시 실행 `Ctrl+Y`, 삭제 `Delete`/`Backspace`
- **일반**: 저장 `Ctrl+S`, 열기 `Ctrl+O`, 전체 선택 `Ctrl+A`, 그룹화 `Ctrl+G`, 그룹 해제 `Ctrl+Shift+G`, 실행 취소 `Ctrl+Z`, 다시 실행 (Edit ▸ Redo에 표시된 단축키 사용, 예: Linux `Ctrl+Shift+Z`), 삭제 `Delete`/`Backspace`

### 원자 라벨 단축키 표

Expand Down
2 changes: 1 addition & 1 deletion docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Select tools from the canvas or edit hovered atoms/bonds with the shortcuts belo
- **Bond Editing (hover over bond)**: Single `1`, Double `2`, Triple `3`, Bold `b`, Wedge `w`, Hash `h`, Dotted `d`, Bold double `Shift+B`, Dotted double `Shift+D`, double-bond alignment `l`/`c`/`r`, Benzene fusion `a`, Ring fusion `4`–`8`
- **Transformations**: Flip Horizontal `Ctrl+Shift+H`, Flip Vertical `Ctrl+Shift+V`, Rotate `Alt+Up/Down` (15°) / `Alt+Left/Right` (1°), Nudge `Shift+Arrows` (10 pt)
- **Alignment**: **Edit ▸ Align** (Left, Center, Right, Top, Middle, Bottom) and **Edit ▸ Distribute** (Horizontally, Vertically)
- **General**: Save `Ctrl+S`, Open `Ctrl+O`, Select All `Ctrl+A`, Group `Ctrl+G`, Ungroup `Ctrl+Shift+G`, Undo `Ctrl+Z`, Redo `Ctrl+Y`, Delete `Delete`/`Backspace`
- **General**: Save `Ctrl+S`, Open `Ctrl+O`, Select All `Ctrl+A`, Group `Ctrl+G`, Ungroup `Ctrl+Shift+G`, Undo `Ctrl+Z`, Redo (use shortcut displayed by Edit ▸ Redo, e.g. `Ctrl+Shift+Z` on Linux), Delete `Delete`/`Backspace`

### Atom-label hotkey map

Expand Down
Loading
Loading