Skip to content

Commit 2799c8d

Browse files
authored
refactor: standardize canvas transformations across layers (#69)
1 parent 8139e37 commit 2799c8d

File tree

16 files changed

+245
-128
lines changed

16 files changed

+245
-128
lines changed

.cursor/rules/event-model-rules.mdc

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,25 @@ The system uses **`CustomEvent`**, and interaction follows a `.on`/`.off`/`.emit
8181
- **Use `event.detail`:** Access event-specific data here.
8282
- **Check `event.detail.target`:** Identify the specific component involved.
8383
- **Emit Custom Events:** Use `graph.emit(...)` to broadcast custom global events.
84-
- **Performance:** Avoid heavy logic in listeners.
84+
- **Performance:** Avoid heavy logic in listeners.
85+
86+
## Connection Creation Events (`ConnectionLayer.ts`)
87+
These events are fired by the `ConnectionLayer` during the interactive creation of connections.
88+
89+
- **`connection-create-start`**: Fires when the user starts dragging a connection from a block or anchor.
90+
- **`event.detail`**: `{ blockId: TBlockId; anchorId: string | undefined; }`
91+
- **Preventable:** Yes. Preventing this stops the connection creation process.
92+
93+
- **`connection-create-hover`**: Fires when the dragged connection endpoint hovers over a potential target block or anchor.
94+
- **`event.detail`**: `{ sourceBlockId: TBlockId; sourceAnchorId: string | undefined; targetBlockId: TBlockId | undefined; targetAnchorId: string | undefined; }`
95+
- **Preventable:** Yes. Preventing this prevents the connection from being made to the current target (hover state might not show).
96+
97+
- **`connection-created`**: Fires when a connection is successfully created (user drops the endpoint onto a valid target).
98+
- **`event.detail`**: `{ sourceBlockId: TBlockId; sourceAnchorId?: string; targetBlockId: TBlockId; targetAnchorId?: string; }`
99+
- **Preventable:** Yes. Preventing this stops the connection from being added to the `connectionsList` store (default action).
100+
101+
- **`connection-create-drop`**: Fires when the user drops the connection endpoint, regardless of whether it landed on a valid target.
102+
- **`event.detail`**: `{ sourceBlockId: TBlockId; sourceAnchorId: string | undefined; targetBlockId?: TBlockId; targetAnchorId?: string; point: Point; }` (target details are present if dropped on a valid target).
103+
- **Preventable:** No (typically used for cleanup or alternative actions on drop).
104+
105+
-------

.cursor/rules/layer-rules.mdc

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ Layers are fundamental to the Canvas rendering pipeline in @gravity-ui/graph. Th
1010

1111
## Key Layer Concepts
1212
- **Purpose:** Each layer is responsible for a specific aspect of the graph, which can include **rendering visual elements** (e.g., `BackgroundLayer`, `ConnectionLayer`) or **managing behavior and logic** (e.g., handling specific user interactions, managing non-visual state). A layer might not have a direct visual representation but still participate in the graph's lifecycle and logic.
13+
- **Base Class:** `src/services/Layer.ts` (Exports `Layer`, `LayerProps`, `LayerContext`).
14+
- **`LayerProps` Interface:** Defines configuration for a layer. Key properties include:
15+
- `canvas`: Configuration for the HTML5 Canvas element (optional).
16+
- `zIndex`: Stacking order.
17+
- `classNames`: CSS classes.
18+
- `respectPixelRatio`: (boolean, default: true) Whether the canvas should account for device pixel ratio for sharper rendering.
19+
- `transformByCameraPosition`: (boolean, default: false) Whether the canvas transform should automatically follow the camera's position and scale.
20+
- `html`: Configuration for the HTML overlay element (optional).
21+
- `zIndex`: Stacking order.
22+
- `classNames`: CSS classes.
23+
- `transformByCameraPosition`: (boolean, default: false) Whether the HTML element's transform should automatically follow the camera's position and scale via CSS `matrix()`.
24+
- `camera`: The `ICamera` instance.
25+
- `graph`: The main `Graph` instance.
26+
- `root`: The root HTML element where the layer will be attached.
27+
- **`LayerContext` Interface:** Provides context information accessible within the layer (via `this.context`). Includes:
28+
- `graph`, `camera`, `constants`, `colors`, `graphCanvas`, `ctx`, `layer` (self-reference).
1329
- **Location:** Core layer logic and specific layer implementations are found in `src/components/canvas/layers/` and potentially base classes/services like `src/services/Layer.ts`.
1430
- **Rendering Order/Priority:** Layers are rendered/processed in a specific sequence determined by the main graph component. The order is crucial for visual correctness and logical flow (e.g., connections below blocks, interaction handlers processed before rendering). Typical order might be: Background -> Connections -> Blocks -> Groups -> Behavior/Interaction Layers -> Highlight Layers.
1531
- **Performance:** Layers optimize rendering and processing by:
@@ -25,7 +41,7 @@ Layers are fundamental to the Canvas rendering pipeline in @gravity-ui/graph. Th
2541
- The base `Layer` class has the following generic signature: `Layer<Props extends LayerProps, Context extends LayerContext, State extends TComponentState>`.
2642
- `LayerProps`, `LayerContext`, and `TComponentState` are defined in `src/services/Layer.ts` and `src/lib/Component.ts` respectively.
2743
- When defining your custom layer, specify your custom Props, Context (if needed, extending `LayerContext`), and State (if needed, extending `TComponentState`) in this order: `class MyLayer extends Layer<TMyLayerProps, TMyLayerContext, TMyLayerState> {...}`.
28-
- Your custom `TMyLayerProps` interface **must extend the base `LayerProps`**. This is because the base constructor requires properties like `graph` and `camera`.
44+
- Your custom `TMyLayerProps` interface **must extend the base `LayerProps`**. This is because the base constructor requires properties like `graph` and `camera`. Make sure to define custom props alongside the base ones.
2945

3046
- **Constructor and Props:**
3147
- The constructor of your custom layer will typically receive the *full* `LayerProps` (or your extended `TMyLayerProps`).
@@ -53,18 +69,19 @@ Layers are fundamental to the Canvas rendering pipeline in @gravity-ui/graph. Th
5369
- **Modifying Existing Layers:**
5470
- When changing how elements are rendered or behaviors are managed, modify the relevant methods (e.g., `render`, event handlers) of the corresponding layer.
5571
- Ensure efficiency; avoid unnecessary computations or drawing invisible elements.
56-
- **Handling Device Pixel Ratio (DPR):** For crisp rendering on HiDPI displays, ensure the layer respects DPR.
57-
- **Recommended:** Set `respectPixelRatio: true` in the `canvas` options when initializing the layer via `super({ canvas: { respectPixelRatio: true }, ... })`. The base `Layer` class will then attempt to handle canvas sizing automatically.
58-
- **Manual (if needed):** If automatic handling isn't sufficient or more control is required:
59-
1. Get the DPR from context: `const dpr = this.context.constants.system.PIXEL_RATIO;`
60-
2. In your layer's `render` method, reset the transform: `ctx.setTransform(1, 0, 0, 1, 0, 0);`
61-
3. Clear the correct area: `ctx.clearRect(0, 0, viewWidth * dpr, viewHeight * dpr);` (where `viewWidth`/`viewHeight` are CSS dimensions from camera state).
62-
4. Scale the context *before* drawing: `ctx.scale(dpr, dpr);`
72+
- **Handling Device Pixel Ratio (DPR) and Transforms:** For crisp rendering and correct positioning, especially when using Canvas:
73+
- **`respectPixelRatio`:** Set this to `true` (default) in `LayerProps.canvas` to enable automatic DPR handling by the base `Layer` class. The canvas dimensions will be scaled, and transforms applied correctly.
74+
- **`transformByCameraPosition`:** Set this to `true` in `LayerProps.canvas` or `LayerProps.html` if the layer's content should move/scale with the camera automatically.
75+
- **Manual Transforms:**
76+
- Use `this.resetTransform()` at the beginning of your `render` method. This clears the canvas and applies the base camera transform (if `transformByCameraPosition` is true for canvas) respecting the DPR.
77+
- Use `this.applyTransform(x, y, scale, respectPixelRatio)` to apply additional transformations relative to the camera or world space, ensuring DPR is accounted for.
78+
- Use `this.getDRP()` to get the calculated Device Pixel Ratio (respecting `respectPixelRatio` flag).
79+
- **Coordinate Systems:** Remember that `this.context.ctx` transformations managed by `resetTransform` and `applyTransform` handle the conversion from world space to the scaled canvas coordinate space. Draw using world coordinates.
6380
- **Layer Lifecycle & Context:**
6481
- Use `afterInit` for setup that requires the canvas and context to be ready.
6582
- Inside `afterInit`, reliably get the canvas using `this.getCanvas()`.
66-
- Initialize or update the layer's full context using `this.setContext({ canvas, ctx: canvas.getContext('2d'), camera: ..., constants: ..., colors: ..., graph: ..., ...this.context });`. This ensures the context object (`this.context`) is correctly typed and populated for your layer.
67-
- Attach event listeners (e.g., `mousemove`, `mouseleave`) to the graph's root element (`this.props.graph.layers?.$root`) within `afterInit`.
83+
- Initialize or update the layer's full context using `this.setContext({ ...this.context, /* your additions */ });`. The base `Layer` already initializes `graph`, `camera`, `colors`, `constants`, `layer`. If you create a canvas/HTML element, you need to add `graphCanvas`/`html` and `ctx` to the context after creation (e.g., in `afterInit`).
84+
- Attach event listeners (e.g., `mousemove`, `mouseleave`) to the graph's root element (`this.props.graph.getGraphHTML()`) or specific layer elements (`this.getCanvas()`, `this.getHTML()`) within `afterInit`.
6885
- Always clean up listeners and subscriptions in the `unmount` method.
6986
- **Camera Interaction & Coordinates:**
7087
- Subscribe to graph's `'camera-change'` event (`this.props.graph.on(...)`) to get updates. The event detail (`event.detail`) provides the `TCameraState` (containing `width`, `height`, `scale`, `x`, `y`).

.cursor/rules/project-rules.mdc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Rendering system:
6262
- Full graph is rendered on Canvas for performance
6363
- When zooming in, HTML/React mode is automatically enabled for interactive elements
6464
- Uses a smart system that tracks visible blocks and renders only them in React
65+
- Rendering is organized into Layers (`src/services/Layer.ts`) which control drawing order and can optimize rendering based on camera position and device pixel ratio (`respectPixelRatio`, `transformByCameraPosition` properties in `LayerProps`).
6566

6667
## Development Guidelines
6768
Development guidelines:

docs/rendering/layers.md

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -70,59 +70,140 @@ const newBlockLayer = graph.addLayer(NewBlockLayer, { ghostBackground: "rgba(0,
7070

7171
### Basic Structure
7272

73-
Each layer can have its own canvas and/or HTML elements for rendering and interaction:
73+
Custom layers should extend the base `Layer` class (`src/services/Layer.ts`). This class provides the core functionality for managing Canvas and HTML elements within the graph's rendering pipeline.
7474

7575
```typescript
76-
import { Layer, LayerContext, LayerProps } from "@/services/Layer";
77-
import { CameraService } from "@/services/camera/CameraService";
76+
import { Layer, LayerContext, LayerProps, TGraphColors, TGraphConstants } from "@/services/Layer";
77+
import { ICamera } from "@/services/camera/CameraService";
7878
import { Graph } from "@/graph";
7979

80+
// Define custom properties for your layer, extending the base LayerProps
8081
interface MyLayerProps extends LayerProps {
8182
customOption?: string;
83+
gridColor?: string;
84+
}
85+
86+
// Optionally extend the LayerContext if your layer needs additional context
87+
interface MyLayerContext extends LayerContext {
88+
// Add specific context properties here
8289
}
8390

84-
export class MyLayer extends Layer<MyLayerProps> {
91+
// Define the Layer, specifying Props and optionally Context
92+
export class MyLayer extends Layer<MyLayerProps, MyLayerContext> {
8593
constructor(props: MyLayerProps) {
94+
// Call the base constructor, passing props and potentially overriding default layer settings
8695
super({
87-
// Canvas element for drawing
96+
// Canvas element configuration (optional)
8897
canvas: {
8998
zIndex: 10,
90-
classNames: ["my-layer"]
99+
classNames: ["my-layer-canvas"],
100+
respectPixelRatio: true, // Default: true. Set to false to disable DPR scaling.
101+
transformByCameraPosition: true // Default: false. Set to true to automatically apply camera transform.
91102
},
92-
// HTML element for DOM-based interactions
103+
// HTML element configuration (optional)
93104
html: {
94-
zIndex: 10,
105+
zIndex: 11,
95106
classNames: ["my-layer-html"],
96-
transformByCameraPosition: true
107+
transformByCameraPosition: true // Default: false. Set to true to apply camera transform via CSS.
97108
},
98-
...props
109+
...props // Pass remaining props, including required `graph` and `camera`
99110
});
100-
111+
112+
// --- Context Setup ---
113+
// Base context (graph, camera, constants, colors, layer) is set by the super constructor.
114+
// You need to add context specific to the elements created by this layer.
115+
// It's recommended to do this in `afterInit` when elements are guaranteed to exist.
116+
}
117+
118+
// `afterInit` is called after the layer's elements (canvas/html) are created and attached.
119+
protected afterInit() {
120+
// Get the created elements (if configured)
121+
const canvas = this.getCanvas(); // Returns HTMLCanvasElement or undefined
122+
const html = this.getHTML(); // Returns HTMLElement or undefined
123+
124+
// Set up the context, including the canvas context if needed
101125
this.setContext({
102-
canvas: this.getCanvas(),
103-
ctx: this.getCanvas().getContext("2d"),
104-
camera: props.camera,
105-
graph: this.props.graph
126+
// Keep existing context properties
127+
...this.context,
128+
// Add properties specific to this layer's setup
129+
graphCanvas: canvas, // Use the correct property name
130+
ctx: canvas?.getContext("2d") || undefined,
131+
// Add any custom context fields defined in MyLayerContext
106132
});
107-
108-
// Subscribe to events
109-
this.context.graph.on("camera-change", this.performRender);
133+
134+
// Now the full context (this.context) is available and typed.
135+
// Subscribe to events here
136+
this.cameraSubscription = this.context.graph.on("camera-change", this.performRender);
137+
// Add other event listeners (e.g., to this.getHTML() or this.getCanvas())
138+
139+
// Perform initial render
140+
this.performRender();
110141
}
111-
112-
// Rendering method
142+
143+
// --- Rendering ---
144+
// This method is called automatically when performRender is invoked (e.g., on camera change).
113145
protected render() {
114-
const { ctx } = this.context;
115-
ctx.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
116-
// Custom rendering code
146+
// Check if context is fully initialized
147+
if (!this.context.ctx || !this.context.graphCanvas) {
148+
return;
149+
}
150+
151+
// Reset transformations and clear canvas
152+
this.resetTransform(); // Applies camera transform if transformByCameraPosition=true, respects DPR
153+
154+
const { ctx, camera, constants, colors } = this.context;
155+
const { scale, x, y } = camera.getCameraState();
156+
157+
// --- Custom Rendering Logic ---
158+
// Draw using world coordinates. `resetTransform` handles the necessary scaling and translation.
159+
ctx.fillStyle = this.props.gridColor || colors.canvas.gridColor; // Example using props/colors
160+
ctx.fillRect(10, 10, 50, 50); // Draws a rectangle at world coordinates (10, 10)
161+
162+
// Example: Apply additional transform relative to the camera
163+
// this.applyTransform(worldX, worldY, worldScale); // Respects DPR
117164
}
118-
119-
// Resource cleanup
165+
166+
// --- Cleanup ---
167+
// Called when the layer is removed from the graph.
120168
protected unmount(): void {
121-
this.context.graph.off("camera-change", this.performRender);
169+
// Always clean up subscriptions and event listeners
170+
this.cameraSubscription?.();
171+
// Remove other listeners added in `afterInit`
172+
super.unmount(); // Calls base unmount logic (removes elements)
122173
}
123174
}
124175
```
125176

177+
### Handling Transformations and Device Pixel Ratio (DPR)
178+
179+
The `Layer` class provides tools to handle rendering correctly with camera transformations and on high-resolution (HiDPI) displays.
180+
181+
- **`respectPixelRatio`** (`LayerProps.canvas.respectPixelRatio`, default: `true`):
182+
- When `true`, the canvas dimensions are automatically scaled by the device pixel ratio (`window.devicePixelRatio`).
183+
- The `resetTransform()` and `applyTransform()` methods automatically account for this scaling, so you can draw using world coordinates without manual DPR calculation.
184+
- Set to `false` only if you need explicit manual control over pixel scaling.
185+
186+
- **`transformByCameraPosition`** (`LayerProps.canvas.transformByCameraPosition` / `LayerProps.html.transformByCameraPosition`, default: `false`):
187+
- **For Canvas:** When `true`, the `resetTransform()` method automatically applies the current camera translation (`x`, `y`) and `scale` to the canvas context (`ctx`). This means your subsequent drawing operations in the `render` method should use world coordinates.
188+
- **For HTML:** When `true`, the HTML element gets the `layer-with-camera` class, and its CSS `transform: matrix(...)` is automatically updated on camera changes to match the camera's position and scale.
189+
190+
- **`resetTransform()`**:
191+
- Call this at the beginning of your canvas `render` method.
192+
- It resets the canvas context's transform to the identity matrix.
193+
- It clears the entire canvas (`clearRect`).
194+
- If `transformByCameraPosition` is `true` for the canvas, it applies the current camera transform (`scale`, `x`, `y`), automatically accounting for DPR if `respectPixelRatio` is `true`.
195+
196+
- **`applyTransform(x, y, scale, respectPixelRatioOverride)`**:
197+
- Applies an *additional* transformation to the canvas context, relative to the current transform (which includes the camera transform if set by `resetTransform`).
198+
- Useful for drawing elements at specific world positions/scales relative to the base camera view.
199+
- It automatically accounts for DPR based on the layer's `respectPixelRatio` setting unless overridden by the optional fourth argument.
200+
201+
- **`getDRP()`**:
202+
- Returns the device pixel ratio value that the layer is currently using (respecting the `respectPixelRatio` flag).
203+
- Use this only if you need the DPR value for complex manual calculations, usually unnecessary when using `resetTransform` and `applyTransform`.
204+
205+
**In summary:** For most canvas layers, set `respectPixelRatio: true` and `transformByCameraPosition: true`, call `resetTransform()` at the start of `render`, and draw using world coordinates. Use `applyTransform` for specific relative positioning if needed.
206+
126207
### Built-in CSS Classes
127208

128209
The library provides several built-in CSS classes that can be used with layers:

src/components/canvas/blocks/Block.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ export class Block<T extends TBlock = TBlock, Props extends TBlockProps = TBlock
376376
return this.getConnectionAnchorPosition(anchor);
377377
};
378378

379-
protected updateChildren(): void | object[] {
379+
protected updateChildren() {
380380
if (!this.isAnchorsAllowed()) {
381381
return undefined;
382382
}

src/components/canvas/connections/BlockConnections.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class BlockConnections extends Component<CoreComponentProps, TComponentSt
5353
this.unsubscribe.forEach((reactionDisposer) => reactionDisposer());
5454
}
5555

56-
protected updateChildren(): void | object[] {
56+
protected updateChildren() {
5757
if (!this.connections) return [];
5858
const settings = this.context.graph.rootStore.settings.$connectionsSettings.value;
5959
const ConnectionCtop = this.context.graph.rootStore.settings.$connection.value || BlockConnection;

0 commit comments

Comments
 (0)