mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
Added three new experimental views
This commit is contained in:
373
dev-docs/reference/CANVAS-OVERLAY-ARCHITECTURE.md
Normal file
373
dev-docs/reference/CANVAS-OVERLAY-ARCHITECTURE.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# Canvas Overlay Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains how canvas overlays integrate with the NodeGraphEditor and the editor's data flow.
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. NodeGraphEditor Initialization
|
||||
|
||||
The overlay is created when the NodeGraphEditor is constructed:
|
||||
|
||||
```typescript
|
||||
// In nodegrapheditor.ts constructor
|
||||
export default class NodeGraphEditor {
|
||||
commentLayer: CommentLayer;
|
||||
|
||||
constructor(domElement, options) {
|
||||
// ... canvas setup
|
||||
|
||||
// Create overlay
|
||||
this.commentLayer = new CommentLayer(this);
|
||||
this.commentLayer.setReadOnly(this.readOnly);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. DOM Structure
|
||||
|
||||
The overlay requires two divs in the DOM hierarchy:
|
||||
|
||||
```html
|
||||
<div id="nodegraph-editor">
|
||||
<canvas id="nodegraph-canvas"></canvas>
|
||||
<div id="nodegraph-background-layer"></div>
|
||||
<!-- Behind canvas -->
|
||||
<div id="nodegraph-dom-layer"></div>
|
||||
<!-- In front of canvas -->
|
||||
</div>
|
||||
```
|
||||
|
||||
CSS z-index layering:
|
||||
|
||||
- Background layer: `z-index: 0`
|
||||
- Canvas: `z-index: 1`
|
||||
- Foreground layer: `z-index: 2`
|
||||
|
||||
### 3. Render Target Setup
|
||||
|
||||
The overlay attaches to the DOM layers:
|
||||
|
||||
```typescript
|
||||
// In nodegrapheditor.ts
|
||||
const backgroundDiv = this.el.find('#nodegraph-background-layer').get(0);
|
||||
const foregroundDiv = this.el.find('#nodegraph-dom-layer').get(0);
|
||||
|
||||
this.commentLayer.renderTo(backgroundDiv, foregroundDiv);
|
||||
```
|
||||
|
||||
### 4. Viewport Synchronization
|
||||
|
||||
The overlay updates whenever the canvas pan/zoom changes:
|
||||
|
||||
```typescript
|
||||
// In nodegrapheditor.ts paint() method
|
||||
paint() {
|
||||
// ... canvas drawing
|
||||
|
||||
// Update overlay transform
|
||||
this.commentLayer.setPanAndScale({
|
||||
x: this.xOffset,
|
||||
y: this.yOffset,
|
||||
scale: this.scale
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### EventDispatcher Integration
|
||||
|
||||
Overlays typically subscribe to model changes using EventDispatcher:
|
||||
|
||||
```typescript
|
||||
class MyOverlay {
|
||||
setComponentModel(model: ComponentModel) {
|
||||
if (this.model) {
|
||||
this.model.off(this); // Clean up old subscriptions
|
||||
}
|
||||
|
||||
this.model = model;
|
||||
|
||||
// Subscribe to changes
|
||||
model.on('nodeAdded', this.onNodeAdded.bind(this), this);
|
||||
model.on('nodeRemoved', this.onNodeRemoved.bind(this), this);
|
||||
model.on('connectionChanged', this.onConnectionChanged.bind(this), this);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
onNodeAdded(node) {
|
||||
// Update overlay state
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Typical Data Flow
|
||||
|
||||
```
|
||||
User Action
|
||||
↓
|
||||
Model Change (ProjectModel/ComponentModel)
|
||||
↓
|
||||
EventDispatcher fires event
|
||||
↓
|
||||
Overlay handler receives event
|
||||
↓
|
||||
Overlay updates React state
|
||||
↓
|
||||
React re-renders overlay
|
||||
```
|
||||
|
||||
## Lifecycle Management
|
||||
|
||||
### Creation
|
||||
|
||||
```typescript
|
||||
constructor(nodegraphEditor: NodeGraphEditor) {
|
||||
this.nodegraphEditor = nodegraphEditor;
|
||||
this.props = { /* initial state */ };
|
||||
}
|
||||
```
|
||||
|
||||
### Attachment
|
||||
|
||||
```typescript
|
||||
renderTo(backgroundDiv: HTMLDivElement, foregroundDiv: HTMLDivElement) {
|
||||
this.backgroundDiv = backgroundDiv;
|
||||
this.foregroundDiv = foregroundDiv;
|
||||
|
||||
// Create React roots
|
||||
this.backgroundRoot = createRoot(backgroundDiv);
|
||||
this.foregroundRoot = createRoot(foregroundDiv);
|
||||
|
||||
// Initial render
|
||||
this._renderReact();
|
||||
}
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
```typescript
|
||||
setPanAndScale(viewport: Viewport) {
|
||||
// Update CSS transform
|
||||
const transform = `scale(${viewport.scale}) translate(${viewport.x}px, ${viewport.y}px)`;
|
||||
this.backgroundDiv.style.transform = transform;
|
||||
this.foregroundDiv.style.transform = transform;
|
||||
|
||||
// Notify React if scale changed (important for react-rnd)
|
||||
if (this.props.scale !== viewport.scale) {
|
||||
this.props.scale = viewport.scale;
|
||||
this._renderReact();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Disposal
|
||||
|
||||
```typescript
|
||||
dispose() {
|
||||
// Unmount React
|
||||
if (this.backgroundRoot) {
|
||||
this.backgroundRoot.unmount();
|
||||
}
|
||||
if (this.foregroundRoot) {
|
||||
this.foregroundRoot.unmount();
|
||||
}
|
||||
|
||||
// Unsubscribe from models
|
||||
if (this.model) {
|
||||
this.model.off(this);
|
||||
}
|
||||
|
||||
// Clean up DOM event listeners
|
||||
// (CommentLayer uses a clever cloneNode trick to remove all listeners)
|
||||
}
|
||||
```
|
||||
|
||||
## Component Model Integration
|
||||
|
||||
### Accessing Graph Data
|
||||
|
||||
The overlay has access to the full component graph through NodeGraphEditor:
|
||||
|
||||
```typescript
|
||||
class MyOverlay {
|
||||
getNodesInView(): NodeGraphNode[] {
|
||||
const model = this.nodegraphEditor.nodeGraphModel;
|
||||
const nodes: NodeGraphNode[] = [];
|
||||
|
||||
model.forEachNode((node) => {
|
||||
nodes.push(node);
|
||||
});
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
getConnections(): Connection[] {
|
||||
const model = this.nodegraphEditor.nodeGraphModel;
|
||||
return model.getAllConnections();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Node Position Access
|
||||
|
||||
Node positions are available through the graph model:
|
||||
|
||||
```typescript
|
||||
getNodeScreenPosition(nodeId: string): Point | null {
|
||||
const model = this.nodegraphEditor.nodeGraphModel;
|
||||
const node = model.findNodeWithId(nodeId);
|
||||
|
||||
if (!node) return null;
|
||||
|
||||
// Node positions are in canvas space
|
||||
return {
|
||||
x: node.x,
|
||||
y: node.y
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Communication with NodeGraphEditor
|
||||
|
||||
### From Overlay to Canvas
|
||||
|
||||
The overlay can trigger canvas operations:
|
||||
|
||||
```typescript
|
||||
// Clear canvas selection
|
||||
this.nodegraphEditor.clearSelection();
|
||||
|
||||
// Select nodes on canvas
|
||||
this.nodegraphEditor.selectNode(node);
|
||||
|
||||
// Trigger repaint
|
||||
this.nodegraphEditor.repaint();
|
||||
|
||||
// Navigate to node
|
||||
this.nodegraphEditor.zoomToFitNodes([node]);
|
||||
```
|
||||
|
||||
### From Canvas to Overlay
|
||||
|
||||
The canvas notifies the overlay of changes:
|
||||
|
||||
```typescript
|
||||
// In nodegrapheditor.ts
|
||||
selectNode(node) {
|
||||
// ... canvas logic
|
||||
|
||||
// Notify overlay
|
||||
this.commentLayer.clearSelection();
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Do
|
||||
|
||||
1. **Clean up subscriptions** - Always unsubscribe from EventDispatcher on dispose
|
||||
2. **Use the context object pattern** - Pass `this` as context to EventDispatcher subscriptions
|
||||
3. **Batch updates** - Group multiple state changes before calling render
|
||||
4. **Check for existence** - Always check if DOM elements exist before using them
|
||||
|
||||
### ❌ Don't
|
||||
|
||||
1. **Don't modify canvas directly** - Work through NodeGraphEditor API
|
||||
2. **Don't store duplicate data** - Reference the model as the source of truth
|
||||
3. **Don't subscribe without context** - Direct EventDispatcher subscriptions leak
|
||||
4. **Don't assume initialization order** - Check for null before accessing properties
|
||||
|
||||
## Example: Complete Overlay Setup
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
|
||||
import { ComponentModel } from '@noodl-models/componentmodel';
|
||||
|
||||
import { NodeGraphEditor } from './nodegrapheditor';
|
||||
|
||||
export default class DataLineageOverlay {
|
||||
private nodegraphEditor: NodeGraphEditor;
|
||||
private model: ComponentModel;
|
||||
private root: Root;
|
||||
private container: HTMLDivElement;
|
||||
private viewport: Viewport;
|
||||
|
||||
constructor(nodegraphEditor: NodeGraphEditor) {
|
||||
this.nodegraphEditor = nodegraphEditor;
|
||||
}
|
||||
|
||||
renderTo(container: HTMLDivElement) {
|
||||
this.container = container;
|
||||
this.root = createRoot(container);
|
||||
this.render();
|
||||
}
|
||||
|
||||
setComponentModel(model: ComponentModel) {
|
||||
if (this.model) {
|
||||
this.model.off(this);
|
||||
}
|
||||
|
||||
this.model = model;
|
||||
|
||||
if (model) {
|
||||
model.on('connectionChanged', this.onDataChanged.bind(this), this);
|
||||
model.on('nodeRemoved', this.onDataChanged.bind(this), this);
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
setPanAndScale(viewport: Viewport) {
|
||||
this.viewport = viewport;
|
||||
const transform = `scale(${viewport.scale}) translate(${viewport.x}px, ${viewport.y}px)`;
|
||||
this.container.style.transform = transform;
|
||||
}
|
||||
|
||||
private onDataChanged() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
if (!this.root) return;
|
||||
|
||||
const paths = this.calculateDataPaths();
|
||||
|
||||
this.root.render(
|
||||
<DataLineageView paths={paths} viewport={this.viewport} onPathClick={this.handlePathClick.bind(this)} />
|
||||
);
|
||||
}
|
||||
|
||||
private calculateDataPaths() {
|
||||
// Analyze graph connections to build data flow paths
|
||||
// ...
|
||||
}
|
||||
|
||||
private handlePathClick(path: DataPath) {
|
||||
// Select nodes involved in this path
|
||||
const nodeIds = path.nodes.map((n) => n.id);
|
||||
this.nodegraphEditor.selectNodes(nodeIds);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
}
|
||||
if (this.model) {
|
||||
this.model.off(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Main Overview](./CANVAS-OVERLAY-PATTERN.md)
|
||||
- [Mouse Event Handling](./CANVAS-OVERLAY-EVENTS.md)
|
||||
- [React Integration](./CANVAS-OVERLAY-REACT.md)
|
||||
Reference in New Issue
Block a user