Skip to content

Commit

Permalink
implement edge labels
Browse files Browse the repository at this point in the history
  • Loading branch information
mggower committed Feb 6, 2024
1 parent cb1b30d commit a038c64
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 26 deletions.
21 changes: 19 additions & 2 deletions examples/native/src/labels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Graph.Node>((label, index) => ({
radius: 10,
label,
id: `${index}-${label}`,
id: `${index}`,
style: NODE_STYLE
}))

Expand Down
76 changes: 69 additions & 7 deletions src/renderers/webgl/edge.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
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 '../..'
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
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions src/renderers/webgl/loaders/AssetLoader.ts
Original file line number Diff line number Diff line change
@@ -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<Texture>

type LoadAssetProps = Partial<Subscriber<Texture>> & { url: string }

export default class AssetLoader {
Expand Down
9 changes: 6 additions & 3 deletions src/renderers/webgl/loaders/AssetManager.ts
Original file line number Diff line number Diff line change
@@ -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<true>
export type AssetSubscription = Subscription<Texture>

export default class AssetManager {
private _font = new FontLoader()
Expand Down
4 changes: 1 addition & 3 deletions src/renderers/webgl/loaders/FontLoader.ts
Original file line number Diff line number Diff line change
@@ -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<true>

type LoadFontProps = Partial<Subscriber<true>> & {
fontFamily: string
fontWeight?: string | number
Expand Down
12 changes: 9 additions & 3 deletions src/renderers/webgl/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions src/renderers/webgl/objects/label/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 10 additions & 2 deletions src/renderers/webgl/objects/label/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { equals } from '../../../../'
*/
export class Label {
mounted = false
offset = 0

private x?: number
private y?: number
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions src/renderers/webgl/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit a038c64

Please sign in to comment.