Skip to content

Commit 6129d47

Browse files
committed
feat(doc): add documentation
1 parent 2eeff28 commit 6129d47

4 files changed

Lines changed: 260 additions & 0 deletions

File tree

doc/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Bot — Developer Documentation
2+
3+
Welcome to the **bot** developer documentation. This guide is designed to help you understand the internal architecture of the project, learn how to extend it with new mathematical models, and customize user interactions.
4+
5+
## Documentation Map
6+
7+
1. **[Architecture & IPC Flow](architecture.md)**: Understand the multi-process design (Parent vs. Child process), the IPC communication pipe, and the vital differences between Events and Commands.
8+
2. **[Connecting a New Model](custom_model.md)**: A step-by-step guide on using the IViewable protocol to bridge any custom mathematical or geometric model to the 3D viewer.
9+
3. **[Customizing Callbacks](callbacks.md)**: Learn how to intercept user interactions using custom Python callbacks without breaking the default UI behavior.
10+
11+
## Directory Layout of the Source Code
12+
13+
* `bot/core/`: The mathematical kernel. Contains core domain models (CADModel utilizing Gmsh and SplineModel utilizing Ferrispline).
14+
* `bot/viewer/`: The public API and the Anti-Corruption Layer (ACL). Includes Viewer, adapters, and serialization helpers.
15+
* `bot/view/`: Internal rendering engine layers built on top of Panda3D (Scene, CurveApp).bot/control/: Input management scripts capturing mouse, keyboard, and camera physics.

doc/architecture.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Architecture and IPC Communication
2+
3+
To keep the Python REPL (e.g., IPython) fully interactive while maintaining a fluid 60 FPS 3D rendering pipeline, **bot** implements a split-process architecture. OpenGL and windowing contexts (Panda3D) run inside a dedicated subprocess, isolated from the mathematical calculations.
4+
5+
## Process Separation
6+
7+
1. **Parent Process (Main Process):**
8+
* Hosts your Python script or interactive REPL session.
9+
* Maintains the true mathematical models (`CADModel`, `SplineModel`).
10+
* Runs a background daemon thread to continuously listen for incoming data from the UI.
11+
2. **Child Process (Subprocess):**
12+
* Runs the Panda3D application engine on its main thread (required for macOS/OpenGL stability).
13+
* Handles user input captures, camera matrices, and drawing loops.
14+
15+
```mermaid
16+
flowchart TB
17+
subgraph Parent["Parent process"]
18+
REPL["IPython REPL"]
19+
EventThread["Existing daemon event thread"]
20+
Models["CADModel / SplineModel"]
21+
Adapters["CADAdapter / SplineAdapter ACL"]
22+
Composite["CompositeViewable"]
23+
Viewer["Viewer"]
24+
REPL --> Models
25+
Models --> Adapters --> Composite --> Viewer
26+
EventThread -->|"handle_event"| Composite
27+
end
28+
29+
subgraph Child["Child process — Panda3D"]
30+
App["ViewerApp"]
31+
Scene["Scene.apply_patch"]
32+
Mouse["MouseHandler — drag unchanged"]
33+
Mouse --> App
34+
App --> Scene
35+
end
36+
37+
Viewer -->|"add / update / delete"| App
38+
App -->|"ViewEvent incl. cp_drag / cp_pick_end"| EventThread
39+
```
40+
41+
## Events vs. Commands
42+
43+
Communication across the IPC Pipe is strictly divided into two distinct paradigms based on direction and intent:
44+
45+
### 1. View Events (`ViewEventType`)
46+
* **Direction:** Child Process -> Parent Process.
47+
* **Intent:** Notification of a physical interaction performed by the user inside the 3D window (e.g., a mouse click, hovering over a curve, or releasing a dragged control point).
48+
* **Data structure:** Serializable dictionaries carrying interaction metadata (e.g., `curve_tag`, `world_pos`, `cp_index`).
49+
50+
### 2. Viewer Commands (`ViewerCommandType` & `SceneUpdateOp`)
51+
* **Direction:** Parent Process -> Child Process.
52+
* **Intent:** Imperative instructions forcing the 3D window to update its state or render new frames.
53+
* **Categories:**
54+
* **Topology Operations (`SceneUpdateOp`):** Heavyweight actions to synchronize 3D structures (`ADD`, `UPDATE`, `DELETE`).
55+
* **Display State Commands (`ViewerCommandType`):** Lightweight UI adjustments (`HIGHLIGHT_CURVE`, `UPDATE_HUD`, `SET_EDIT_MODE`).
56+
57+
### Comparative Summary
58+
59+
| Feature | View Event | Viewer Command |
60+
| :--- | :--- | :--- |
61+
| **Origin** | Subprocess User Inputs | Parent Math Kernel/Script |
62+
| **Destination** | Background Listener Thread | 3D Engine Command Queue |
63+
| **Philosophy** | "Something happened in the canvas" | "Change your pixels right now" |
64+
| **Heavy Payloads** | No (metadata coordinates only) | Yes (contains float32 byte buffers for geometry) |

