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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
* Subscribe to canvas `resize` event to track viewport size;
* Subscribe to `changeCells` event from `DiagramModel` to track graph content changes.
- Add `TemplateProps.onlySelected` flag to use in the element templates to track if the element is the only one selected without performance penalty.
- Avoid per-layer frame delay when processing canvas layer updates without calling `RenderingState.syncUpdate()`:
* Add `useLayerDebouncedStore()` hook as more flexible way to debounce and update with the canvas layer.
- Avoid eager link type creation for relation links, only create and fetch them on first render.

#### 💅 Polish
Expand Down Expand Up @@ -62,6 +64,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Deprecate and hide by default "Edit" and "Delete" action buttons in `StandardEntity` expanded state (can be re-enabled by setting `showActions` prop to `true`).

#### 🐛 Fixed
- Fix link rendering lagging behind when moving elements.
- Fix `RdfDataProvider.links()` returning empty results when called with `linkTypeIds` parameter.
- Fix `HaloLink` and visual authoring link path highlight being rendered on top on elements by placing it onto `overLinkGeometry` widget layer instead.
- Fix `HaloLink` link path highlighting not updating on link re-route.
Expand Down
7 changes: 4 additions & 3 deletions examples/styleCustomization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ function StyleCustomizationExample() {
}

function BookDecorations() {
const {model} = Reactodia.useCanvas();
const {canvas, model} = Reactodia.useCanvas();

// Update decorations when graph content changes
Reactodia.useSyncStore(
Reactodia.useFrameDebouncedStore(
Reactodia.useEventStore(model.events, 'changeCells')
Reactodia.useLayerDebouncedStore(
Reactodia.useEventStore(model.events, 'changeCells'),
canvas.renderingState
),
() => model.cellsVersion
);
Expand Down
2 changes: 1 addition & 1 deletion src/coreUtils/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class Debouncer {
}

private schedule() {
if (typeof this.scheduled === 'undefined') {
if (this.scheduled === undefined) {
if (this.timeout === 'frame') {
this.scheduled = requestAnimationFrame(this.runSynchronously);
} else {
Expand Down
59 changes: 32 additions & 27 deletions src/diagram/elementLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as React from 'react';
import { createPortal, flushSync } from 'react-dom';

import { EventObserver } from '../coreUtils/events';
import { Debouncer } from '../coreUtils/scheduler';

import { ElementTemplate, TemplateProps } from './customization';

Expand Down Expand Up @@ -60,11 +59,9 @@ export class ElementLayer extends React.Component<ElementLayerProps, State> {
requests: new Map<string, RedrawFlags>(),
forAll: RedrawFlags.None,
};
private delayedRedraw = new Debouncer();
private readonly memoizedElements = new WeakMap<ElementState, React.ReactElement>();

private sizeRequests = new Map<string, SizeUpdateRequest>();
private delayedUpdateSizes = new Debouncer();

constructor(props: ElementLayerProps) {
super(props);
Expand Down Expand Up @@ -178,21 +175,18 @@ export class ElementLayer extends React.Component<ElementLayerProps, State> {
this.listener.listen(renderingState.shared.events, 'changeHighlight', () => {
this.requestRedrawAll(RedrawFlags.RecomputeBlurred);
});
this.listener.listen(renderingState.events, 'syncUpdate', ({layer}) => {
flushSync(() => {
if (layer === RenderingLayer.Element) {
this.delayedRedraw.runSynchronously();
} else if (layer === RenderingLayer.ElementSize) {
this.delayedUpdateSizes.runSynchronously();
}
});
});
}

componentWillUnmount() {
this.listener.stopListening();
this.delayedRedraw.dispose();
this.delayedUpdateSizes.dispose();
this.props.renderingState.cancelOnLayerUpdate(
RenderingLayer.Element,
this.redrawElements
);
this.props.renderingState.cancelOnLayerUpdate(
RenderingLayer.ElementSize,
this.recomputeQueuedSizes
);
}

private requestRedraw = (element: Element, request: RedrawFlags) => {
Expand All @@ -203,12 +197,18 @@ export class ElementLayer extends React.Component<ElementLayerProps, State> {
}
const existing = this.redrawBatch.requests.get(element.id) || RedrawFlags.None;
this.redrawBatch.requests.set(element.id, existing | request);
this.delayedRedraw.call(this.redrawElements);
this.props.renderingState.scheduleOnLayerUpdate(
RenderingLayer.Element,
this.redrawElements
);
};

private requestRedrawAll(request: RedrawFlags) {
this.redrawBatch.forAll |= request;
this.delayedRedraw.call(this.redrawElements);
this.props.renderingState.scheduleOnLayerUpdate(
RenderingLayer.Element,
this.redrawElements
);
}

private redrawElements = () => {
Expand All @@ -217,21 +217,26 @@ export class ElementLayer extends React.Component<ElementLayerProps, State> {
forAll: RedrawFlags.None,
requests: new Map<string, RedrawFlags>(),
};
this.setState((state, props) => ({
version: committedBatch.forAll === RedrawFlags.Discard
? (state.version + 1) : state.version,
elementStates: applyRedrawRequests(
props.model,
props.renderingState.shared,
committedBatch,
state.elementStates
)
}));
flushSync(() => {
this.setState((state, props) => ({
version: committedBatch.forAll === RedrawFlags.Discard
? (state.version + 1) : state.version,
elementStates: applyRedrawRequests(
props.model,
props.renderingState.shared,
committedBatch,
state.elementStates
)
}));
});
};

private requestSizeUpdate = (element: Element, node: HTMLDivElement) => {
this.sizeRequests.set(element.id, {element, node});
this.delayedUpdateSizes.call(this.recomputeQueuedSizes);
this.props.renderingState.scheduleOnLayerUpdate(
RenderingLayer.ElementSize,
this.recomputeQueuedSizes
);
};

private recomputeQueuedSizes = () => {
Expand Down
75 changes: 37 additions & 38 deletions src/diagram/linkLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import cx from 'clsx';
import * as React from 'react';
import { createPortal } from 'react-dom';
import { createPortal, flushSync } from 'react-dom';

import { EventObserver } from '../coreUtils/events';
import { Debouncer } from '../coreUtils/scheduler';
import { useEventStore, useSyncStore } from '../coreUtils/hooks';

import { useCanvas } from './canvasApi';
import { restoreCapturedLinkGeometry } from './commands';
Expand All @@ -15,7 +15,9 @@ import {
} from './geometry';
import { DiagramModel } from './model';
import { type PaperTransform, HtmlPaperLayer } from './paper';
import { MutableRenderingState, RenderingLayer } from './renderingState';
import {
type MutableRenderingState, RenderingLayer, useLayerDebouncedStore,
} from './renderingState';

export interface LinkLayerProps {
model: DiagramModel;
Expand Down Expand Up @@ -44,7 +46,6 @@ const CLASS_NAME = 'reactodia-link-layer';

export class LinkLayer extends React.Component<LinkLayerProps, LinkLayerState> {
private readonly listener = new EventObserver();
private readonly delayedUpdate = new Debouncer();

private providedContext: LinkLayerContext;

Expand All @@ -53,7 +54,6 @@ export class LinkLayer extends React.Component<LinkLayerProps, LinkLayerState> {
private scheduledToUpdate = new Set<string>();

private labelMeasureRequests = new Set<MeasurableLabel>();
private delayedMeasureLabels = new Debouncer();

private readonly memoizedLinks = new WeakMap<Link, React.ReactElement>();

Expand Down Expand Up @@ -135,18 +135,6 @@ export class LinkLayer extends React.Component<LinkLayerProps, LinkLayerState> {
updateChangedRoutes(newRoutes, previous);
updateChangedRoutes(previous, newRoutes);
});
this.listener.listen(renderingState.events, 'syncUpdate', ({layer}) => {
switch (layer) {
case RenderingLayer.Link: {
this.delayedUpdate.runSynchronously();
break;
}
case RenderingLayer.LinkLabel: {
this.delayedMeasureLabels.runSynchronously();
break;
}
}
});
}

shouldComponentUpdate(nextProps: LinkLayerProps, nextState: LinkLayerState): boolean {
Expand All @@ -159,23 +147,35 @@ export class LinkLayer extends React.Component<LinkLayerProps, LinkLayerState> {

componentWillUnmount() {
this.listener.stopListening();
this.delayedUpdate.dispose();
this.delayedMeasureLabels.dispose();
this.props.renderingState.cancelOnLayerUpdate(
RenderingLayer.Link,
this.performUpdate
);
this.props.renderingState.cancelOnLayerUpdate(
RenderingLayer.LinkLabel,
this.measureLabels
);
}

private scheduleUpdateAll = () => {
if (this.updateState !== UpdateRequest.All) {
this.updateState = UpdateRequest.All;
this.scheduledToUpdate = new Set<string>();
}
this.delayedUpdate.call(this.performUpdate);
this.props.renderingState.scheduleOnLayerUpdate(
RenderingLayer.Link,
this.performUpdate
);
};

private scheduleUpdateLink(linkId: string) {
if (this.updateState === UpdateRequest.Partial) {
this.scheduledToUpdate.add(linkId);
}
this.delayedUpdate.call(this.performUpdate);
this.props.renderingState.scheduleOnLayerUpdate(
RenderingLayer.Link,
this.performUpdate
);
}

private popShouldUpdatePredicate(): (model: Link) => boolean {
Expand All @@ -189,7 +189,10 @@ export class LinkLayer extends React.Component<LinkLayerProps, LinkLayerState> {

private scheduleLabelMeasure = (label: MeasurableLabel) => {
this.labelMeasureRequests.add(label);
this.delayedMeasureLabels.call(this.measureLabels);
this.props.renderingState.scheduleOnLayerUpdate(
RenderingLayer.LinkLabel,
this.measureLabels
);
};

private clearLabelMeasure = (label: MeasurableLabel) => {
Expand Down Expand Up @@ -218,8 +221,10 @@ export class LinkLayer extends React.Component<LinkLayerProps, LinkLayerState> {
};

private performUpdate = () => {
this.setState({
shouldUpdateLink: this.popShouldUpdatePredicate(),
flushSync(() => {
this.setState({
shouldUpdateLink: this.popShouldUpdatePredicate(),
});
});
};

Expand Down Expand Up @@ -682,20 +687,14 @@ function LinkMarkersInner(props: {
}) {
const {model, renderingState} = props;

const [cellsVersion, setCellsVersion] = React.useState(model.cellsVersion);

React.useEffect(() => {
const listener = new EventObserver();
const delayedUpdate = new Debouncer();

listener.listen(renderingState.events, 'syncUpdate', ({layer}) => {
if (layer !== RenderingLayer.Link) { return; }
delayedUpdate.runSynchronously();
});
listener.listen(renderingState.events, 'changeLinkTemplates', () => {
delayedUpdate.call(() => setCellsVersion(model.cellsVersion));
});
}, []);
useSyncStore(
useLayerDebouncedStore(
useEventStore(renderingState.events, 'changeLinkTemplates'),
renderingState,
RenderingLayer.Link
),
() => model.cellsVersion
);

const sourceMarkers = new Set<LinkMarkerStyle>();
const targetMarkers = new Set<LinkMarkerStyle>();
Expand Down
Loading