Skip to content

Commit 380608e

Browse files
committed
Track scene object creator attribution
1 parent b520d40 commit 380608e

31 files changed

Lines changed: 379 additions & 84 deletions

docs/content/developer/changes_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ This page tracks major architectural and documentation milestones.
3535
- 2026-07-14: The ElasNetMT add-on was renamed to **ElastNetMT**. The Python package is now `elastnetmt`, the MolSysViewer bridge module is `molsysviewer_elastnetmt`, and `KNOWN_ADDON_MODULES` discovers the new name only. Environments with the old package installed must reinstall it; the previous module name is no longer discovered.
3636
- 2026-07-15: Breaking scene-manager consistency fix: `info(tag)` now returns a `dict` for one tag across regions, shapes, annotations, measurements, and layers; shapes and measurements previously returned a one-element list. A missing tag raises `ValueError` in all five managers; regions and layers previously leaked `KeyError`. Calling `info()` without a tag continues to return a list.
3737
- 2026-07-15: Layer summaries no longer expose `kind`: a layer can contain objects from several scene domains, so reporting the last attached member's kind was misleading. The internal `Layer.kind` protocol/state field remains unchanged.
38+
- 2026-07-15: Scene-object creator attribution added. Add-ons can wrap public scene construction in `with view.attributed_to("elastnetmt"):`; layers, regions, shapes, annotations, measurements, and sections created inside expose an immutable `owner` property. The optional owner is shown in Studio summaries and survives state-v2 export/import. It is informational only and never restricts rename, movement, visibility, or deletion. Older v2 documents without `owner` restore objects as user-created (`owner=None`).

docs/content/developer/public_api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ edits on an existing viewer are provided by the MolSysMT addon namespace:
5555

5656
`MolSysView` is also explicitly growing an inspection-oriented object API beyond the minimal viewer shell. Public user-facing methods now include:
5757

58+
- `attributed_to(owner)`
59+
- context manager for add-ons creating scene objects through the public API
60+
- objects created inside expose the immutable informational property `owner`
61+
- attribution does not grant ownership rights or prevent user operations
5862
- `load(..., mode="add" | "replace" | "append_structures" | "auto", ...)`
5963
- current default is `mode="add"`
6064
- first load initializes `whole` without creating an automatic region

