diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts index c5d40c4..5acc0c1 100644 --- a/examples/native/src/labels/index.ts +++ b/examples/native/src/labels/index.ts @@ -66,11 +66,28 @@ const data = [ const collide = Collide.Layout() -const edges: Graph.Edge[] = [] +const edges: Graph.Edge[] = [ + { + id: '0::1', + source: '0', + target: '1', + label: 'EDGE LABEL 0 --> 1', + style: { + label: { + fontName: 'EdgeLabel', + fontFamily: 'Roboto', + fontSize: 10, + color: DARK_GREEN, + margin: 4 + } + } + } +] + let nodes = data.map((label, index) => ({ radius: 10, label, - id: `${index}-${label}`, + id: `${index}`, style: NODE_STYLE })) diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts index 8513177..b260af0 100644 --- a/src/renderers/webgl/edge.ts +++ b/src/renderers/webgl/edge.ts @@ -1,5 +1,5 @@ import { type Renderer } from '.' -import { MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM } from './utils' +import { MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM, MIN_LABEL_ZOOM, midPoint } from './utils' import { movePoint } from './utils' import { NodeRenderer } from './node' import * as Graph from '../..' @@ -7,6 +7,8 @@ import { Arrow } from './objects/arrow' import { LineSegment } from './objects/lineSegment' import { FederatedPointerEvent } from 'pixi.js' import { EdgeHitArea } from './interaction/edgeHitArea' +import { Label } from './objects/label' +import { FontSubscription } from './loaders/AssetManager' const DEFAULT_EDGE_WIDTH = 1 const DEFAULT_EDGE_COLOR = 0xaaaaaa @@ -14,16 +16,17 @@ const DEFAULT_ARROW = 'none' export class EdgeRenderer { edge?: Graph.Edge - + label?: Label renderer: Renderer lineSegment: LineSegment source!: NodeRenderer target!: NodeRenderer - x0?: number - y0?: number - x1?: number - y1?: number - theta?: number + x0 = 0 + y0 = 0 + x1 = 0 + y1 = 0 + theta = 0 + center: [x: number, y: number] = [0, 0] width?: number stroke?: string | number strokeOpacity?: number @@ -35,8 +38,10 @@ export class EdgeRenderer { private lineMounted = false private forwardArrowMounted = false private reverseArrowMounted = false + private labelMounted = false private doubleClickTimeout: NodeJS.Timeout | undefined private doubleClick = false + private _loader?: FontSubscription constructor(renderer: Renderer, edge: Graph.Edge, source: NodeRenderer, target: NodeRenderer) { this.renderer = renderer @@ -72,6 +77,44 @@ export class EdgeRenderer { } } + this._loader?.unsubscribe() + if (edge.label === undefined || edge.label.trim() === '') { + if (this.label) { + this.renderer.labelObjectManager.delete(this.label) + this.labelMounted = false + this.label = undefined + } + } else if (this.renderer.assets.shouldLoadFont(edge.style?.label)) { + this._loader = this.renderer.assets.loadFont({ + fontFamily: edge.style.label.fontFamily, + fontWeight: edge.style.label.fontWeight, + timeout: 10000, + resolve: () => { + this._loader = undefined + if (!edge.label) { + return + } else if (this.label) { + this.label.update(edge.label, edge.style?.label) + } else { + this.label = new Label(this.renderer.fontBook, this.renderer.labelsContainer, edge.label, edge.style?.label) + this.label.rotation = this.theta + this.label.moveTo(...this.center) + if ( + this.renderer.zoom > MIN_LABEL_ZOOM && + this.visible(Math.min(this.x0, this.x1), Math.min(this.y0, this.y1), Math.max(this.x0, this.x1), Math.max(this.y0, this.y1)) + ) { + this.renderer.labelObjectManager.mount(this.label) + this.labelMounted = true + } + } + } + }) + } else if (this.label === undefined) { + this.label = new Label(this.renderer.fontBook, this.renderer.labelsContainer, edge.label, edge.style?.label) + } else { + this.label.update(edge.label, edge.style?.label) + } + this.edge = edge return this @@ -130,6 +173,17 @@ export class EdgeRenderer { } } + if (this.label) { + const shouldLabelMount = isVisible && this.renderer.zoom > MIN_LABEL_ZOOM + if (shouldLabelMount && !this.labelMounted) { + this.renderer.labelObjectManager.mount(this.label) + this.labelMounted = true + } else if (!shouldLabelMount && this.labelMounted) { + this.renderer.labelObjectManager.unmount(this.label) + this.labelMounted = false + } + } + if (isVisible) { const width = this.edge?.style?.width ?? DEFAULT_EDGE_WIDTH const stroke = this.edge?.style?.stroke ?? DEFAULT_EDGE_COLOR @@ -185,6 +239,12 @@ export class EdgeRenderer { edgeY0 = edgePoint[1] } + this.center = midPoint(edgeX0, edgeY0, edgeX1, edgeY1) + if (this.label) { + this.label.rotation = this.theta + this.label.moveTo(...this.center) + } + this.lineSegment.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.stroke, this.strokeOpacity) // TODO - draw hitArea over arrow this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.theta) @@ -193,6 +253,8 @@ export class EdgeRenderer { } delete() { + this._loader?.unsubscribe() + this._loader = undefined this.renderer.edgeObjectManager.delete(this.lineSegment) if (this.arrow?.forward) { this.renderer.edgeArrowObjectManager.delete(this.arrow.forward) diff --git a/src/renderers/webgl/loaders/AssetLoader.ts b/src/renderers/webgl/loaders/AssetLoader.ts index 559f8a0..dbf3440 100644 --- a/src/renderers/webgl/loaders/AssetLoader.ts +++ b/src/renderers/webgl/loaders/AssetLoader.ts @@ -1,9 +1,7 @@ -import { Publisher, Subscriber, Subscription } from './PubSub' +import { Publisher, Subscriber } from './PubSub' import { Assets, Texture } from 'pixi.js' import { noop } from '../../../utils' -export type AssetSubscription = Subscription - type LoadAssetProps = Partial> & { url: string } export default class AssetLoader { diff --git a/src/renderers/webgl/loaders/AssetManager.ts b/src/renderers/webgl/loaders/AssetManager.ts index 6583408..3e74014 100644 --- a/src/renderers/webgl/loaders/AssetManager.ts +++ b/src/renderers/webgl/loaders/AssetManager.ts @@ -1,7 +1,10 @@ -import FontLoader, { FontSubscription } from './FontLoader' -import AssetLoader, { AssetSubscription } from './AssetLoader' +import FontLoader from './FontLoader' +import AssetLoader from './AssetLoader' +import { Subscription } from './PubSub' +import { Texture } from 'pixi.js' -export type { FontSubscription, AssetSubscription } +export type FontSubscription = Subscription +export type AssetSubscription = Subscription export default class AssetManager { private _font = new FontLoader() diff --git a/src/renderers/webgl/loaders/FontLoader.ts b/src/renderers/webgl/loaders/FontLoader.ts index b7769dd..4fb553c 100644 --- a/src/renderers/webgl/loaders/FontLoader.ts +++ b/src/renderers/webgl/loaders/FontLoader.ts @@ -1,10 +1,8 @@ -import { Subscriber, Publisher, Subscription } from './PubSub' +import { Subscriber, Publisher } from './PubSub' import { STYLE_DEFAULTS } from '../objects/label/utils' import { noop } from '../../../utils' import FontFaceObserver from 'fontfaceobserver' -export type FontSubscription = Subscription - type LoadFontProps = Partial> & { fontFamily: string fontWeight?: string | number diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index a6316e8..1c87aef 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -56,6 +56,7 @@ export class NodeRenderer { this.node = node + this._labelLoader?.unsubscribe() // TODO -> manage asset loading in object's class if (node.label === undefined || node.label.trim() === '') { if (this.label) { @@ -64,7 +65,6 @@ export class NodeRenderer { this.label = undefined } } else if (this.renderer.assets.shouldLoadFont(node.style?.label)) { - this._labelLoader?.unsubscribe() this._labelLoader = this.renderer.assets.loadFont({ fontFamily: node.style.label.fontFamily, fontWeight: node.style.label.fontWeight, @@ -78,7 +78,8 @@ export class NodeRenderer { this.label.update(node.label, node.style?.label) } else { this.label = new Label(this.renderer.fontBook, this.renderer.labelsContainer, node.label, node.style?.label) - this.label.moveTo(this.x, this.y, this.strokes.radius) + this.label.offset = this.strokes.radius + this.label.moveTo(this.x, this.y) this.mountLabel(this.visible() && this.renderer.zoom > MIN_LABEL_ZOOM) } } @@ -565,7 +566,12 @@ export class NodeRenderer { this.fill.update(this.x, this.y, radius, node.style) this.strokes.update(this.x, this.y, radius, node.style) - this.label?.moveTo(this.x, this.y, this.strokes.radius) + + if (this.label) { + this.label.offset = this.strokes.radius + this.label.moveTo(this.x, this.y) + } + this.icon?.moveTo(this.x, this.y) this.hitArea.update(this.x, this.y, radius) } diff --git a/src/renderers/webgl/objects/label/background.ts b/src/renderers/webgl/objects/label/background.ts index 5e9e97d..451ab5b 100644 --- a/src/renderers/webgl/objects/label/background.ts +++ b/src/renderers/webgl/objects/label/background.ts @@ -98,6 +98,10 @@ export class LabelBackground { this.label = text } + set rotation(rotation: number) { + this.sprite.rotation = rotation + } + private resize() { const { height, width } = this.size if (height !== this.sprite.height) { diff --git a/src/renderers/webgl/objects/label/index.ts b/src/renderers/webgl/objects/label/index.ts index 3029c47..ffd86c4 100644 --- a/src/renderers/webgl/objects/label/index.ts +++ b/src/renderers/webgl/objects/label/index.ts @@ -14,6 +14,7 @@ import { equals } from '../../../../' */ export class Label { mounted = false + offset = 0 private x?: number private y?: number @@ -85,8 +86,8 @@ export class Label { return this } - moveTo(x: number, y: number, offset = 0) { - const { label, bg } = utils.getLabelCoordinates(x, y, offset, this.isBitmapText(), this.style) + moveTo(x: number, y: number) { + const { label, bg } = utils.getLabelCoordinates(x, y, this.offset, this.isBitmapText(), this.style) this.labelBackground?.moveTo(bg.x, bg.y) @@ -130,6 +131,13 @@ export class Label { return undefined } + set rotation(rotation: number) { + this.text.rotation = rotation + if (this.labelBackground) { + this.labelBackground.rotation = rotation + } + } + private create() { const label = this.label const style = this.style diff --git a/src/renderers/webgl/utils.ts b/src/renderers/webgl/utils.ts index 11f3ea2..b5e5f57 100644 --- a/src/renderers/webgl/utils.ts +++ b/src/renderers/webgl/utils.ts @@ -27,9 +27,11 @@ export const logUnknownEdgeError = (source: Graph.Node | undefined, target: Grap } } -export const movePoint = (x: number, y: number, angle: number, distance: number) => - [x + Math.cos(angle) * distance, y + Math.sin(angle) * distance] as const +export const movePoint = (x: number, y: number, angle: number, distance: number): [x: number, y: number] => [ + x + Math.cos(angle) * distance, + y + Math.sin(angle) * distance +] -export const midPoint = (x0: number, y0: number, x1: number, y1: number) => [(x0 + x1) / 2, (y0 + y1) / 2] as const +export const midPoint = (x0: number, y0: number, x1: number, y1: number): [x: number, y: number] => [(x0 + x1) / 2, (y0 + y1) / 2] export const length = (x0: number, y0: number, x1: number, y1: number) => Math.hypot(x1 - x0, y1 - y0)