Skip to content
Draft
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
89 changes: 85 additions & 4 deletions src/components/canvas/blocks/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { Anchor, TAnchor } from "../anchors";
import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";

import { BlockMinimalistic, TBlockMinimalisticParams } from "./BlockMinimalistic";
import { BlockController } from "./controllers/BlockController";

export type TBlockSettings = {
Expand Down Expand Up @@ -127,10 +128,21 @@

public $viewState = signal<BlockViewState>({ zIndex: 0, order: 0 });

protected minimalisticFastView: BlockMinimalistic;

constructor(props: Props, parent: Component) {
super(props, parent);

this.minimalisticFastView = new BlockMinimalistic(
() => this.getGeometry(),
() => !this.hidden && this.shouldRender,
this.getMinimalisticParams()
);

this.subscribe(props.id);

// Register in batch immediately - will be used only on minimalistic scale
this.registerInBatch();
}

public getEntityId() {
Expand Down Expand Up @@ -169,6 +181,63 @@
};
}

protected getMinimalisticParams(): TBlockMinimalisticParams {
return {
fill: this.context.colors.block.background,
border: {
radius: 0,
color: this.state.selected ? this.context.colors.block.selectedBorder : this.context.colors.block.border,
width: clamp(
this.context.camera.getRelative(this.context.constants.block.BORDER_WIDTH),
this.context.constants.block.BORDER_WIDTH,
10
),
},
};
}

protected updateMinimalisticParams(): void {
this.minimalisticFastView.setParams(this.getMinimalisticParams());
}

protected getBatchGroup(): string {
return this.state.selected ? "selected" : "default";
}

protected registerInBatch(): void {
const batch = this.context.blockBatch;
if (batch) {
batch.add(this.minimalisticFastView, {
zIndex: this.calcZIndex(),
group: this.getBatchGroup(),
});
}
}

protected unregisterFromBatch(): void {
const batch = this.context.blockBatch;
if (batch) {
batch.delete(this.minimalisticFastView);
}
}

protected updateInBatch(): void {
const batch = this.context.blockBatch;
if (batch) {
batch.update(this.minimalisticFastView, {
zIndex: this.calcZIndex(),
group: this.getBatchGroup(),
});
}
}

protected markBatchDirty(): void {
const batch = this.context.blockBatch;
if (batch) {
batch.markDirty(this.minimalisticFastView);
}
}

public getConfigFlag<K extends keyof TGraphSettingsConfig>(flagPath: K) {
return this.context.graph.rootStore.settings.getConfigFlag(flagPath);
}
Expand Down Expand Up @@ -232,6 +301,12 @@
protected stateChanged(nextState: T): void {
if (!this.firstRender && nextState.selected !== this.state.selected) {
this.raiseBlock();
// Update batch group if selection changed
this.updateMinimalisticParams();
this.updateInBatch();
} else {
// Geometry or other state changed, mark as dirty
this.markBatchDirty();
}
return super.stateChanged(nextState);
}
Expand Down Expand Up @@ -518,11 +593,14 @@

const scaleLevel = this.context.graph.cameraService.getCameraBlockScaleLevel();

// Skip individual rendering for minimalistic scale
// Blocks component will render all blocks via batch
if (scaleLevel === ECameraScaleLevel.Minimalistic) {
console.log("render minimalistic block", this.state.name);

Check warning on line 599 in src/components/canvas/blocks/Block.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
return;
}