molsysviewer/annotations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def summarize(record: dict[str, Any]) -> dict[str, Any]:
148148
return {
149149
"kind": "label" if record.get("op") == "add_label" else "annotation",
150150
"tag": record_tag,
151+
"owner": None if layer is None else layer.owner,
151152
"layer_tag": None if layer is None else getattr(layer, "layer_tag", record_tag),
152153
"text": options.get("text"),
153154
"style": dict(options.get("style") or {}),

molsysviewer/js/src/managers/handlers/state-handlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ interface RegionEntry {
7474

7575
export interface RegionSummary {
7676
tag: string;
77+
owner?: string;
7778
atom_indices: number[];
7879
atom_count: number;
7980
selection?: string;
@@ -1042,6 +1043,7 @@ export class StateHandlers {
10421043
.filter(item => typeof item?.tag === "string")
10431044
.map(item => ({
10441045
tag: item.tag,
1046+
owner: typeof item.owner === "string" ? item.owner : undefined,
10451047
atom_indices: Array.isArray(item.atom_indices)
10461048
? item.atom_indices.filter((value): value is number => typeof value === "number")
10471049
: [],

molsysviewer/js/src/managers/viewer-controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,6 +2173,7 @@ export class MolSysViewerController {
21732173
.filter((item: any) => typeof item?.tag === "string" && (item.provenance === "auto" || item.provenance === "user"))
21742174
.map((item: any) => ({
21752175
tag: item.tag,
2176+
owner: typeof item.owner === "string" ? item.owner : undefined,
21762177
provenance: item.provenance,
21772178
hidden: !!item.hidden,
21782179
}));
@@ -2184,6 +2185,7 @@ export class MolSysViewerController {
21842185
this.annotationSummaries = records.filter((item: any) => typeof item?.tag === "string").map((item: any) => ({
21852186
kind: typeof item.kind === "string" ? item.kind : "annotation",
21862187
tag: item.tag,
2188+
owner: typeof item.owner === "string" ? item.owner : undefined,
21872189
layerTag: typeof item.layer_tag === "string" ? item.layer_tag : undefined,
21882190
text: typeof item.text === "string" ? item.text : item.tag,
21892191
style: item.style && typeof item.style === "object" ? {
@@ -2215,6 +2217,7 @@ export class MolSysViewerController {
22152217
this.measurementSummaries = records.filter((item: any) => typeof item?.tag === "string").map((item: any) => ({
22162218
kind: ["distance", "angle", "dihedral"].includes(item.kind) ? item.kind : "measurement",
22172219
tag: item.tag,
2220+
owner: typeof item.owner === "string" ? item.owner : undefined,
22182221
layerTag: typeof item.layer_tag === "string" ? item.layer_tag : undefined,
22192222
picks: typeof item.n_picks === "number" ? item.n_picks : 0,
22202223
hidden: !!item.hidden,
@@ -2259,6 +2262,7 @@ export class MolSysViewerController {
22592262
op: typeof item.op === "string" ? item.op : "",
22602263
kind: typeof item.kind === "string" ? item.kind : "shape",
22612264
tag: item.tag,
2265+
owner: typeof item.owner === "string" ? item.owner : undefined,
22622266
layerTag: typeof item.layer_tag === "string" ? item.layer_tag : undefined,
22632267
title: typeof item.title === "string" ? item.title : item.tag,
22642268
subtitle: typeof item.subtitle === "string" ? item.subtitle : undefined,
@@ -2829,20 +2833,23 @@ export class MolSysViewerController {
28292833
...this.annotationSummaries.map((item) => ({
28302834
kind: "annotation" as const,
28312835
tag: item.tag,
2836+
owner: item.owner,
28322837
title: item.text,
28332838
layerTag: item.layerTag,
28342839
hidden: item.hidden,
28352840
})),
28362841
...this.measurementSummaries.map((item) => ({
28372842
kind: "measurement" as const,
28382843
tag: item.tag,
2844+
owner: item.owner,
28392845
title: `${item.kind[0].toUpperCase()}${item.kind.slice(1)}`,
28402846
layerTag: item.layerTag,
28412847
hidden: item.hidden,
28422848
})),
28432849
...this.shapeSummaries.map((item) => ({
28442850
kind: "shape" as const,
28452851
tag: item.tag,
2852+
owner: item.owner,
28462853
title: item.title,
28472854
layerTag: item.layerTag,
28482855
hidden: item.hidden,

molsysviewer/js/src/messages/viewer-messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ export type SetRegionSummariesMessage = {
474474
op: "set_region_summaries";
475475
regions?: Array<{
476476
tag: string;
477+
owner?: string | null;
477478
atom_indices?: number[];
478479
atom_count?: number;
479480
selection?: string;
@@ -496,6 +497,7 @@ export type SetLayerSummariesMessage = {
496497
op: "set_layer_summaries";
497498
layers?: Array<{
498499
tag: string;
500+
owner?: string | null;
499501
provenance: "auto" | "user";
500502
hidden?: boolean;
501503
}>;
@@ -521,6 +523,7 @@ export type SetAnnotationSummariesMessage = {
521523
annotations?: Array<{
522524
kind?: string;
523525
tag: string;
526+
owner?: string | null;
524527
layer_tag?: string | null;
525528
text?: string | null;
526529
style?: LabelStyle | null;
@@ -540,6 +543,7 @@ export type SetMeasurementSummariesMessage = {
540543
measurements?: Array<{
541544
kind?: string;
542545
tag: string;
546+
owner?: string | null;
543547
layer_tag?: string | null;
544548
n_picks?: number;
545549
atom_indices?: number[];
@@ -575,6 +579,7 @@ export type SetShapeSummariesMessage = {
575579
op?: string;
576580
kind?: string;
577581
tag: string;
582+
owner?: string | null;
578583
layer_tag?: string | null;
579584
title?: string;
580585
subtitle?: string | null;

molsysviewer/js/src/ui/group-panel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export type OnAnnotationContext = (target: ContextMenuTarget, pageX: number, pag
8585
export type SavedSelectionSummary = { tag: string; atom_count: number; element_level?: string };
8686
export type RegionSummary = {
8787
tag: string;
88+
owner?: string;
8889
atom_count: number;
8990
hidden: boolean;
9091
layer?: string | null;

molsysviewer/js/src/ui/panels/annotations-panel.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type AnnotationLabelStyle = {
1313
export type AnnotationSummary = {
1414
kind: string;
1515
tag: string;
16+
owner?: string;
1617
layerTag?: string;
1718
text: string;
1819
hidden: boolean;
@@ -223,8 +224,8 @@ export class AnnotationsPanel extends BasePanel {
223224

224225
const identity = document.createElement("div");
225226
identity.textContent = item.broken
226-
? `${item.tag} · anchor broken`
227-
: `${item.tag} · ${item.nAtoms} atom${item.nAtoms === 1 ? "" : "s"}${item.layerTag && item.layerTag !== item.tag ? ` · layer: ${item.layerTag}` : ""}`;
227+
? `${item.tag} · anchor broken${item.owner ? ` · from ${item.owner}` : ""}`
228+
: `${item.tag} · ${item.nAtoms} atom${item.nAtoms === 1 ? "" : "s"}${item.layerTag && item.layerTag !== item.tag ? ` · layer: ${item.layerTag}` : ""}${item.owner ? ` · from ${item.owner}` : ""}`;
228229
identity.setAttribute("data-molsysviewer-annotation-identity", item.tag);
229230
Object.assign(identity.style, { fontSize: "10px", color: "rgba(244,244,245,0.58)" });
230231
row.appendChild(identity);

molsysviewer/js/src/ui/panels/layers-panel.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { makeButton, makeSectionHeader, makeSettingsCard } from "./ui-helpers";
55

66
export type LayerSummary = {
77
tag: string;
8+
owner?: string;
89
provenance: "auto" | "user";
910
hidden: boolean;
1011
};
1112

1213
export type LayerObjectSummary = {
1314
kind: "annotation" | "measurement" | "shape";
1415
tag: string;
16+
owner?: string;
1517
title: string;
1618
layerTag?: string;
1719
hidden?: boolean;
@@ -21,6 +23,7 @@ type MemberKind = "region" | LayerObjectSummary["kind"];
2123
type LayerMember = {
2224
kind: MemberKind;
2325
tag: string;
26+
owner?: string;
2427
title: string;
2528
layerTag?: string | null;
2629
hidden: boolean;
@@ -91,6 +94,7 @@ export class LayersPanel extends BasePanel {
9194
...this.regions.map(region => ({
9295
kind: "region" as const,
9396
tag: region.tag,
97+
owner: region.owner,
9498
title: region.tag,
9599
layerTag: region.layer,
96100
hidden: region.hidden,
@@ -99,6 +103,7 @@ export class LayersPanel extends BasePanel {
99103
...this.objects.map(object => ({
100104
kind: object.kind,
101105
tag: object.tag,
106+
owner: object.owner,
102107
title: object.title,
103108
layerTag: object.layerTag,
104109
hidden: !!object.hidden,
@@ -145,7 +150,7 @@ export class LayersPanel extends BasePanel {
145150
card.setAttribute("data-molsysviewer-layer-card", layer.tag);
146151

147152
const summary = document.createElement("div");
148-
summary.textContent = `${layer.members.length} member${layer.members.length === 1 ? "" : "s"}`;
153+
summary.textContent = `${layer.members.length} member${layer.members.length === 1 ? "" : "s"}${layer.owner ? ` · from ${layer.owner}` : ""}`;
149154
Object.assign(summary.style, { fontSize: "10px", color: "rgba(244,244,245,0.56)" });
150155
card.appendChild(summary);
151156

@@ -239,7 +244,7 @@ export class LayersPanel extends BasePanel {
239244
background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.05)",
240245
});
241246
const label = document.createElement("div");
242-
label.textContent = `${member.title} · ${member.kind}${member.atomCount === undefined ? "" : ` · ${member.atomCount} atoms`}`;
247+
label.textContent = `${member.title} · ${member.kind}${member.atomCount === undefined ? "" : ` · ${member.atomCount} atoms`}${member.owner ? ` · from ${member.owner}` : ""}`;
243248
Object.assign(label.style, { minWidth: "0", fontSize: "11px", color: member.hidden ? "rgba(244,244,245,0.42)" : "#f4f4f5" });
244249
const remove = makeButton("Remove", () => this.ctx.onAction("remove_member_from_layer", {
245250
layer: layerTag,

molsysviewer/js/src/ui/panels/measures-panel.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { makeButton, makeSectionHeader } from "./ui-helpers";
66
export type MeasurementSummary = {
77
kind: "distance" | "angle" | "dihedral" | "measurement";
88
tag: string;
9+
owner?: string;
910
layerTag?: string;
1011
picks: number;
1112
hidden: boolean;
@@ -202,7 +203,7 @@ export class MeasuresPanel extends BasePanel {
202203
row.appendChild(head);
203204

204205
const identity = document.createElement("div");
205-
identity.textContent = `${item.kind} · ${item.tag}${item.layerTag && item.layerTag !== item.tag ? ` · layer: ${item.layerTag}` : ""}`;
206+
identity.textContent = `${item.kind} · ${item.tag}${item.layerTag && item.layerTag !== item.tag ? ` · layer: ${item.layerTag}` : ""}${item.owner ? ` · from ${item.owner}` : ""}`;
206207
Object.assign(identity.style, { fontSize: "10px", color: "rgba(244,244,245,0.58)" });
207208
row.appendChild(identity);
208209
const endpoints = document.createElement("div");

0 commit comments

Comments
 (0)