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
45 changes: 44 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ WASM viewer, so najaeda users get a viewer from `pip install` alone:
viewer's requests come back over the widget comm channel as
`{"json": "<message>"}` and are answered in the kernel, so the view shows
the netlist as edited by earlier cells. `Schematic.annotate(items)`
pushes diagnoses.
pushes diagnoses; `show_instance()`/`selected`/`on_select()` exchange
instances with najaeda (see "Getting to one hierarchical instance"
under Wire protocol).

The viewer bundle `static/naja-schematic.js` is **not** checked in: it's the
WASM target configured with `-DNAJA_SCHEMATIC_WASM_MODULE=ON` (single file,
Expand Down Expand Up @@ -347,6 +349,41 @@ all, so it gets diagnosis data via **File > Load Diagnosis JSON...**
(reads a `{"items": [...]}` file or a bare array through the same
`DiagnosisItem` parser) instead.

Getting to one hierarchical instance works in both directions, keyed by
the same instance-name path as diagnosis items:

- **Host → viewer.** `focus_instance` is a server push,
`{"response":"focus_instance","path":["u1","u2"]}`, from
`Schematic.show_instance()` / `show(instance=...)`, re-pushed after each
`root_response` like diagnoses. The viewer answers it with a
`resolve_instance` request (`{"request":"resolve_instance","path":[...]}`),
which both providers implement (`protocol.py`
`_handle_resolve_instance`, `LocalSNLProvider::buildResolveInstanceResponse`).
The reply looks like this:
```json
{ "response": "instance_resolved", "path": ["u1","u2"], "found": true,
"instance": { // absent for the top design ([])
"path": [["u1", 3, "Mod"], ["u2", 7, "AND2"]], // [name, child_id, model] per level
"design_ref": {...}, "has_instances": false, "source_loc": null,
"terms": [ {"name": "A", "child_id": 0, "direction": 0}, ... ] } } // expanded_instance_terms shape
```
The viewer then:
- clears the schematic and draws the instance alone with every pin open
(`EquipotentialView::showInstance()`, parsed by
`startInstanceFromResolved()`);
- selects it (`SelectionStore`);
- reveals it in the tree. `NetlistTree::reveal()` walks down the path,
requesting each level's Instances/Primitives group as needed; it's
asynchronous and `advanceReveal()` runs each frame.
- **Viewer → host.** There is one selected instance: click an instance row
in the tree or a box/frame body in the schematic. Each change sends a
notification with no reply, `{"request":"instance_selected","path":[...]}`,
plus a `get_properties` for it. The notebook widget intercepts
`instance_selected` into `Schematic.selected_path`; `Schematic.selected`
returns it as a najaeda `netlist.Instance`, and `on_select()` gives a
callback. The other hosts only log it (`protocol.py`) or ignore it
(`LocalSNLProvider`).

`get_properties`/`properties_response` is a general name/value inspector for
whatever object the UI asks about — an instance (including the top design
itself), a term/pin, or a net — answered by both `LocalSNLProvider`
Expand Down Expand Up @@ -414,6 +451,12 @@ appear as boxes/pins there).
`root_response`/`root_loaded`.
- **`PropertiesView`** — renders the current `PropertiesStore` contents as a
two-column name/value table into the "Properties" bottom-panel tab.
- **`SelectionStore`** — global static store (same pattern) for the one
selected instance, by pathKey (`""` = top). It's set from the tree, the
schematic or a host `focus_instance`, and drawn highlighted in both
views. Its listener (installed in `AppLogic.cpp`) reports each change to
the host. `NetlistTree` reveals selections made outside it by comparing
`revision()`. Cleared on every fresh `root_response`/`root_loaded`.
- **`SchematicLayout`** — the schematic's pure placement geometry, split out
of `EquipotentialView` so it's unit-testable without ImGui frames or a
provider (`tests/SchematicLayoutTest.cpp`): `IncrementalLayout` (per-net
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ set(CORE_SOURCES
src/SourceView.cpp
src/PropertiesStore.cpp
src/PropertiesView.cpp
src/SelectionStore.cpp
src/AppLogic.cpp
${IMGUI_SOURCES}
)
Expand Down Expand Up @@ -236,6 +237,7 @@ if(NOT EMSCRIPTEN_BUILD)
src/DiagnosisStore.cpp
src/SourceStore.cpp
src/PropertiesStore.cpp
src/SelectionStore.cpp
src/SchematicLayout.cpp
src/SchematicInteraction.cpp
src/EquipotentialView.cpp
Expand Down
3 changes: 3 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ path = [
"src/SchematicLayout.h",
"src/SchematicView.cpp",
"src/SchematicView.h",
"src/SelectionStore.cpp",
"src/SelectionStore.h",
"src/SourceStore.cpp",
"src/SourceStore.h",
"src/SourceView.cpp",
Expand All @@ -92,6 +94,7 @@ path = [
"tests/GUIDataTest.cpp",
"tests/NetlistTreeTest.cpp",
"tests/SchematicInteractionTest.cpp",
"tests/SelectionTest.cpp",
"tests/SchematicLayoutTest.cpp",
"tests/SourceStoreTest.cpp",
"tests/TypesJsonTest.cpp",
Expand Down
20 changes: 20 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ view.annotate([{"kind": "instance", "path": ["u_sub"], "severity": "error",
view
```

The view and your najaeda code can point each other at instances.
`show_instance()` takes a najaeda `Instance`, a `"u1/u2"` path or a list of
names. The viewer opens its tree down to that instance, selects it, and
draws it alone in the schematic. Each pin there is open, so you can click
one to add its net and grow the schematic from the instance. In the other
direction, the instance selected in the viewer (click it in the tree or the
schematic) is available as a najaeda `Instance`:

```python
view.show_instance(netlist.get_instance_by_path(["u_sub", "u_and"]))
view.show_instance("u_sub/u_and") # same thing

inst = view.selected # what the user clicked, or None
inst.get_model_name()

view.on_select(lambda inst: print("selected", inst))
```

`naja_schematic.show(instance="u_sub/u_and")` starts a new view on it.

## From a shell

```bash
Expand Down
102 changes: 79 additions & 23 deletions python/naja_schematic/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,42 +454,89 @@ def _handle_trace_driver(u, request):
}]


def bit_terms_json(design):
# Every pin of a design, bus terms expanded bit by bit ("data[3]", with
# "bit" set) so each gets its own schematic pin.
terms = []
for term in design.getTerms():
if isinstance(term, naja.SNLBusTerm):
lo, hi = sorted((term.getLSB(), term.getMSB()))
for b in range(lo, hi + 1):
bit_term = term.getBusTermBit(b)
if bit_term:
terms.append({
"name": f"{term.getName()}[{b}]",
"child_id": bit_term.getID(),
"direction": direction_to_int(bit_term.getDirection()),
"bit": b,
})
else:
terms.append({
"name": term.getName(),
"child_id": term.getID(),
"direction": direction_to_int(term.getDirection()),
})
return terms


def _handle_expand_instance_terms(u, request):
path_key = request.get("path_key", "")
design_ref = get_design_ref(request.get("design_ref"))
log.debug("expand_instance_terms for path_key=%r design_ref=%s", path_key, design_ref)
design = u.getSNLDesign(design_ref) if design_ref else None
if not design:
log.warning("expand_instance_terms: design not found for %s", design_ref)
terms = []

if design:
for term in design.getTerms():
if isinstance(term, naja.SNLBusTerm):
lo, hi = sorted((term.getLSB(), term.getMSB()))
for b in range(lo, hi + 1):
bit_term = term.getBusTermBit(b)
if bit_term:
terms.append({
"name": f"{term.getName()}[{b}]",
"child_id": bit_term.getID(),
"direction": direction_to_int(bit_term.getDirection()),
"bit": b,
})
else:
terms.append({
"name": term.getName(),
"child_id": term.getID(),
"direction": direction_to_int(term.getDirection()),
})

return [{
"response": "expanded_instance_terms",
"path_key": path_key,
"terms": terms
"terms": bit_terms_json(design) if design else []
}]


def _handle_resolve_instance(u, request):
# Everything the viewer needs to show one instance, given only its
# instance-name path (the same convention as get_properties and
# diagnosis items): the child_id/model of each path level, to reveal it
# in the tree and address its pins, plus its full pin list, to draw it
# alone in the schematic as a starting point.
path = [str(name) for name in request.get("path", [])]
top = u.getTopDesign()
design, instance = resolve_instance_path(top, path) if top else (None, None)
reply = {"response": "instance_resolved", "path": path,
"found": top is not None and (instance is not None or not path)}
if instance is None:
if top is not None and path:
log.warning("resolve_instance: could not resolve instance path %s", path)
return [reply]
levels = []
current = top
for name in path:
inst = current.getInstance(name)
levels.append([inst.getName(), inst.getID(), inst.getModel().getName()])
current = inst.getModel()
reply["instance"] = {
"path": levels,
"design_ref": {
"db_id": design.getDB().getID(),
"library_id": design.getLibrary().getID(),
"design_id": design.getID(),
},
"has_instances": (design.hasNonPrimitiveInstances() or
has_visible_primitive_instances(design)),
"source_loc": get_source_loc(instance),
"terms": bit_terms_json(design),
}
return [reply]


def _handle_instance_selected(u, request):
# A notification, not a request: the viewer reports the instance the
# user selected. The notebook widget intercepts it (Schematic.selected);
# elsewhere it's only logged.
log.info("Viewer selected instance: %s", "/".join(request.get("path") or []) or "<top>")
return []


def _handle_load_instance_internals(u, request):
path_key = request.get("path_key", "")
design_ref = get_design_ref(request.get("design_ref"))
Expand Down Expand Up @@ -670,6 +717,8 @@ def _handle_get_properties(u, request):
"load_instance_internals": _handle_load_instance_internals,
"load_source": _handle_load_source,
"get_properties": _handle_get_properties,
"resolve_instance": _handle_resolve_instance,
"instance_selected": _handle_instance_selected,
}


Expand Down Expand Up @@ -709,3 +758,10 @@ def diagnosis_response(items):
if isinstance(items, dict):
items = items.get("items", [])
return {"response": "diagnosis_response", "items": list(items)}


def focus_instance(path):
"""Build a focus_instance push message: the viewer resolves `path` (a
list of instance names, top excluded; [] = the top design), reveals and
selects it in the tree, and draws it alone in the schematic."""
return {"response": "focus_instance", "path": [str(name) for name in path]}
Loading
Loading