doc/callbacks.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Customizing Callbacks
2+
3+
The `Viewer` provides an explicit mechanism to attach custom user routines directly to 3D interface interaction events. This enables user scripts to execute domain calculations instantly inside the main process when a user interacts with the canvas.
4+
5+
## How Callbacks Execute Under the Hood
6+
7+
When a user triggers an action in the 3D canvas, the data flows across the IPC tunnel. The background listener thread processes the incoming event using a **dual-stage execution policy**:
8+
9+
1. **User Callbacks (Interception Stage):** The listener thread checks if a customized hook is registered for this specific `ViewEventType`. If found, it executes it immediately, supplying the event metadata.
10+
2. **Default Handlers (Visual State Stage):** Next, the system *always* hands the event over to `_default_event_handler`. This ensures standard automated behaviors (like real-time hovering highlights, canvas panning math, and text HUD changes) continue working seamlessly.
11+
12+
## API Usage
13+
14+
### Registering a Callback
15+
Use `viewer.add_callback(event_type, callable)` to listen for specific actions.
16+
17+
```python
18+
import bot
19+
from bot.viewer.contracts import ViewEventType
20+
21+
# Initialize core model and viewer
22+
cad_model = bot.CADModel()
23+
viewer = bot.Viewer()
24+
viewer.connect_models(cad_model)
25+
26+
# 1. Define your custom interaction function
27+
def my_custom_pick_handler(event_data):
28+
# event_data contains strict structured dict payloads from the event type
29+
coordinates = event_data.get("world_pos")
30+
print(f"[REPL NOTICE] User clicked absolute space position: {coordinates}")
31+
32+
# Example action: Modify the mathematical model live based on click location
33+
if coordinates:
34+
cad_model.add_point(coordinates)
35+
36+
# 2. Wire the handler to the PICK event action type
37+
viewer.add_callback(ViewEventType.PICK, my_custom_pick_handler)
38+
39+
viewer.run()
40+
```
41+
42+
## Unregistering a Callback
43+
To clear out a previously set behavior and fall back purely on standard handling loops, use `remove_callback(event_type)`:
44+
45+
```python
46+
viewer.remove_callback(ViewEventType.PICK)
47+
```
48+
49+
## Key Interactive Event Reference
50+
51+
The following table details the most common events you can attach callbacks to:
52+
53+
| Event Type Enum (`ViewEventType`) | Payload Content Context | Common Use Case |
54+
| :--- | :--- | :--- |
55+
| `HOVER` | `{"tag": str \| None}` | Trigger external UI tooltips or database queries on elements. |
56+
| `CURVE_SELECTED` | `{"curve_tag": str}` | Open property option cards or focus cameras on selection. |
57+
| `CP_PICK_START` | `{"curve_tag": str, "cp_index": int, "world_pos": [...]}` | Freeze global history undo stacks or record structural pre-states. |
58+
| `CP_PICK_END` | `{"curve_tag": str, "cp_index": int, "world_pos": [...]}` | Commit the finalized drag movement to the mathematical model kernel. |
59+
| `PICK` | `{"world_pos": [x, y, z]}` | Instantiate new custom points, nodes, or primitives at absolute locations. |

