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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Support user-resizable element templates with `ElementSize` template state property:
* Resizable elements display "box with handles" in the `Halo` to change the size;
* Changed element sizes are captured and restored by `RestoreGeometry` command.
- Allow to customize link template separately for each link instead of only based on its link type IRI in `linkTemplateResolver` for `Canvas`.
- Add `EditorController.applyAuthoringChanges()` method to apply current authoring changes to the diagram (i.e. change entity data, delete relations, etc) and reset the change state to be empty.

#### ⏱ Performance
Expand Down
6 changes: 3 additions & 3 deletions examples/classicWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ function ClassicWorkspaceExample() {
}
return undefined;
},
linkTemplateResolver: type => {
if (type === 'http://www.w3.org/2000/01/rdf-schema#subClassOf') {
linkTemplateResolver: (linkType, link) => {
if (linkType === 'http://www.w3.org/2000/01/rdf-schema#subClassOf') {
return Reactodia.DefaultLinkTemplate;
}
return OntologyLinkTemplates(type);
return OntologyLinkTemplates(linkType, link);
},
}}
toolbar={{
Expand Down
4 changes: 2 additions & 2 deletions examples/styleCustomization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ function StyleCustomizationExample() {
}
return undefined;
},
linkTemplateResolver: type => {
if (!type.startsWith('urn:reactodia:')) {
linkTemplateResolver: (linkType, link) => {
if (linkType) {
return DoubleArrowLinkTemplate;
}
},
Expand Down
8 changes: 3 additions & 5 deletions src/diagram/customization.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type * as React from 'react';

import type { LinkTypeIri } from '../data/model';

import type { Element, ElementTemplateState, Link } from './elements';
import type { SizeProvider, Vector } from './geometry';
import type { GraphStructure } from './model';
Expand All @@ -15,14 +13,14 @@ import type { GraphStructure } from './model';
*/
export type TypeStyleResolver = (types: ReadonlyArray<string>) => TypeStyle | undefined;
/**
* Provides a custom template to render an element on the canvas.
* Provides a custom template to render an element (graph node) on the canvas.
*/
export type ElementTemplateResolver = (element: Element) =>
ElementTemplate | ElementTemplateComponent | undefined;
/**
* Provides a custom rendering on a diagram for links of specific type.
* Provides a custom template to render a link (graph edge) on the canvas.
*/
export type LinkTemplateResolver = (linkTypeId: LinkTypeIri) => LinkTemplate | undefined;
export type LinkTemplateResolver = (link: Link) => LinkTemplate | undefined;

/**
* Common style for a type or set of types to display in various parts of the UI.
Expand Down
101 changes: 56 additions & 45 deletions src/diagram/linkLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,8 @@ const LINK_CLASS = 'reactodia-link';
function LinkView(props: LinkViewProps) {
const {link, model, renderingState} = props;

const template = React.useMemo(
() => renderingState.createLinkTemplate(link.typeId),
[link.typeId]
);
const template = renderingState.getLinkTemplate(link);
const defaultTemplate = renderingState.shared.defaultLinkResolver(link);

const source = model.getElement(link.sourceId);
const target = model.getElement(link.targetId);
Expand Down Expand Up @@ -310,15 +308,20 @@ function LinkView(props: LinkViewProps) {
return getPointAlongPolyline(polyline, polylineLength * offset);
};

const typeIndex = renderingState.ensureLinkTypeIndex(link.typeId);
const markerSource = template.markerSource ?? defaultTemplate.markerSource;
const markerTarget = template.markerTarget ?? defaultTemplate.markerTarget;
const markerSourceIndex = markerSource
? renderingState.ensureLinkMarkerIndex(markerSource) : undefined;
const markerTargetIndex = markerTarget
? renderingState.ensureLinkMarkerIndex(markerTarget) : undefined;

const {highlighter} = renderingState.shared;
const isBlurred = highlighter && !highlighter(link);

const renderedLink = template.renderLink({
link,
markerSource: `url(#${linkMarkerKey(typeIndex, true)})`,
markerTarget: `url(#${linkMarkerKey(typeIndex, false)})`,
markerSource: markerSourceIndex ? `url(#${linkMarkerKey(markerSourceIndex, true)})` : '',
markerTarget: markerTargetIndex ? `url(#${linkMarkerKey(markerTargetIndex, false)})` : '',
path: spline.toPath(),
getPathPosition,
route,
Expand Down Expand Up @@ -674,6 +677,7 @@ class VertexTools extends React.Component<{
}

export interface LinkMarkersProps {
model: DiagramModel;
renderingState: MutableRenderingState;
}

Expand All @@ -682,38 +686,50 @@ export class LinkMarkers extends React.Component<LinkMarkersProps> {
private readonly delayedUpdate = new Debouncer();

render() {
const {renderingState} = this.props;
const {model, renderingState} = this.props;

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

for (const link of model.links) {
const template = renderingState.getLinkTemplate(link);
const defaultTemplate = renderingState.shared.defaultLinkResolver(link);

const markers: Array<React.ReactElement<LinkMarkerProps>> = [];

for (const [linkTypeId, template] of renderingState.getLinkTemplates()) {
const defaultTemplate = renderingState.shared.defaultLinkResolver(linkTypeId);
const typeIndex = renderingState.ensureLinkTypeIndex(linkTypeId);

if (template.markerSource) {
markers.push(
<LinkMarker key={typeIndex * 2}
linkTypeIndex={typeIndex}
isStartMarker={true}
style={template.markerSource}
defaultStyle={defaultTemplate.markerSource}
/>
);
const markerSource = template.markerSource ?? defaultTemplate.markerSource;
if (markerSource) {
sourceMarkers.add(markerSource);
}

if (template.markerTarget) {
markers.push(
<LinkMarker key={typeIndex * 2 + 1}
linkTypeIndex={typeIndex}
isStartMarker={false}
style={template.markerTarget}
defaultStyle={defaultTemplate.markerTarget}
/>
);
const markerTarget = template.markerTarget ?? defaultTemplate.markerTarget;
if (markerTarget) {
targetMarkers.add(markerTarget);
}
}

return <defs>{markers}</defs>;

return (
<defs>
{Array.from(sourceMarkers, marker => {
const index = renderingState.ensureLinkMarkerIndex(marker);
return (
<LinkMarker key={index}
markerIndex={index}
isStartMarker={true}
style={marker}
/>
);
})}
{Array.from(targetMarkers, marker => {
const index = renderingState.ensureLinkMarkerIndex(marker);
return (
<LinkMarker key={index}
markerIndex={index}
isStartMarker={false}
style={marker}
/>
);
})}
</defs>
);
}

componentDidMount() {
Expand All @@ -738,10 +754,9 @@ export class LinkMarkers extends React.Component<LinkMarkersProps> {
}

interface LinkMarkerProps {
linkTypeIndex: number;
markerIndex: number;
isStartMarker: boolean;
style: LinkMarkerStyle;
defaultStyle: LinkMarkerStyle | undefined;
}

const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
Expand All @@ -760,19 +775,15 @@ class LinkMarker extends React.Component<LinkMarkerProps> {
return;
}

const {linkTypeIndex, isStartMarker, style, defaultStyle} = this.props;
const {
d = defaultStyle?.d,
width = defaultStyle?.width,
height = defaultStyle?.height,
} = style;
const {markerIndex, isStartMarker, style} = this.props;
const {d, width, height} = style;
if (!(d !== undefined && width !== undefined && height !== undefined)) {
return;
}

const className = 'reactodia-link-marker';
marker.setAttribute('class', className);
marker.setAttribute('id', linkMarkerKey(linkTypeIndex, isStartMarker));
marker.setAttribute('id', linkMarkerKey(markerIndex, isStartMarker));
marker.setAttribute('markerWidth', String(width));
marker.setAttribute('markerHeight', String(height));
marker.setAttribute('orient', 'auto');
Expand All @@ -795,6 +806,6 @@ class LinkMarker extends React.Component<LinkMarkerProps> {
};
}

function linkMarkerKey(linkTypeIndex: number, startMarker: boolean) {
return `reactodia-marker-${startMarker ? 'start' : 'end'}-${linkTypeIndex}`;
function linkMarkerKey(markerIndex: number, startMarker: boolean) {
return `reactodia-marker-${startMarker ? 'start' : 'end'}-${markerIndex}`;
}
4 changes: 3 additions & 1 deletion src/diagram/paperArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ export class PaperArea extends React.Component<PaperAreaProps, State> implements
className={`${CLASS_NAME}__canvas`}
style={{overflow: 'visible'}}
paperTransform={paperTransform}>
<LinkMarkers renderingState={renderingState} />
<LinkMarkers model={model}
renderingState={renderingState}
/>
<LinkLayer model={model}
renderingState={renderingState}
/>
Expand Down
49 changes: 23 additions & 26 deletions src/diagram/renderingState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import { Debouncer } from '../coreUtils/scheduler';

import {
ElementTemplate, ElementTemplateComponent, ElementTemplateResolver, LinkTemplateResolver,
LinkTemplate, LinkRouter, RoutedLink, RoutedLinks,
LinkTemplate, LinkMarkerStyle, LinkRouter, RoutedLink, RoutedLinks,
} from './customization';

import { LinkTypeIri } from '../data/model';

import { Element, Link } from './elements';
import { Rect, ShapeGeometry, Size, SizeProvider, boundsOf, isPolylineEqual } from './geometry';
import { DefaultLinkRouter } from './linkRouter';
Expand Down Expand Up @@ -159,13 +157,13 @@ export interface RenderingState extends SizeProvider {
*/
getLinkLabelBounds(link: Link): Rect | undefined;
/**
* Resolve template component for the element.
* Resolve template for the graph element.
*/
getElementTemplate(element: Element): ElementTemplate;
/**
* Returns link templates for all types of rendered links.
* Resolve template for the graph link.
*/
getLinkTemplates(): ReadonlyMap<LinkTypeIri, LinkTemplate>;
getLinkTemplate(link: Link): LinkTemplate;
/**
* Returns route data for all links in the graph.
*/
Expand Down Expand Up @@ -193,10 +191,10 @@ export class MutableRenderingState implements RenderingState {
private readonly linkLabelContainer = document.createElement('div');
private readonly linkLabelBounds = new WeakMap<Link, Rect>();

private readonly linkTypeIndex = new Map<LinkTypeIri, number>();
private static nextLinkTypeIndex = 0;

private readonly linkTemplates = new Map<LinkTypeIri, LinkTemplate>();
private cachedLinkTemplates = new WeakMap<Link, LinkTemplate>();
private readonly linkMarkerIndex = new WeakMap<LinkMarkerStyle, number>();
private static nextLinkMarkerIndex = 1;

private readonly delayedUpdateRoutings = new Debouncer();
private routings: RoutedLinks = new Map<string, RoutedLink>();

Expand Down Expand Up @@ -231,7 +229,7 @@ export class MutableRenderingState implements RenderingState {
this.scheduleUpdateRoutings();
});
this.listener.listen(this.model.events, 'discardGraph', () => {
this.linkTemplates.clear();
this.cachedLinkTemplates = new WeakMap<Link, LinkTemplate>();

const routings = this.routings;
this.routings = new Map();
Expand Down Expand Up @@ -353,28 +351,27 @@ export class MutableRenderingState implements RenderingState {
};
}

ensureLinkTypeIndex(linkTypeId: LinkTypeIri): number {
let typeIndex = this.linkTypeIndex.get(linkTypeId);
if (typeIndex === undefined) {
typeIndex = MutableRenderingState.nextLinkTypeIndex++;
this.linkTypeIndex.set(linkTypeId, typeIndex);
ensureLinkMarkerIndex(linkMarker: LinkMarkerStyle): number {
let index = this.linkMarkerIndex.get(linkMarker);
if (index === undefined) {
index = MutableRenderingState.nextLinkMarkerIndex++;
if (MutableRenderingState.nextLinkMarkerIndex >= Number.MAX_SAFE_INTEGER) {
MutableRenderingState.nextLinkMarkerIndex = 1;
}
this.linkMarkerIndex.set(linkMarker, index);
}
return typeIndex;
}

getLinkTemplates(): ReadonlyMap<LinkTypeIri, LinkTemplate> {
return this.linkTemplates;
return index;
}

createLinkTemplate(linkTypeId: LinkTypeIri): LinkTemplate {
const existingTemplate = this.linkTemplates.get(linkTypeId);
getLinkTemplate(link: Link): LinkTemplate {
const existingTemplate = this.cachedLinkTemplates.get(link);
if (existingTemplate) {
return existingTemplate;
}

const template = this.resolveLinkTemplate(linkTypeId)
?? this.shared.defaultLinkResolver(linkTypeId);
this.linkTemplates.set(linkTypeId, template);
const template = this.resolveLinkTemplate(link)
?? this.shared.defaultLinkResolver(link);
this.cachedLinkTemplates.set(link, template);
this.source.trigger('changeLinkTemplates', {source: this});
return template;
}
Expand Down
5 changes: 2 additions & 3 deletions src/diagram/sharedCanvasState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';

import { Events, EventSource, EventObserver, PropertyChange } from '../coreUtils/events';

import type { LinkTypeIri } from '../data/model';
import { TemplateProperties, setTemplateProperty } from '../data/schema';

import type { CanvasApi, CanvasDropEvent } from './canvasApi';
Expand Down Expand Up @@ -58,7 +57,7 @@ export type CellHighlighter = (item: Element | Link) => boolean;
/** @hidden */
export interface SharedCanvasStateOptions {
defaultElementResolver: (element: Element) => ElementTemplate;
defaultLinkResolver: (linkType: LinkTypeIri) => LinkTemplate;
defaultLinkResolver: (link: Link) => LinkTemplate;
defaultLayout: LayoutFunction;
renameLinkProvider?: RenameLinkProvider;
}
Expand Down Expand Up @@ -90,7 +89,7 @@ export class SharedCanvasState {
* Default link template resolver to use as a fallback
* (returns a default template for any link).
*/
readonly defaultLinkResolver: (linkTypeId: LinkTypeIri) => LinkTemplate;
readonly defaultLinkResolver: (link: Link) => LinkTemplate;
/**
* Default layout algorithm function to use if it's not specified explicitly.
*/
Expand Down
Loading