switch (scaleLevel) {
case ECameraScaleLevel.Minimalistic: {
this.renderMinimalisticBlock(this.context.ctx);
break;
}
case ECameraScaleLevel.Schematic: {
this.renderSchematicView(this.context.ctx);
break;
Expand All @@ -535,6 +613,9 @@
}

protected override unmount(): void {
// Unregister from batch
this.unregisterFromBatch();

// Release ownership of all ports owned by this block
const connectionsList = this.context.graph.rootStore.connectionsList;

Expand Down
98 changes: 98 additions & 0 deletions src/components/canvas/blocks/BlockMinimalistic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { TRect } from "../../../utils/types/shapes";
import { Path2DRenderInstance, Path2DRenderStyleResult } from "../connections/BatchPath2D";

export type TBlockMinimalisticParams = {
fill: string;
border: {
radius: number;
color: string;
width: number;
};
};

/**
* Minimalistic block representation for batch rendering using Path2D.
* This class wraps a Block and provides a simplified rendering interface
* optimized for performance at low zoom levels.
*/
export class BlockMinimalistic implements Path2DRenderInstance {
protected path: Path2D | null = null;
protected lastGeometry: TRect | null = null;

constructor(
protected getGeometry: () => TRect,
protected getVisible: () => boolean,
protected params: TBlockMinimalisticParams
) {}

public getPath(): Path2D | undefined | null {
const geometry = this.getGeometry();

// Rebuild path if geometry changed
if (
!this.path ||
!this.lastGeometry ||
this.lastGeometry.x !== geometry.x ||
this.lastGeometry.y !== geometry.y ||
this.lastGeometry.width !== geometry.width ||
this.lastGeometry.height !== geometry.height
) {
this.path = this.createPath(geometry);
this.lastGeometry = { ...geometry };
}

return this.path;
}

protected createPath(geometry: TRect): Path2D {
const path = new Path2D();
const { radius } = this.params.border;

if (radius > 0) {
// Rounded rectangle
const { x, y, width, height } = geometry;
const r = Math.min(radius, width / 2, height / 2);

path.moveTo(x + r, y);
path.lineTo(x + width - r, y);
path.quadraticCurveTo(x + width, y, x + width, y + r);
path.lineTo(x + width, y + height - r);
path.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
path.lineTo(x + r, y + height);
path.quadraticCurveTo(x, y + height, x, y + height - r);
path.lineTo(x, y + r);
path.quadraticCurveTo(x, y, x + r, y);
path.closePath();
} else {
// Simple rectangle
path.rect(geometry.x, geometry.y, geometry.width, geometry.height);
}

return path;
}

public style(ctx: CanvasRenderingContext2D): Path2DRenderStyleResult | undefined {
ctx.fillStyle = this.params.fill;
ctx.strokeStyle = this.params.border.color;
ctx.lineWidth = this.params.border.width;

return { type: "both" };
}

public isPathVisible(): boolean {
return this.getVisible();
}

public setParams(params: TBlockMinimalisticParams): void {
this.params = params;
// Invalidate path cache if border radius changed
if (this.lastGeometry && params.border.radius !== this.params.border.radius) {
this.path = null;
}
}

public invalidate(): void {
this.path = null;
this.lastGeometry = null;
}
}
56 changes: 56 additions & 0 deletions src/components/canvas/blocks/Blocks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { Component } from "../../../lib/Component";
import { CoreComponent } from "../../../lib/CoreComponent";
import { ESchedulerPriority } from "../../../lib/Scheduler";
import { ECameraScaleLevel } from "../../../services/camera/CameraService";
import { BlockState } from "../../../store/block/Block";
import { debounce } from "../../../utils/utils/schedule";
import { BatchPath2DRenderer } from "../connections/BatchPath2D";
import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";

import { Block } from "./Block";

export type TBlocksContext = TGraphLayerContext & {
blockBatch: BatchPath2DRenderer;
};

export class Blocks extends Component {
protected blocks: BlockState[] = [];
protected blocksView = {};
Expand All @@ -15,9 +23,33 @@ export class Blocks extends Component {

private font: string;

protected batch: BatchPath2DRenderer;

/**
* Debounced update to batch multiple changes into a single render cycle.
*/
private scheduleUpdate = debounce(
() => {
this.performRender();
},
{
priority: ESchedulerPriority.HIGHEST,
frameInterval: 1,
}
);

constructor(props: {}, context: CoreComponent) {
super(props, context);

this.batch = new BatchPath2DRenderer(
() => this.performRender(),
this.context.constants.block.PATH2D_CHUNK_SIZE || 100
);

this.setContext({
blockBatch: this.batch,
});

this.unsubscribe = this.subscribe();

this.prepareFont(this.getFontScale());
Expand All @@ -33,9 +65,19 @@ export class Blocks extends Component {
this.performRender();
}

protected willRender(): void {
super.willRender();
const scaleLevel = this.context.graph.cameraService.getCameraBlockScaleLevel();
if (scaleLevel === ECameraScaleLevel.Minimalistic) {
this.shouldRenderChildren = false;
this.shouldUpdateChildren = false;
}
}

protected subscribe() {
this.blocks = this.context.graph.rootStore.blocksList.$blocks.value;
this.blocksView = this.context.graph.rootStore.settings.getConfigFlag("blockComponents");

return [
this.context.graph.rootStore.blocksList.$blocks.subscribe((blocks) => {
this.blocks = blocks;
Expand All @@ -54,6 +96,7 @@ export class Blocks extends Component {

protected unmount() {
super.unmount();
this.scheduleUpdate.cancel();
this.unsubscribe.forEach((cb) => cb());
}

Expand All @@ -71,4 +114,17 @@ export class Blocks extends Component {
);
});
}

protected render(): void {
const scaleLevel = this.context.graph.cameraService.getCameraBlockScaleLevel();

// Batch rendering only for minimalistic scale level
if (scaleLevel === ECameraScaleLevel.Minimalistic) {
const paths = this.batch.orderedPaths.get();
for (const path of paths) {
path.render(this.context.ctx);
}
}
// For other scale levels, blocks render themselves through their render methods
}
}
2 changes: 1 addition & 1 deletion src/components/canvas/connections/BlockConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class BlockConnections extends Component<CoreComponentProps, TComponentSt
private scheduleUpdate = debounce(
() => {
this.performRender();
this.shouldUpdateChildren = true;
// this.shouldUpdateChildren = true;
},
{
priority: ESchedulerPriority.HIGHEST,
Expand Down
1 change: 1 addition & 0 deletions src/components/canvas/layers/graphLayer/GraphLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type TGraphLayerContext = LayerContext & {
root: HTMLDivElement;
ownerDocument: Document;
graph: Graph;
blockBatch?: import("../../connections/BatchPath2D").BatchPath2DRenderer;
};

const rootBubblingEventTypes = new Set([
Expand Down
5 changes: 4 additions & 1 deletion src/graphConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ export type TGraphConstants = {
WIDTH: number;
HEIGHT: number;
SNAPPING_GRID_SIZE: number;
/** Size of Path2D chunks for batch rendering on minimalistic scale */
PATH2D_CHUNK_SIZE: number;
};

connection: {
Expand Down Expand Up @@ -217,14 +219,15 @@ export const initGraphConstants: TGraphConstants = {
WIDTH: 200,
HEIGHT: 160,
SNAPPING_GRID_SIZE: 1,
PATH2D_CHUNK_SIZE: 400,
},
connection: {
MUTED_CANVAS_CONNECTION_WIDTH: 0.8,
SCALES: [0.01, 0.125, 0.125],
DEFAULT_Z_INDEX: 0,
THRESHOLD_LINE_HIT: 8,
MIN_ZOOM_FOR_CONNECTION_ARROW_AND_LABEL: 0.25,
PATH2D_CHUNK_SIZE: 100,
PATH2D_CHUNK_SIZE: 400,
LABEL: {
INNER_PADDINGS: [0, 0, 0, 0],
},
Expand Down
Loading
Loading