doc/custom_model.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Connecting a New Model
2+
3+
The `Viewer` is decoupled from concrete business classes using an Anti-Corruption Layer (ACL) pattern. To connect a new data model, you must wrap it in an adapter class that adheres to the `IViewable` protocol.
4+
5+
## The IViewable Protocol
6+
7+
Every adapter must implement the following 4 methods:
8+
1. `bind_update(callback)`: Registers a callback that triggers whenever the underlying model is modified.
9+
2. `unbind_update()`: Clears the registered callback.
10+
3. `get_delta_load()`: Packs the entire initial state of the model into an `ADD` operation payload.
11+
4. `handle_event(event)`: Translates inbound `ViewEvent` user actions into a list of executable `ViewerCommand` payloads.
12+
13+
## Step-by-Step Implementation Example
14+
15+
Let's write a fully compliant adapter for a hypothetical custom polygonal model component (`CustomPolylineModel`).
16+
17+
### 1. Build the Adapter Class
18+
19+
```python
20+
import logging
21+
from bot.viewer.viewable import IViewable
22+
from bot.viewer.contracts import ScenePayload, SceneUpdateOp, ViewEventType, ViewerCommandType, ViewerCommand, ViewEvent
23+
from bot.viewer.serialize import pack_curve_delta
24+
from bot.viewer.tags import encode, decode, is_namespaced
25+
26+
_logger = logging.getLogger(__name__)
27+
28+
class CustomPolylineAdapter(IViewable):
29+
NAMESPACE = "polyline"
30+
31+
def __init__(self, model):
32+
self._model = model
33+
self._update_callback = None
34+
self._last_hovered = None
35+
# Connect to your model's observer pattern
36+
self._model.add_observer(self)
37+
38+
def bind_update(self, callback):
39+
self._update_callback = callback
40+
41+
def unbind_update(self):
42+
self._update_callback = None
43+
44+
def get_delta_load(self):
45+
"""Constructs the initial full scene payload."""
46+
return {
47+
"op": SceneUpdateOp.ADD,
48+
"changed_curves": self._build_render_deltas(),
49+
"bounds": {
50+
"min": [-10, -10, -10],
51+
"max": [10, 10, 10],
52+
"center": [0, 0, 0],
53+
"size": [20, 20, 20]
54+
}
55+
}
56+
57+
def handle_event(self, event):
58+
"""Processes user clicks or selections on this specific model namespace."""
59+
commands = []
60+
event_type = event.get("event_type")
61+
tag = event.get("curve_tag") or event.get("tag")
62+
63+
# Validate that the tag belongs to this adapter's namespace
64+
if tag and is_namespaced(str(tag)):
65+
ns, local_id = decode(str(tag))
66+
if ns != self.NAMESPACE:
67+
return []
68+
else:
69+
return []
70+
71+
if event_type == ViewEventType.CURVE_SELECTED:
72+
commands.append({
73+
"cmd": ViewerCommandType.UPDATE_HUD,
74+
"text": f"Selected Polyline Local ID: {local_id}"
75+
})
76+
commands.append({
77+
"cmd": ViewerCommandType.HIGHLIGHT_CURVE,
78+
"tag": str(tag),
79+
"color": [0, 0.8, 1, 1] # Highlight blue
80+
})
81+
return commands
82+
83+
def update(self, model):
84+
"""Triggered automatically when the domain model emits a change notice."""
85+
if self._update_callback is not None:
86+
payload = {
87+
"op": SceneUpdateOp.UPDATE,
88+
"changed_curves": self._build_render_deltas()
89+
}
90+
self._update_callback(payload)
91+
92+
def _build_render_deltas(self):
93+
"""Converts internal raw coordinates to flat float32 byte payloads."""
94+
deltas = {}
95+
for item_id, polyline in self._model.get_all_items().items():
96+
# Generate a namespaced tag boundary (e.g., 'polyline:1')
97+
ns_tag = encode(self.NAMESPACE, item_id)
98+
99+
# pack_curve_delta converts vertex arrays to structural bytes
100+
deltas[ns_tag] = pack_curve_delta(
101+
curve_points=polyline.vertices, # list of [x, y, z]
102+
edges=polyline.edges, # list of (idx_a, idx_b)
103+
curve_type="linear"
104+
)
105+
return deltas
106+
```
107+
108+
### 2. Connect and Execute
109+
To initialize and bind your new adapter configuration directly via the Viewer:
110+
111+
112+
```python
113+
from bot.viewer.viewer import Viewer
114+
# Assuming your models exist:
115+
my_model = CustomPolylineModel()
116+
117+
viewer = Viewer()
118+
# Inject the custom adapter into the private interface adapter slot
119+
viewer._connect(CustomPolylineAdapter(my_model))
120+
viewer.run()
121+
```
122+

0 commit comments

Comments
 (0)