From 411ed397e4f5086c3ccf762ac64248805ac024c1 Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Tue, 13 Feb 2024 10:57:41 -0500 Subject: [PATCH 01/12] RectangleTexture and createRenderTexture util --- src/renderers/webgl/textures/ArrowTexture.ts | 23 ++------- src/renderers/webgl/textures/CircleTexture.ts | 21 ++------ .../webgl/textures/RectangleTexture.ts | 32 ++++++++++++ .../webgl/textures/TextIconTexture.ts | 49 +++++-------------- src/utils/webgl.ts | 28 +++++++++++ 5 files changed, 79 insertions(+), 74 deletions(-) create mode 100644 src/renderers/webgl/textures/RectangleTexture.ts create mode 100644 src/utils/webgl.ts diff --git a/src/renderers/webgl/textures/ArrowTexture.ts b/src/renderers/webgl/textures/ArrowTexture.ts index 7d11d11..6a4448b 100644 --- a/src/renderers/webgl/textures/ArrowTexture.ts +++ b/src/renderers/webgl/textures/ArrowTexture.ts @@ -1,7 +1,8 @@ -import { RenderTexture, Graphics, Matrix, MSAA_QUALITY, Renderer as PixiRenderer } from 'pixi.js' +import { RenderTexture, Graphics, Matrix } from 'pixi.js' +import { createRenderTexture } from '../../../utils/webgl' import { MIN_TEXTURE_ZOOM } from '../../../utils/constants' -import { Texture } from '../../../types' import { Renderer } from '..' +import { Texture } from '../../../types' export default class ArrowTexture implements Texture { private texture: RenderTexture @@ -12,23 +13,7 @@ export default class ArrowTexture implements Texture { .lineTo(this.height * this.scaleFactor, this.width * this.scaleFactor * 0.5) .lineTo(this.height * this.scaleFactor, -this.width * this.scaleFactor * 0.5) - this.texture = RenderTexture.create({ - width: graphic.width, - height: graphic.height, - multisample: MSAA_QUALITY.HIGH, - resolution: 2 - }) - - renderer.app.renderer.render(graphic, { - renderTexture: this.texture, - transform: new Matrix(1, 0, 0, 1, 0, graphic.height / 2) - }) - - if (renderer.app.renderer instanceof PixiRenderer) { - renderer.app.renderer.framebuffer.blit() - } - - graphic.destroy(true) + this.texture = createRenderTexture(renderer.app, graphic, new Matrix(1, 0, 0, 1, 0, graphic.height / 2)) } get() { diff --git a/src/renderers/webgl/textures/CircleTexture.ts b/src/renderers/webgl/textures/CircleTexture.ts index e1c6b5b..e9f6a07 100644 --- a/src/renderers/webgl/textures/CircleTexture.ts +++ b/src/renderers/webgl/textures/CircleTexture.ts @@ -1,4 +1,5 @@ -import { RenderTexture, Graphics, Matrix, MSAA_QUALITY, Renderer as PixiRenderer } from 'pixi.js' +import { RenderTexture, Graphics, Matrix } from 'pixi.js' +import { createRenderTexture } from '../../../utils/webgl' import { MIN_TEXTURE_ZOOM } from '../../../utils/constants' import { Texture } from '../../../types' import { Renderer } from '..' @@ -9,23 +10,7 @@ export default class CircleTexture implements Texture { constructor(renderer: Renderer) { const graphic = new Graphics().beginFill(0xffffff).drawCircle(0, 0, this.scaleFactor) - this.texture = RenderTexture.create({ - width: graphic.width, - height: graphic.height, - multisample: MSAA_QUALITY.HIGH, - resolution: 2 - }) - - renderer.app.renderer.render(graphic, { - renderTexture: this.texture, - transform: new Matrix(1, 0, 0, 1, graphic.width / 2, graphic.height / 2) - }) - - if (renderer.app.renderer instanceof PixiRenderer) { - renderer.app.renderer.framebuffer.blit() - } - - graphic.destroy(true) + this.texture = createRenderTexture(renderer.app, graphic, new Matrix(1, 0, 0, 1, graphic.width / 2, graphic.height / 2)) } get() { diff --git a/src/renderers/webgl/textures/RectangleTexture.ts b/src/renderers/webgl/textures/RectangleTexture.ts new file mode 100644 index 0000000..e89382e --- /dev/null +++ b/src/renderers/webgl/textures/RectangleTexture.ts @@ -0,0 +1,32 @@ +import { Graphics, Matrix, RenderTexture } from 'pixi.js' +import { createRenderTexture } from '../../../utils/webgl' +import { MIN_TEXTURE_ZOOM } from '../../../utils/constants' +import { Renderer } from '..' +import { Texture } from '../../../types' + +export default class RectangleTexture implements Texture { + scaleFactor: number + private texture: RenderTexture + + constructor(renderer: Renderer) { + this.scaleFactor = Math.max(renderer.width, renderer.height) * this.minTextureZoom + + const graphic = new Graphics().beginFill(0xffffff).drawRect(0, 0, this.scaleFactor, this.scaleFactor) + + this.texture = createRenderTexture(renderer.app, graphic, new Matrix(1, 0, 0, 1, graphic.width / 2, graphic.height / 2)) + } + + get() { + return this.texture + } + + delete() { + this.texture.destroy() + return undefined + } + + // TODO -> make configurable + private get minTextureZoom() { + return MIN_TEXTURE_ZOOM + } +} diff --git a/src/renderers/webgl/textures/TextIconTexture.ts b/src/renderers/webgl/textures/TextIconTexture.ts index 07d7be2..35e3528 100644 --- a/src/renderers/webgl/textures/TextIconTexture.ts +++ b/src/renderers/webgl/textures/TextIconTexture.ts @@ -1,13 +1,11 @@ -import { RenderTexture, Text as PixiText, MSAA_QUALITY, Matrix, Renderer as PixiRenderer } from 'pixi.js' -import { DEFAULT_RESOLUTION, DEFAULT_TEXT_STYLE, MIN_TEXTURE_ZOOM } from '../../../utils/constants' +import { RenderTexture, Text as PixiText } from 'pixi.js' +import { createRenderTexture } from '../../../utils/webgl' import { TextIcon, Texture } from '../../../types' +import { MIN_TEXTURE_ZOOM } from '../../../utils/constants' import { Renderer } from '..' import TextTexture from './TextTexture' -const getCacheKey = ({ content, style = {} }: TextIcon) => { - const { color, stroke, fontFamily, fontSize, fontWeight } = { ...style, ...DEFAULT_TEXT_STYLE } - return [content, color, stroke.color, stroke.width, fontFamily, fontSize, fontWeight].join('-') -} +const join = (...args: (string | number)[]) => args.join('-') export default class TextIconTexture implements Texture { protected cache: { [key: string]: RenderTexture } = {} @@ -17,10 +15,16 @@ export default class TextIconTexture implements Texture { } get(icon: TextIcon) { - const key = getCacheKey(icon) + const style = new TextTexture(icon.style, { defaultTextStyle: { align: 'center' } }) + const key = join(icon.content, style.color, style.stroke.color, style.stroke.width, style.fontFamily, style.fontSize, style.fontWeight) if (this.cache[key] === undefined) { - this.cache[key] = this.createTexture(icon) + style.fontSize = style.fontSize * this.scaleFactor + + const object = new PixiText(icon.content, style.getTextStyle()) + object.updateText(true) + + this.cache[key] = createRenderTexture(this.renderer.app, object) } return this.cache[key] @@ -39,33 +43,4 @@ export default class TextIconTexture implements Texture { get scaleFactor() { return MIN_TEXTURE_ZOOM } - get resolution() { - return DEFAULT_RESOLUTION - } - - private createTexture(icon: TextIcon) { - const style = new TextTexture(icon.style, { defaultTextStyle: { align: 'center' } }) - style.fontSize = style.fontSize * this.scaleFactor - - const object = new PixiText(icon.content, style.getTextStyle()) - - object.updateText(true) - - const renderTexture = RenderTexture.create({ - width: object.width, - height: object.height, - multisample: MSAA_QUALITY.HIGH, - resolution: this.resolution - }) - - this.renderer.app.renderer.render(object, { renderTexture, transform: new Matrix() }) - - if (this.renderer.app.renderer instanceof PixiRenderer) { - this.renderer.app.renderer.framebuffer.blit() - } - - object.destroy(true) - - return renderTexture - } } diff --git a/src/utils/webgl.ts b/src/utils/webgl.ts new file mode 100644 index 0000000..8f04e7c --- /dev/null +++ b/src/utils/webgl.ts @@ -0,0 +1,28 @@ +import { Application, Renderer, Container, Matrix, RenderTexture, IBaseTextureOptions, MSAA_QUALITY, SCALE_MODES } from 'pixi.js' +import { DEFAULT_RESOLUTION } from './constants' + +export const createRenderTexture = ( + app: A, + graphic: G, + transform = new Matrix(), + options: IBaseTextureOptions = {} +) => { + const renderTexture = RenderTexture.create({ + width: graphic.width, + height: graphic.height, + resolution: DEFAULT_RESOLUTION, + multisample: MSAA_QUALITY.HIGH, + scaleMode: SCALE_MODES.LINEAR, + ...options + }) + + app.renderer.render(graphic, { renderTexture, transform }) + + if (app.renderer instanceof Renderer) { + app.renderer.framebuffer.blit() + } + + graphic.destroy(true) + + return renderTexture +} From 0dc830443b6c6b18609d3233de5ea76f0630c33d Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Tue, 13 Feb 2024 11:48:28 -0500 Subject: [PATCH 02/12] standardize RenderObject type --- src/renderers/webgl/LifecycleManager.ts | 4 +- src/renderers/webgl/node.ts | 8 +- src/renderers/webgl/objects/Icon.ts | 4 +- src/renderers/webgl/objects/circle/Circle.ts | 124 ++++++++++++++++++ .../webgl/objects/circle/CircleStrokes.ts | 118 +++++++++++++++++ src/renderers/webgl/objects/nodeFill.ts | 76 ----------- src/renderers/webgl/objects/nodeStrokes.ts | 10 +- .../webgl/objects/rectangle/Rectangle.ts | 20 +++ .../objects/rectangle/RectangleStrokes.ts | 20 +++ src/renderers/webgl/objects/text/Text.ts | 4 +- .../webgl/objects/text/TextHighlight.ts | 4 +- src/types/internal.ts | 4 + src/utils/constants.ts | 8 +- 13 files changed, 310 insertions(+), 94 deletions(-) create mode 100644 src/renderers/webgl/objects/circle/Circle.ts create mode 100644 src/renderers/webgl/objects/circle/CircleStrokes.ts delete mode 100644 src/renderers/webgl/objects/nodeFill.ts create mode 100644 src/renderers/webgl/objects/rectangle/Rectangle.ts create mode 100644 src/renderers/webgl/objects/rectangle/RectangleStrokes.ts diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts index fbdc473..968ca8e 100644 --- a/src/renderers/webgl/LifecycleManager.ts +++ b/src/renderers/webgl/LifecycleManager.ts @@ -1,13 +1,13 @@ import { NodeStrokes } from './objects/nodeStrokes' import { LineSegment } from './objects/lineSegment' -import { NodeFill } from './objects/nodeFill' import { Arrow } from './objects/arrow' import ObjectManager from './objects/ObjectManager' +import Circle from './objects/circle/Circle' import Icon from './objects/Icon' import Text from './objects/text/Text' export default class LifecycleManager { - nodes = new ObjectManager(2000) + nodes = new ObjectManager(2000) icons = new ObjectManager(1000) edges = new ObjectManager(2000) arrows = new ObjectManager(1000) diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index f03e7b5..1fb44e2 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -3,9 +3,9 @@ import { FederatedPointerEvent } from 'pixi.js' import { NodeStrokes } from './objects/nodeStrokes' import { NodeHitArea } from './interaction/nodeHitArea' import { interpolate } from '../../utils/helpers' -import { NodeFill } from './objects/nodeFill' import { type Renderer } from '.' import type { Node } from '../../types' +import Circle from './objects/circle/Circle' import Text from './objects/text/Text' import Icon from './objects/Icon' @@ -13,7 +13,7 @@ export class NodeRenderer { node!: Node x!: number y!: number - fill: NodeFill + fill: Circle label?: Text icon?: Icon strokes: NodeStrokes @@ -32,7 +32,7 @@ export class NodeRenderer { constructor(renderer: Renderer, node: Node) { this.renderer = renderer - this.fill = new NodeFill(this.renderer.nodesContainer, this.renderer.circle) + this.fill = new Circle(renderer.nodesContainer, renderer.circle, node.style?.color) this.strokes = new NodeStrokes(this.renderer.nodesContainer, this.renderer.circle, this.fill) this.hitArea = new NodeHitArea(this.renderer.interactionContainer, this) this.update(node) @@ -491,7 +491,7 @@ export class NodeRenderer { this.x = x this.y = y - this.fill.update(this.x, this.y, radius, node.style) + this.fill.moveTo(this.x, this.y).resize(radius) this.strokes.update(this.x, this.y, radius, node.style) if (this.label) { diff --git a/src/renderers/webgl/objects/Icon.ts b/src/renderers/webgl/objects/Icon.ts index fb8667d..5b01b5e 100644 --- a/src/renderers/webgl/objects/Icon.ts +++ b/src/renderers/webgl/objects/Icon.ts @@ -1,10 +1,10 @@ import { Container, Sprite, Texture } from 'pixi.js' -import { IconStyle } from '../../../types' +import { IconStyle, RenderObject } from '../../../types' import { equals } from '../../../utils/api' import AssetManager, { AssetSubscription, FontSubscription } from '../loaders/AssetManager' import TextIconTexture from '../textures/TextIconTexture' -export default class Icon { +export default class Icon implements RenderObject { mounted = false private x = 0 diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts new file mode 100644 index 0000000..3595c4c --- /dev/null +++ b/src/renderers/webgl/objects/circle/Circle.ts @@ -0,0 +1,124 @@ +import { FillStyle, RenderObject } from '../../../../types' +import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' +import { Container, Sprite } from 'pixi.js' +import CircleTexture from '../../textures/CircleTexture' + +export default class Circle implements RenderObject { + mounted = false + + private _x = 0 + private _y = 0 + private _radius = 0 + + private object: Sprite + private style: Required = DEFAULT_FILL_STYLE + + constructor( + private container: Container, + private texture: CircleTexture, + color?: string, + opacity?: number + ) { + this.container = container + this.texture = texture + + this.style = { + color: color ?? this.style.color, + opacity: opacity ?? this.style.opacity + } + + this.object = this.create() + } + + update(color = this.style.color, opacity = this.style.opacity) { + if (color !== this.style.color) { + this.style.color = color + this.object.tint = color + } + + if (opacity !== this.style.opacity) { + this.style.opacity = opacity + this.object.alpha = opacity + } + + return this + } + + moveTo(x: number, y: number) { + if (x !== this.x) { + this._x = x + this.object.x = x + } + + if (y !== this.y) { + this._y = y + this.object.y = y + } + + return this + } + + resize(radius: number) { + if (this._radius !== radius) { + this._radius = radius + this.object.scale.set(radius / this.texture.scaleFactor) + } + + return this + } + + mount() { + // TODO - why is mounting/unmouting fill Sprite less efficient? + if (!this.mounted) { + this.mounted = true + this.object.visible = true + } + + return this + } + + unmount() { + if (this.mounted) { + this.mounted = false + this.object.visible = false + } + + return this + } + + delete() { + this.unmount() + this.container.removeChild(this.object) + this.object.destroy() + + return undefined + } + + getContainerIndex() { + return this.container.getChildIndex(this.object) + } + + get x() { + return this._x + } + + get y() { + return this._y + } + + get radius() { + return this._radius + } + + private create() { + const object = new Sprite(this.texture.get()) + object.anchor.set(0.5) + object.x = this._x + object.y = this._y + object.visible = this.mounted + object.tint = this.style.color + object.alpha = this.style.opacity + this.container.addChild(object) + return object + } +} diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts new file mode 100644 index 0000000..6ba9a53 --- /dev/null +++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts @@ -0,0 +1,118 @@ +import { FillStyle, RenderObject } from '../../../../types' +import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' +import { Container, Sprite } from 'pixi.js' +import CircleTexture from '../../textures/CircleTexture' + +export default class CircleStrokes implements RenderObject { + mounted = false + + private _x = 0 + private _y = 0 + private _radius = 0 + + private object: Sprite + private style: Required = DEFAULT_FILL_STYLE + + constructor( + private container: Container, + private texture: CircleTexture, + style?: FillStyle + ) { + this.container = container + this.texture = texture + + if (style) { + this.style = { ...DEFAULT_FILL_STYLE, ...style } + } + + this.object = this.create() + } + + update(color = this.style.color, opacity = this.style.opacity) { + if (color !== this.style.color) { + this.style.color = color + this.object.tint = color + } + + if (opacity !== this.style.opacity) { + this.style.opacity = opacity + this.object.alpha = opacity + } + + return this + } + + moveTo(x: number, y: number) { + if (x !== this.x) { + this._x = x + this.object.x = x + } + + if (y !== this.y) { + this._y = y + this.object.y = y + } + + return this + } + + resize(radius: number) { + if (this._radius !== radius) { + this._radius = radius + this.object.scale.set(radius / this.texture.scaleFactor) + } + + return this + } + + mount() { + // TODO - why is mounting/unmouting fill Sprite less efficient? + if (!this.mounted) { + this.mounted = true + this.object.visible = true + } + + return this + } + + unmount() { + if (this.mounted) { + this.mounted = false + this.object.visible = false + } + + return this + } + + delete() { + this.unmount() + this.container.removeChild(this.object) + this.object.destroy() + + return undefined + } + + get x() { + return this._x + } + + get y() { + return this._y + } + + get radius() { + return this._radius + } + + private create() { + const object = new Sprite(this.texture.get()) + object.anchor.set(0.5) + object.x = this._x + object.y = this._y + object.visible = this.mounted + object.tint = this.style.color + object.alpha = this.style.opacity + this.container.addChild(object) + return object + } +} diff --git a/src/renderers/webgl/objects/nodeFill.ts b/src/renderers/webgl/objects/nodeFill.ts deleted file mode 100644 index e5a940a..0000000 --- a/src/renderers/webgl/objects/nodeFill.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Container, Sprite } from 'pixi.js' -import type { NodeStyle } from '../../../types' -import CircleTexture from '../textures/CircleTexture' - -const DEFAULT_NODE_FILL = 0xaaaaaa - -export class NodeFill { - mounted = false - fill: Sprite // TODO - make private - - private container: Container - private circleTexture: CircleTexture - private radius?: number - private style?: NodeStyle - - constructor(container: Container, circleTexture: CircleTexture) { - this.container = container - this.circleTexture = circleTexture - this.fill = new Sprite(this.circleTexture.get()) - this.fill.anchor.set(0.5) - this.fill.visible = false - - this.container.addChild(this.fill) - } - - update(x: number, y: number, radius: number, style?: NodeStyle) { - if ((style?.color ?? DEFAULT_NODE_FILL) !== (this.style?.color ?? DEFAULT_NODE_FILL)) { - this.fill.tint = style?.color ?? DEFAULT_NODE_FILL - } - - if (radius !== this.radius) { - this.fill.scale.set(radius / this.circleTexture.scaleFactor) - this.radius = radius - } - - this.fill.x = x - this.fill.y = y - - this.style = style - - return this - } - - mount() { - if (!this.mounted) { - // TODO - why is mounting/unmouting fill Sprite less efficient? - this.fill.visible = true - // this.container.addChild(this.fill) - this.mounted = true - } - - return this - } - - unmount() { - if (this.mounted) { - this.fill.visible = false - // this.container.removeChild(this.fill) - this.mounted = false - } - - return this - } - - delete() { - this.unmount() - this.container.removeChild(this.fill) - this.fill.destroy() - - return undefined - } - - getContainerIndex() { - return this.container.getChildIndex(this.fill) - } -} diff --git a/src/renderers/webgl/objects/nodeStrokes.ts b/src/renderers/webgl/objects/nodeStrokes.ts index 00243b6..cd6968e 100644 --- a/src/renderers/webgl/objects/nodeStrokes.ts +++ b/src/renderers/webgl/objects/nodeStrokes.ts @@ -1,6 +1,5 @@ import { Container, Sprite } from 'pixi.js' import CircleTexture from '../textures/CircleTexture' -import { NodeFill } from './nodeFill' import type { NodeStyle } from '../../../types' export class NodeStrokes { @@ -8,12 +7,13 @@ export class NodeStrokes { radius = 0 sprites?: Sprite[] // TODO - make private - private container: Container - private circleTexture: CircleTexture - private fill: NodeFill private style?: NodeStyle - constructor(container: Container, circleTexture: CircleTexture, fill: NodeFill) { + constructor( + private container: Container, + private circleTexture: CircleTexture, + private fill: { getContainerIndex: () => number } + ) { this.container = container this.circleTexture = circleTexture this.fill = fill diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts new file mode 100644 index 0000000..089d210 --- /dev/null +++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts @@ -0,0 +1,20 @@ +import { RenderObject } from '../../../../types' + +export default class Rectangle implements RenderObject { + mounted = false + + moveTo(): this { + return this + } + mount() { + this.mounted = true + return this + } + unmount(): this { + this.mounted = false + return this + } + delete(): void { + this.unmount() + } +} diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts new file mode 100644 index 0000000..7c0bb01 --- /dev/null +++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts @@ -0,0 +1,20 @@ +import { RenderObject } from '../../../../types' + +export default class RectangleStrokes implements RenderObject { + mounted = false + + moveTo(): this { + return this + } + mount() { + this.mounted = true + return this + } + unmount(): this { + this.mounted = false + return this + } + delete(): void { + this.unmount() + } +} diff --git a/src/renderers/webgl/objects/text/Text.ts b/src/renderers/webgl/objects/text/Text.ts index 06b61dd..b523e39 100644 --- a/src/renderers/webgl/objects/text/Text.ts +++ b/src/renderers/webgl/objects/text/Text.ts @@ -1,11 +1,11 @@ -import type { Bounds, TextStyle, TextObject, FontWeight } from '../../../../types' +import type { Bounds, TextStyle, TextObject, FontWeight, RenderObject } from '../../../../types' import { BitmapText, Container, Text as PixiText } from 'pixi.js' import { equals } from '../../../../utils/api' import TextTexture, { TextTextureOptions } from '../../textures/TextTexture' import AssetManager, { FontSubscription } from '../../loaders/AssetManager' import TextHighlight from './TextHighlight' -export default class Text { +export default class Text implements RenderObject { mounted = false offset = 0 diff --git a/src/renderers/webgl/objects/text/TextHighlight.ts b/src/renderers/webgl/objects/text/TextHighlight.ts index c96ce70..d7ecba1 100644 --- a/src/renderers/webgl/objects/text/TextHighlight.ts +++ b/src/renderers/webgl/objects/text/TextHighlight.ts @@ -1,7 +1,7 @@ import { Container, Sprite, Texture } from 'pixi.js' -import { FillStyle, TextObject } from '../../../../types' +import { FillStyle, RenderObject, TextObject } from '../../../../types' -export default class TextHighlight { +export default class TextHighlight implements RenderObject { mounted = false private x = 0 diff --git a/src/types/internal.ts b/src/types/internal.ts index d31e657..71a61c1 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -17,3 +17,7 @@ export interface RenderObjectLifecycle { unmount(): this delete(): void } + +export interface RenderObject extends RenderObjectLifecycle { + moveTo(...args: number[]): this +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8e8bd81..160cdda 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -13,7 +13,13 @@ export const DEFAULT_OPACITY = 1 export const COLORS = { BLACK: '#000000', - WHITE: '#FFFFFF' + WHITE: '#FFFFFF', + GREY: '#AAAAAA' +} + +export const DEFAULT_FILL_STYLE = { + color: COLORS.GREY, + opacity: DEFAULT_OPACITY } export const DEFAULT_TEXT_STYLE = { From 7d10c9117dca9f3f3435e63f0782e53583142137 Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Tue, 13 Feb 2024 13:28:30 -0500 Subject: [PATCH 03/12] replace nodeFill and nodeStrokes with Circle and CircleStrokes --- examples/native/src/labels/index.ts | 17 ++- src/renderers/webgl/LifecycleManager.ts | 4 +- src/renderers/webgl/node.ts | 110 +++++++++------ src/renderers/webgl/objects/circle/Circle.ts | 25 ++-- .../webgl/objects/circle/CircleStrokes.ts | 128 ++++++++++-------- 5 files changed, 165 insertions(+), 119 deletions(-) diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts index 5147f0c..87a0dc5 100644 --- a/examples/native/src/labels/index.ts +++ b/examples/native/src/labels/index.ts @@ -26,7 +26,10 @@ const TEXT_ICON: Graph.TextIcon = { const NODE_STYLE: Graph.NodeStyle = { color: GREEN, icon: TEXT_ICON, - stroke: [{ width: 2, color: GREEN_LIGHT }], + stroke: [ + { width: 1, color: '#FFF' }, + { width: 2, color: GREEN_LIGHT } + ], label: { position: 'bottom', fontName: 'NodeLabel', @@ -39,7 +42,11 @@ const NODE_STYLE: Graph.NodeStyle = { const NODE_HOVER_STYLE: Graph.NodeStyle = { color: DARK_GREEN, icon: IMAGE_ICON, - stroke: [{ width: 2, color: GREEN_LIGHT }], + stroke: [ + { width: 1, color: '#FFF' }, + { width: 2, color: GREEN_LIGHT }, + { width: 1, color: DARK_GREEN } + ], label: { position: 'bottom', fontName: 'NodeLabelHover', @@ -119,7 +126,9 @@ const options: Renderer.Options = { }, onNodePointerEnter: (event: Renderer.NodePointerEvent) => { // console.log('node pointer enter', `x: ${event.x}, y: ${event.y}`) - nodes = nodes.map((node) => (node.id === event.target.id ? { ...node, label: node.label + ' 北京', style: NODE_HOVER_STYLE } : node)) + nodes = nodes.map((node) => + node.id === event.target.id ? { ...node, radius: 15, label: node.label + ' 北京', style: NODE_HOVER_STYLE } : node + ) renderer.update({ nodes, edges, options }) }, onNodeDrag: (event: Renderer.NodeDragEvent) => { @@ -132,7 +141,7 @@ const options: Renderer.Options = { onNodePointerLeave: (event: Renderer.NodePointerEvent) => { // console.log('node pointer leave', `x: ${event.x}, y: ${event.y}`) nodes = nodes.map((node) => - node.id === event.target.id ? { ...node, label: node.label?.slice(0, node.label.length - 3), style: NODE_STYLE } : node + node.id === event.target.id ? { ...node, radius: 10, label: node.label?.slice(0, node.label.length - 3), style: NODE_STYLE } : node ) renderer.update({ nodes, edges, options }) } diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts index 968ca8e..2cdce0d 100644 --- a/src/renderers/webgl/LifecycleManager.ts +++ b/src/renderers/webgl/LifecycleManager.ts @@ -1,13 +1,13 @@ -import { NodeStrokes } from './objects/nodeStrokes' import { LineSegment } from './objects/lineSegment' import { Arrow } from './objects/arrow' import ObjectManager from './objects/ObjectManager' +import CircleStrokes from './objects/circle/CircleStrokes' import Circle from './objects/circle/Circle' import Icon from './objects/Icon' import Text from './objects/text/Text' export default class LifecycleManager { - nodes = new ObjectManager(2000) + nodes = new ObjectManager(2000) icons = new ObjectManager(1000) edges = new ObjectManager(2000) arrows = new ObjectManager(1000) diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index 1fb44e2..5e2d189 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -1,22 +1,23 @@ import { DEFAULT_LABEL_STYLE, MIN_LABEL_ZOOM, MIN_INTERACTION_ZOOM, MIN_NODE_STROKE_ZOOM, MIN_NODE_ICON_ZOOM } from '../../utils/constants' import { FederatedPointerEvent } from 'pixi.js' -import { NodeStrokes } from './objects/nodeStrokes' import { NodeHitArea } from './interaction/nodeHitArea' import { interpolate } from '../../utils/helpers' import { type Renderer } from '.' import type { Node } from '../../types' import Circle from './objects/circle/Circle' +import CircleStrokes from './objects/circle/CircleStrokes' import Text from './objects/text/Text' import Icon from './objects/Icon' export class NodeRenderer { - node!: Node - x!: number - y!: number + node: Node + x = 0 + y = 0 + radius = 0 fill: Circle + strokes: CircleStrokes label?: Text icon?: Icon - strokes: NodeStrokes private hitArea: NodeHitArea private renderer: Renderer @@ -32,13 +33,19 @@ export class NodeRenderer { constructor(renderer: Renderer, node: Node) { this.renderer = renderer - this.fill = new Circle(renderer.nodesContainer, renderer.circle, node.style?.color) - this.strokes = new NodeStrokes(this.renderer.nodesContainer, this.renderer.circle, this.fill) + this.fill = new Circle(renderer.nodesContainer, renderer.circle) + this.strokes = new CircleStrokes(renderer.nodesContainer, renderer.circle, this.fill) this.hitArea = new NodeHitArea(this.renderer.interactionContainer, this) + this.node = node this.update(node) } update(node: Node) { + this.node = node + + this.fill.update(node.style?.color) + this.strokes.update(node.style?.stroke) + if (this.label) { if (node.label === undefined || node.label.trim() === '') { this.managers.labels.delete(this.label) @@ -67,9 +74,10 @@ export class NodeRenderer { const x = node.x ?? 0 const y = node.y ?? 0 + const radius = node.radius const xChanged = x !== this.x const yChanged = y !== this.y - const radiusChanged = node.radius !== this.node?.radius + const radiusChanged = radius !== this.radius if ( (xChanged || yChanged || radiusChanged) && @@ -84,54 +92,56 @@ export class NodeRenderer { this.interpolateY = interpolate(this.y, y, this.renderer.animateNodePosition) } if (radiusChanged && this.renderer.animateNodeRadius) { - this.interpolateRadius = interpolate(this.y, y, this.renderer.animateNodeRadius) + this.interpolateRadius = interpolate(this.radius, radius, this.renderer.animateNodeRadius) } } else { - this.setPosition(node, x, y, node.radius) + this.resize(radius).moveTo(x, y) this.interpolateX = undefined this.interpolateY = undefined this.interpolateRadius = undefined } - this.node = node - return this } render(dt: number) { - let _x: number | undefined - let _y: number | undefined - let _radius: number | undefined + if (this.interpolateRadius) { + const { value, done } = this.interpolateRadius(dt) - if (this.interpolateX) { - const { value, done } = this.interpolateX(dt) - _x = value + this.resize(value) if (done) { - this.interpolateX = undefined + this.interpolateRadius = undefined + } + + if (this.label && !this.interpolateX && !this.interpolateY) { + this.label.moveTo(this.x, this.y) } } - if (this.interpolateY) { - const { value, done } = this.interpolateY(dt) - _y = value + if (this.interpolateX || this.interpolateY) { + let x = this.x + let y = this.y - if (done) { - this.interpolateY = undefined + if (this.interpolateX) { + const { value, done } = this.interpolateX(dt) + x = value + + if (done) { + this.interpolateX = undefined + } } - } - if (this.interpolateRadius) { - const { value, done } = this.interpolateRadius(dt) - _radius = value + if (this.interpolateY) { + const { value, done } = this.interpolateY(dt) + y = value - if (done) { - this.interpolateRadius = undefined + if (done) { + this.interpolateY = undefined + } } - } - if (_x !== undefined || _y !== undefined || _radius !== undefined) { - this.setPosition(this.node, _x ?? this.x, _y ?? this.y, _radius ?? this.node.radius) + this.moveTo(x, y) } const isVisible = this.visible() @@ -487,20 +497,34 @@ export class NodeRenderer { this.doubleClick = false } - private setPosition(node: Node, x: number, y: number, radius: number) { - this.x = x - this.y = y + private resize(radius: number) { + if (radius !== this.radius) { + this.radius = radius + this.fill.resize(radius) + this.strokes.resize(radius) - this.fill.moveTo(this.x, this.y).resize(radius) - this.strokes.update(this.x, this.y, radius, node.style) + if (this.label) { + this.label.offset = this.strokes.radius + } - if (this.label) { - this.label.offset = this.strokes.radius - this.label.moveTo(this.x, this.y) + this.hitArea.update(this.x, this.y, radius) } - this.icon?.moveTo(this.x, this.y) - this.hitArea.update(this.x, this.y, radius) + return this + } + + private moveTo(x: number, y: number) { + if (x !== this.x || y !== this.y) { + this.x = x + this.y = y + this.fill.moveTo(x, y) + this.strokes.moveTo(x, y) + this.label?.moveTo(x, y) + this.icon?.moveTo(x, y) + this.hitArea.update(x, y, this.radius) + } + + return this } private visible() { diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts index 3595c4c..87b6e85 100644 --- a/src/renderers/webgl/objects/circle/Circle.ts +++ b/src/renderers/webgl/objects/circle/Circle.ts @@ -2,6 +2,7 @@ import { FillStyle, RenderObject } from '../../../../types' import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' import { Container, Sprite } from 'pixi.js' import CircleTexture from '../../textures/CircleTexture' +import { isNumber } from '../../../../utils/helpers' export default class Circle implements RenderObject { mounted = false @@ -16,21 +17,14 @@ export default class Circle implements RenderObject { constructor( private container: Container, private texture: CircleTexture, - color?: string, - opacity?: number + index?: number ) { this.container = container this.texture = texture - - this.style = { - color: color ?? this.style.color, - opacity: opacity ?? this.style.opacity - } - - this.object = this.create() + this.object = this.create(index) } - update(color = this.style.color, opacity = this.style.opacity) { + update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) { if (color !== this.style.color) { this.style.color = color this.object.tint = color @@ -110,15 +104,22 @@ export default class Circle implements RenderObject { return this._radius } - private create() { + private create(index?: number) { const object = new Sprite(this.texture.get()) + object.anchor.set(0.5) object.x = this._x object.y = this._y object.visible = this.mounted object.tint = this.style.color object.alpha = this.style.opacity - this.container.addChild(object) + + if (isNumber(index)) { + this.container.addChildAt(object, index) + } else { + this.container.addChild(object) + } + return object } } diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts index 6ba9a53..2021375 100644 --- a/src/renderers/webgl/objects/circle/CircleStrokes.ts +++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts @@ -1,75 +1,82 @@ -import { FillStyle, RenderObject } from '../../../../types' -import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' -import { Container, Sprite } from 'pixi.js' +import { RenderObject, Stroke } from '../../../../types' +import { Container } from 'pixi.js' +import { equals } from '../../../../utils/api' import CircleTexture from '../../textures/CircleTexture' +import Circle from './Circle' export default class CircleStrokes implements RenderObject { mounted = false - private _x = 0 - private _y = 0 - private _radius = 0 - - private object: Sprite - private style: Required = DEFAULT_FILL_STYLE + private x = 0 + private y = 0 + private minRadius = 0 + private maxRadius = 0 + private objects: Circle[] = [] + private strokes: Stroke[] = [] constructor( private container: Container, private texture: CircleTexture, - style?: FillStyle + private fill: Circle ) { this.container = container this.texture = texture - - if (style) { - this.style = { ...DEFAULT_FILL_STYLE, ...style } - } - - this.object = this.create() + this.fill = fill + this.minRadius = fill.radius + this.maxRadius = fill.radius } - update(color = this.style.color, opacity = this.style.opacity) { - if (color !== this.style.color) { - this.style.color = color - this.object.tint = color - } + update(strokes: Stroke[] = []) { + if (!equals(this.strokes, strokes)) { + const isMounted = this.mounted - if (opacity !== this.style.opacity) { - this.style.opacity = opacity - this.object.alpha = opacity + this.delete() + this.applyStrokes(strokes) + + if (isMounted) { + this.mount() + } } return this } moveTo(x: number, y: number) { - if (x !== this.x) { - this._x = x - this.object.x = x - } + const dirty = x !== this.x || y !== this.y + + if (dirty) { + this.x = x + this.y = y - if (y !== this.y) { - this._y = y - this.object.y = y + for (const object of this.objects) { + object.moveTo(x, y) + } } return this } resize(radius: number) { - if (this._radius !== radius) { - this._radius = radius - this.object.scale.set(radius / this.texture.scaleFactor) + if (radius !== this.minRadius) { + this.minRadius = radius + this.maxRadius = radius + + for (let i = 0; i < this.strokes.length; i += 1) { + this.maxRadius += this.strokes[i].width + this.objects[i].resize(this.maxRadius) + } } return this } mount() { - // TODO - why is mounting/unmouting fill Sprite less efficient? if (!this.mounted) { this.mounted = true - this.object.visible = true + + for (const object of this.objects) { + object.mount() + } } return this @@ -78,41 +85,46 @@ export default class CircleStrokes implements RenderObject { unmount() { if (this.mounted) { this.mounted = false - this.object.visible = false + + for (const object of this.objects) { + object.unmount() + } } return this } delete() { - this.unmount() - this.container.removeChild(this.object) - this.object.destroy() + this.mounted = false - return undefined - } + for (const object of this.objects) { + object.delete() + } - get x() { - return this._x - } + this.strokes = [] + this.objects = [] + this.maxRadius = 0 - get y() { - return this._y + return undefined } get radius() { - return this._radius + return this.maxRadius } - private create() { - const object = new Sprite(this.texture.get()) - object.anchor.set(0.5) - object.x = this._x - object.y = this._y - object.visible = this.mounted - object.tint = this.style.color - object.alpha = this.style.opacity - this.container.addChild(object) - return object + private applyStrokes(strokes: Stroke[]) { + this.objects = [] + this.strokes = strokes + this.maxRadius = this.minRadius + + const index = this.fill.getContainerIndex() + + for (const { color, width } of strokes) { + this.maxRadius += width + const object = new Circle(this.container, this.texture, index) + this.objects.push(object.update(color).resize(this.maxRadius).moveTo(this.x, this.y)) + } + + return this } } From a821fcbda65fbbf2b89cbadfe171fde672af09e4 Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Tue, 13 Feb 2024 13:48:16 -0500 Subject: [PATCH 04/12] geometry objects: Rectangle and RectangleStrokes --- src/renderers/webgl/objects/circle/Circle.ts | 24 ++-- .../webgl/objects/circle/CircleStrokes.ts | 13 +- src/renderers/webgl/objects/nodeStrokes.ts | 105 -------------- .../webgl/objects/rectangle/Rectangle.ts | 110 ++++++++++++++- .../objects/rectangle/RectangleStrokes.ts | 129 +++++++++++++++++- 5 files changed, 242 insertions(+), 139 deletions(-) delete mode 100644 src/renderers/webgl/objects/nodeStrokes.ts diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts index 87b6e85..b79cb67 100644 --- a/src/renderers/webgl/objects/circle/Circle.ts +++ b/src/renderers/webgl/objects/circle/Circle.ts @@ -7,8 +7,8 @@ import { isNumber } from '../../../../utils/helpers' export default class Circle implements RenderObject { mounted = false - private _x = 0 - private _y = 0 + private x = 0 + private y = 0 private _radius = 0 private object: Sprite @@ -40,12 +40,12 @@ export default class Circle implements RenderObject { moveTo(x: number, y: number) { if (x !== this.x) { - this._x = x + this.x = x this.object.x = x } if (y !== this.y) { - this._y = y + this.y = y this.object.y = y } @@ -65,7 +65,7 @@ export default class Circle implements RenderObject { // TODO - why is mounting/unmouting fill Sprite less efficient? if (!this.mounted) { this.mounted = true - this.object.visible = true + this.object.visible = this.mounted } return this @@ -74,7 +74,7 @@ export default class Circle implements RenderObject { unmount() { if (this.mounted) { this.mounted = false - this.object.visible = false + this.object.visible = this.mounted } return this @@ -92,14 +92,6 @@ export default class Circle implements RenderObject { return this.container.getChildIndex(this.object) } - get x() { - return this._x - } - - get y() { - return this._y - } - get radius() { return this._radius } @@ -108,8 +100,8 @@ export default class Circle implements RenderObject { const object = new Sprite(this.texture.get()) object.anchor.set(0.5) - object.x = this._x - object.y = this._y + object.x = this.x + object.y = this.y object.visible = this.mounted object.tint = this.style.color object.alpha = this.style.opacity diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts index 2021375..c4da0f1 100644 --- a/src/renderers/webgl/objects/circle/CircleStrokes.ts +++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts @@ -62,8 +62,7 @@ export default class CircleStrokes implements RenderObject { this.maxRadius = radius for (let i = 0; i < this.strokes.length; i += 1) { - this.maxRadius += this.strokes[i].width - this.objects[i].resize(this.maxRadius) + this.objects[i].resize(this.increment(this.strokes[i].width)) } } @@ -103,7 +102,7 @@ export default class CircleStrokes implements RenderObject { this.strokes = [] this.objects = [] - this.maxRadius = 0 + this.maxRadius = this.minRadius return undefined } @@ -112,6 +111,11 @@ export default class CircleStrokes implements RenderObject { return this.maxRadius } + private increment(width: number) { + this.maxRadius += width + return this.maxRadius + } + private applyStrokes(strokes: Stroke[]) { this.objects = [] this.strokes = strokes @@ -120,9 +124,8 @@ export default class CircleStrokes implements RenderObject { const index = this.fill.getContainerIndex() for (const { color, width } of strokes) { - this.maxRadius += width const object = new Circle(this.container, this.texture, index) - this.objects.push(object.update(color).resize(this.maxRadius).moveTo(this.x, this.y)) + this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y)) } return this diff --git a/src/renderers/webgl/objects/nodeStrokes.ts b/src/renderers/webgl/objects/nodeStrokes.ts deleted file mode 100644 index cd6968e..0000000 --- a/src/renderers/webgl/objects/nodeStrokes.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Container, Sprite } from 'pixi.js' -import CircleTexture from '../textures/CircleTexture' -import type { NodeStyle } from '../../../types' - -export class NodeStrokes { - mounted = false - radius = 0 - sprites?: Sprite[] // TODO - make private - - private style?: NodeStyle - - constructor( - private container: Container, - private circleTexture: CircleTexture, - private fill: { getContainerIndex: () => number } - ) { - this.container = container - this.circleTexture = circleTexture - this.fill = fill - } - - update(x: number, y: number, radius: number, style?: NodeStyle) { - if (style?.stroke !== this.style?.stroke) { - // exit - const isMounted = this.mounted - this.delete() - - if (style?.stroke?.length) { - // enter - - this.sprites = Array(style.stroke.length) - - this.radius = radius - - for (let i = 0; i < style.stroke.length; i++) { - this.radius += style.stroke[i].width - const circle = new Sprite(this.circleTexture.get()) - circle.anchor.set(0.5) - circle.scale.set(this.radius / this.circleTexture.scaleFactor) - circle.tint = style.stroke[i].color - circle.x = x - circle.y = y - this.sprites[i] = circle - } - if (isMounted) { - this.mount() - } - } - } else if (this.sprites && this.style?.stroke) { - // reposition - this.radius = radius - - for (let i = 0; i < this.sprites.length; i++) { - this.radius += this.style.stroke[i].width - const scale = this.radius / this.circleTexture.scaleFactor - if (scale !== this.sprites[i].scale.x) { - this.sprites[i].scale.set(this.radius / this.circleTexture.scaleFactor) - } - this.sprites[i].x = x - this.sprites[i].y = y - } - } - - this.style = style - - return this - } - - mount() { - if (!this.mounted && this.sprites) { - const strokeContainerIndex = this.fill.getContainerIndex() - - for (let i = this.sprites.length - 1; i >= 0; i--) { - this.container.addChildAt(this.sprites[i], strokeContainerIndex) - } - this.mounted = true - } - - return this - } - - unmount() { - if (this.mounted && this.sprites) { - for (let i = this.sprites.length - 1; i >= 0; i--) { - this.container.removeChild(this.sprites[i]) - } - this.mounted = false - } - - return this - } - - delete() { - this.radius = 0 - this.unmount() - - if (this.sprites) { - for (const stroke of this.sprites) { - stroke.destroy() - } - } - - return undefined - } -} diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts index 089d210..ddf2474 100644 --- a/src/renderers/webgl/objects/rectangle/Rectangle.ts +++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts @@ -1,20 +1,118 @@ -import { RenderObject } from '../../../../types' +import { Dimensions, FillStyle, RenderObject } from '../../../../types' +import { Container, Sprite } from 'pixi.js' +import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' +import { isNumber } from '../../../../utils/helpers' +import RectangleTexture from '../../textures/RectangleTexture' export default class Rectangle implements RenderObject { mounted = false - moveTo(): this { + private x = 0 + private y = 0 + private width = 0 + private height = 0 + private object: Sprite + private style: Required = DEFAULT_FILL_STYLE + + constructor( + private container: Container, + private texture: RectangleTexture, + index?: number + ) { + this.container = container + this.texture = texture + this.object = this.create(index) + } + + update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) { + if (color !== this.style.color) { + this.style.color = color + this.object.tint = color + } + + if (opacity !== this.style.opacity) { + this.style.opacity = opacity + this.object.alpha = opacity + } + return this } + + moveTo(x: number, y: number) { + if (x !== this.x) { + this.x = x + this.object.x = x + } + + if (y !== this.y) { + this.y = y + this.object.y = y + } + + return this + } + + resize({ width, height }: Dimensions) { + if (this.width !== width || this.height !== height) { + this.width = width + this.height = height + this.object.scale.set(width / this.texture.scaleFactor, height / this.texture.scaleFactor) + } + + return this + } + mount() { - this.mounted = true + // TODO - why is mounting/unmouting fill Sprite less efficient? + if (!this.mounted) { + this.mounted = true + this.object.visible = this.mounted + } + return this } - unmount(): this { - this.mounted = false + + unmount() { + if (this.mounted) { + this.mounted = false + this.object.visible = this.mounted + } + return this } - delete(): void { + + delete() { this.unmount() + this.container.removeChild(this.object) + this.object.destroy() + + return undefined + } + + getContainerIndex() { + return this.container.getChildIndex(this.object) + } + + get size(): Dimensions { + return { width: this.width, height: this.height } + } + + private create(index?: number) { + const object = new Sprite(this.texture.get()) + + object.anchor.set(0.5) + object.x = this.x + object.y = this.y + object.visible = this.mounted + object.tint = this.style.color + object.alpha = this.style.opacity + + if (isNumber(index)) { + this.container.addChildAt(object, index) + } else { + this.container.addChild(object) + } + + return object } } diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts index 7c0bb01..5113ec9 100644 --- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts +++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts @@ -1,20 +1,135 @@ -import { RenderObject } from '../../../../types' +import { Dimensions, RenderObject, Stroke } from '../../../../types' +import { Container } from 'pixi.js' +import { equals } from '../../../../utils/api' +import RectangleTexture from '../../textures/RectangleTexture' +import Rectangle from './Rectangle' export default class RectangleStrokes implements RenderObject { mounted = false - moveTo(): this { + private x = 0 + private y = 0 + + private minSize: Dimensions = { width: 0, height: 0 } + private maxSize: Dimensions = { width: 0, height: 0 } + private objects: Rectangle[] = [] + private strokes: Stroke[] = [] + + constructor( + private container: Container, + private texture: RectangleTexture, + private fill: Rectangle + ) { + this.container = container + this.texture = texture + this.fill = fill + this.minSize = this.fill.size + this.maxSize = this.minSize + } + + update(strokes: Stroke[] = []) { + if (!equals(this.strokes, strokes)) { + const isMounted = this.mounted + + this.delete() + this.applyStrokes(strokes) + + if (isMounted) { + this.mount() + } + } + + return this + } + + moveTo(x: number, y: number) { + const dirty = x !== this.x || y !== this.y + + if (dirty) { + this.x = x + this.y = y + + for (const object of this.objects) { + object.moveTo(x, y) + } + } + + return this + } + + resize(width: number, height: number) { + if (width !== this.minSize.width || height !== this.minSize.height) { + this.minSize = { width, height } + this.maxSize = this.minSize + + for (let i = 0; i < this.strokes.length; i += 1) { + this.objects[i].resize(this.increment(this.strokes[i].width)) + } + } + return this } + mount() { - this.mounted = true + if (!this.mounted) { + this.mounted = true + + for (const object of this.objects) { + object.mount() + } + } + return this } - unmount(): this { - this.mounted = false + + unmount() { + if (this.mounted) { + this.mounted = false + + for (const object of this.objects) { + object.unmount() + } + } + return this } - delete(): void { - this.unmount() + + delete() { + this.mounted = false + + for (const object of this.objects) { + object.delete() + } + + this.strokes = [] + this.objects = [] + this.maxSize = this.minSize + + return undefined + } + + get size() { + return this.maxSize + } + + private increment(value: number) { + this.maxSize.height += value + this.maxSize.width += value + return this.maxSize + } + + private applyStrokes(strokes: Stroke[]) { + this.objects = [] + this.strokes = strokes + this.maxSize = this.minSize + + const index = this.fill.getContainerIndex() + + for (const { color, width } of strokes) { + const object = new Rectangle(this.container, this.texture, index) + this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y)) + } + + return this } } From 7aadaf0adbc2fc25dd389f4e4d24bcba9c4d769b Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Tue, 13 Feb 2024 15:38:34 -0500 Subject: [PATCH 05/12] LineSegment, LineStrokes for edges --- examples/native/src/labels/index.ts | 55 +++++-- examples/native/src/perf/index.ts | 4 +- src/renderers/webgl/LifecycleManager.ts | 9 +- src/renderers/webgl/edge.ts | 97 ++++++------ src/renderers/webgl/node.ts | 2 +- src/renderers/webgl/objects/arrow.ts | 81 +++++++--- src/renderers/webgl/objects/circle/Circle.ts | 17 +- .../webgl/objects/circle/CircleStrokes.ts | 4 +- .../webgl/objects/line/LineSegment.ts | 109 +++++++++++++ .../webgl/objects/line/LineStrokes.ts | 149 ++++++++++++++++++ src/renderers/webgl/objects/lineSegment.ts | 53 ------- .../webgl/objects/rectangle/Rectangle.ts | 16 +- .../objects/rectangle/RectangleStrokes.ts | 4 +- src/renderers/webgl/objects/text/Text.ts | 15 +- .../webgl/objects/text/TextHighlight.ts | 13 +- src/renderers/webgl/textures/TextTexture.ts | 3 +- src/renderers/webgl/utils.ts | 6 +- src/types/api.ts | 11 +- src/types/internal.ts | 2 + src/utils/constants.ts | 23 ++- 20 files changed, 475 insertions(+), 198 deletions(-) create mode 100644 src/renderers/webgl/objects/line/LineSegment.ts create mode 100644 src/renderers/webgl/objects/line/LineStrokes.ts delete mode 100644 src/renderers/webgl/objects/lineSegment.ts diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts index 87a0dc5..51f89f6 100644 --- a/examples/native/src/labels/index.ts +++ b/examples/native/src/labels/index.ts @@ -57,6 +57,28 @@ const NODE_HOVER_STYLE: Graph.NodeStyle = { } } +const EDGE_STYLE: Graph.EdgeStyle = { + arrow: 'both', + label: { + fontName: 'EdgeLabel', + fontFamily: 'Roboto', + fontSize: 10, + color: DARK_GREEN, + margin: 4 + } +} +const EDGE_HOVER_STYLE: Graph.EdgeStyle = { + arrow: 'both', + stroke: [{ width: 1, color: DARK_GREEN }], + label: { + fontName: 'EdgeLabel', + fontFamily: 'Roboto', + fontSize: 10, + color: DARK_GREEN, + margin: 4 + } +} + const data = [ 'Myriel', 'Napoleon', @@ -74,22 +96,13 @@ const data = [ const collide = Collide.Layout() -const edges: Graph.Edge[] = [ +let edges: Graph.Edge[] = [ { id: '0::1', source: '0', target: '1', label: '0 <- EDGE LABEL -> 1', - style: { - arrow: 'both', - label: { - fontName: 'EdgeLabel', - fontFamily: 'Roboto', - fontSize: 10, - color: DARK_GREEN, - margin: 4 - } - } + style: EDGE_STYLE } ] @@ -143,6 +156,26 @@ const options: Renderer.Options = { nodes = nodes.map((node) => node.id === event.target.id ? { ...node, radius: 10, label: node.label?.slice(0, node.label.length - 3), style: NODE_STYLE } : node ) + renderer.update({ nodes, edges, options }) + }, + onEdgePointerEnter: (event: Renderer.EdgePointerEvent) => { + edges = edges.map((edge) => { + if (edge.id === event.target.id) { + return { ...edge, style: EDGE_HOVER_STYLE } + } + return edge + }) + + renderer.update({ nodes, edges, options }) + }, + onEdgePointerLeave: (event: Renderer.EdgePointerEvent) => { + edges = edges.map((edge) => { + if (edge.id === event.target.id) { + return { ...edge, style: EDGE_STYLE } + } + return edge + }) + renderer.update({ nodes, edges, options }) } } diff --git a/examples/native/src/perf/index.ts b/examples/native/src/perf/index.ts index 1555127..c56cb85 100644 --- a/examples/native/src/perf/index.ts +++ b/examples/native/src/perf/index.ts @@ -53,13 +53,13 @@ const NODE_HOVER_STYLE: Graph.NodeStyle = { const EDGE_STYLE: Graph.EdgeStyle = { width: 1, - stroke: '#aaa', + color: '#aaa', arrow: 'reverse' } const EDGE_HOVER_STYLE: Graph.EdgeStyle = { width: 2, - stroke: '#f66', + color: '#f66', arrow: 'reverse' } diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts index 2cdce0d..6776b1b 100644 --- a/src/renderers/webgl/LifecycleManager.ts +++ b/src/renderers/webgl/LifecycleManager.ts @@ -1,15 +1,16 @@ -import { LineSegment } from './objects/lineSegment' -import { Arrow } from './objects/arrow' +import LineSegment from './objects/line/LineSegment' +import LineStrokes from './objects/line/LineStrokes' import ObjectManager from './objects/ObjectManager' import CircleStrokes from './objects/circle/CircleStrokes' import Circle from './objects/circle/Circle' +import Arrow from './objects/Arrow' import Icon from './objects/Icon' import Text from './objects/text/Text' export default class LifecycleManager { - nodes = new ObjectManager(2000) + nodes = new ObjectManager(2000) + edges = new ObjectManager(2000) icons = new ObjectManager(1000) - edges = new ObjectManager(2000) arrows = new ObjectManager(1000) labels = new ObjectManager(2000) interactions = new ObjectManager(2000) diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts index 3a46e67..d6b6e2b 100644 --- a/src/renderers/webgl/edge.ts +++ b/src/renderers/webgl/edge.ts @@ -1,48 +1,49 @@ -import { DEFAULT_LABEL_STYLE, MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM, MIN_LABEL_ZOOM } from '../../utils/constants' +import { DEFAULT_ARROW, DEFAULT_LABEL_STYLE, MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM, MIN_LABEL_ZOOM } from '../../utils/constants' import { type Renderer } from '.' import { midPoint } from './utils' import { movePoint } from './utils' import { NodeRenderer } from './node' -import type { ArrowStyle, Edge } from '../../types' -import { Arrow } from './objects/arrow' -import { LineSegment } from './objects/lineSegment' +import type { ArrowStyle, Edge, PointTuple } from '../../types' +import Arrow from './objects/Arrow' +import LineSegment from './objects/line/LineSegment' +import LineStrokes from './objects/line/LineStrokes' import { FederatedPointerEvent } from 'pixi.js' import { EdgeHitArea } from './interaction/edgeHitArea' -import { angle } from '../../utils/api' +import { angle, distance } from '../../utils/api' import Text from './objects/text/Text' -const DEFAULT_EDGE_WIDTH = 1 -const DEFAULT_EDGE_COLOR = 0xaaaaaa -const DEFAULT_ARROW = 'none' - export class EdgeRenderer { edge!: Edge source!: NodeRenderer target!: NodeRenderer - label?: Text - renderer: Renderer - lineSegment: LineSegment - x0 = 0 - y0 = 0 - x1 = 0 - y1 = 0 - theta = 0 - center: [x: number, y: number] = [0, 0] - width?: number - stroke?: string | number - strokeOpacity?: number sourceRadius?: number targetRadius?: number + private x0 = 0 + private y0 = 0 + private x1 = 0 + private y1 = 0 + private length = 0 + private theta = 0 + private center: PointTuple = [0, 0] + private lineSegment: LineSegment + private strokes: LineStrokes private hitArea: EdgeHitArea + private label?: Text private forwardArrow?: Arrow private reverseArrow?: Arrow private doubleClickTimeout: NodeJS.Timeout | undefined private doubleClick = false - constructor(renderer: Renderer, edge: Edge, source: NodeRenderer, target: NodeRenderer) { + constructor( + private renderer: Renderer, + edge: Edge, + source: NodeRenderer, + target: NodeRenderer + ) { this.renderer = renderer this.lineSegment = new LineSegment(this.renderer.edgesContainer) + this.strokes = new LineStrokes(this.renderer.edgesContainer, this.lineSegment) this.hitArea = new EdgeHitArea(this.renderer.interactionContainer, this) this.update(edge, source, target) } @@ -52,6 +53,9 @@ export class EdgeRenderer { this.source = source this.target = target + this.lineSegment.update(edge.style?.color, edge.style?.width, edge.style?.opacity) + this.strokes.update(edge.style?.stroke) + const arrow = edge.style?.arrow ?? DEFAULT_ARROW if (arrow !== this.arrow) { @@ -74,6 +78,9 @@ export class EdgeRenderer { } } + this.forwardArrow?.update(edge.style?.color, edge.style?.opacity) + this.reverseArrow?.update(edge.style?.color, edge.style?.opacity) + if (this.label) { if (edge.label === undefined || edge.label.trim() === '') { this.managers.labels.delete(this.label) @@ -97,24 +104,14 @@ export class EdgeRenderer { const isVisible = this.visible(x0, y0, x1, y1) if (isVisible) { - const width = this.edge?.style?.width ?? DEFAULT_EDGE_WIDTH - const stroke = this.edge?.style?.stroke ?? DEFAULT_EDGE_COLOR - const strokeOpacity = this.edge?.style?.strokeOpacity ?? 1 - if ( x0 !== this.x0 || y0 !== this.y0 || x1 !== this.x1 || y1 !== this.y1 || sourceRadius !== this.sourceRadius || - targetRadius !== this.targetRadius || - width !== this.width || - stroke !== this.stroke || - strokeOpacity !== this.strokeOpacity + targetRadius !== this.targetRadius ) { - this.width = width - this.stroke = stroke - this.strokeOpacity = strokeOpacity this.sourceRadius = sourceRadius this.targetRadius = targetRadius this.x0 = x0 @@ -131,8 +128,8 @@ export class EdgeRenderer { const edgePoint = movePoint(x1, y1, this.theta, this.targetRadius + this.forwardArrow.height) edgeX1 = edgePoint[0] edgeY1 = edgePoint[1] - const [arrowX1, arrowY1] = movePoint(x1, y1, this.theta, this.targetRadius) - this.forwardArrow.update(arrowX1, arrowY1, this.theta, this.stroke, this.strokeOpacity) + + this.forwardArrow.rotate(this.theta).moveTo(...movePoint(x1, y1, this.theta, this.targetRadius)) } else { const edgePoint = movePoint(x1, y1, this.theta, this.targetRadius) edgeX1 = edgePoint[0] @@ -143,8 +140,8 @@ export class EdgeRenderer { const edgePoint = movePoint(x0, y0, this.theta, -this.sourceRadius - this.reverseArrow.height) edgeX0 = edgePoint[0] edgeY0 = edgePoint[1] - const [arrowX0, arrowY0] = movePoint(x0, y0, this.theta, -this.sourceRadius) - this.reverseArrow.update(arrowX0, arrowY0, this.theta + Math.PI, this.stroke, this.strokeOpacity) + + this.reverseArrow.rotate(this.theta + Math.PI).moveTo(...movePoint(x0, y0, this.theta, -this.sourceRadius)) } else { const edgePoint = movePoint(x0, y0, this.theta, -this.sourceRadius) edgeX0 = edgePoint[0] @@ -152,15 +149,17 @@ export class EdgeRenderer { } this.center = midPoint(edgeX0, edgeY0, edgeX1, edgeY1) + this.length = distance(edgeX0, edgeY0, edgeX1, edgeY1) + + // TODO -> draw hitArea/strokes over arrows + this.lineSegment.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0) + this.strokes.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0) + this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.strokes.width, this.theta) if (this.label) { - this.label.rotation = this.theta - this.label.moveTo(...this.center) + this.label.offset = this.strokes.width + this.label.rotate(this.theta).moveTo(...this.center) } - - // TODO - draw hitArea over arrow - this.lineSegment.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.stroke, this.strokeOpacity) - this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.theta) } } @@ -181,6 +180,13 @@ export class EdgeRenderer { this.managers.edges.unmount(this.lineSegment) } + const strokesMounted = this.managers.edges.isMounted(this.strokes) + if (isVisible && !strokesMounted) { + this.managers.edges.mount(this.strokes) + } else if (!isVisible && strokesMounted) { + this.managers.edges.unmount(this.strokes) + } + if (this.forwardArrow) { const forwardArrowMounted = this.managers.arrows.isMounted(this.forwardArrow) if (isVisible && !forwardArrowMounted) { @@ -219,6 +225,7 @@ export class EdgeRenderer { clearTimeout(this.doubleClickTimeout) this.managers.edges.delete(this.lineSegment) + this.managers.edges.delete(this.strokes) this.managers.interactions.delete(this.hitArea) if (this.forwardArrow) { this.managers.arrows.delete(this.forwardArrow) @@ -428,8 +435,8 @@ export class EdgeRenderer { const style = this.edge.style?.label if (label !== undefined && label.trim() !== '' && this.label === undefined) { this.label = new Text(this.renderer.assets, this.renderer.labelsContainer, label, style, DEFAULT_LABEL_STYLE) - this.label.rotation = this.theta - this.label.moveTo(...this.center) + this.label.offset = this.strokes.width + this.label.rotate(this.theta).moveTo(...this.center) } return this diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index 5e2d189..2a72378 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -43,7 +43,7 @@ export class NodeRenderer { update(node: Node) { this.node = node - this.fill.update(node.style?.color) + this.fill.update(node.style?.color, node.style?.opacity) this.strokes.update(node.style?.stroke) if (this.label) { diff --git a/src/renderers/webgl/objects/arrow.ts b/src/renderers/webgl/objects/arrow.ts index 3895ba3..bcf2db5 100644 --- a/src/renderers/webgl/objects/arrow.ts +++ b/src/renderers/webgl/objects/arrow.ts @@ -1,38 +1,54 @@ import { Container, Sprite } from 'pixi.js' +import { FillStyle, RenderObject } from '../../../types' import ArrowTexture from '../textures/ArrowTexture' +import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../utils/constants' -export class Arrow { +export default class Arrow implements RenderObject { mounted = false - height: number - width: number - private container: Container - private arrowTexture: ArrowTexture - private arrow: Sprite + private x = 0 + private y = 0 + private object: Sprite + private style: Required = DEFAULT_FILL_STYLE - constructor(container: Container, arrowTexture: ArrowTexture) { + constructor( + private container: Container, + private texture: ArrowTexture + ) { this.container = container - this.arrowTexture = arrowTexture - this.arrow = new Sprite(this.arrowTexture.get()) - this.height = this.arrowTexture.height - this.width = this.arrowTexture.width - this.arrow.anchor.set(0, 0.5) - this.arrow.scale.set(1 / this.arrowTexture.scaleFactor) + this.texture = texture + this.object = this.create() } - update(x: number, y: number, rotation: number, color: string | number, opacity: number) { - this.arrow.x = x - this.arrow.y = y - this.arrow.rotation = rotation - this.arrow.tint = color - this.arrow.alpha = opacity + update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) { + this.style = { color, opacity } + this.object.tint = this.style.color + this.object.alpha = this.style.opacity + return this + } + + moveTo(x: number, y: number) { + if (x !== this.x) { + this.x = x + this.object.x = x + } + + if (y !== this.y) { + this.y = y + this.object.y = y + } + + return this + } + rotate(angle: number) { + this.object.rotation = angle return this } mount() { if (!this.mounted) { - this.container.addChild(this.arrow) + this.container.addChild(this.object) this.mounted = true } @@ -41,7 +57,7 @@ export class Arrow { unmount() { if (this.mounted) { - this.container.removeChild(this.arrow) + this.container.removeChild(this.object) this.mounted = false } @@ -50,8 +66,29 @@ export class Arrow { delete() { this.unmount() - this.arrow.destroy() + this.object.destroy() return undefined } + + get width() { + return this.texture.width + } + + get height() { + return this.texture.height + } + + private get scale() { + return 1 / this.texture.scaleFactor + } + + private create() { + const object = new Sprite(this.texture.get()) + object.anchor.set(0, 0.5) + object.scale.set(this.scale) + object.tint = this.style.color + object.alpha = this.style.opacity + return object + } } diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts index b79cb67..2a6e974 100644 --- a/src/renderers/webgl/objects/circle/Circle.ts +++ b/src/renderers/webgl/objects/circle/Circle.ts @@ -1,5 +1,5 @@ import { FillStyle, RenderObject } from '../../../../types' -import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' +import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../../utils/constants' import { Container, Sprite } from 'pixi.js' import CircleTexture from '../../textures/CircleTexture' import { isNumber } from '../../../../utils/helpers' @@ -24,17 +24,10 @@ export default class Circle implements RenderObject { this.object = this.create(index) } - update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) { - if (color !== this.style.color) { - this.style.color = color - this.object.tint = color - } - - if (opacity !== this.style.opacity) { - this.style.opacity = opacity - this.object.alpha = opacity - } - + update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) { + this.style = { color, opacity } + this.object.tint = this.style.color + this.object.alpha = this.style.opacity return this } diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts index c4da0f1..a6e5983 100644 --- a/src/renderers/webgl/objects/circle/CircleStrokes.ts +++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts @@ -123,9 +123,9 @@ export default class CircleStrokes implements RenderObject { const index = this.fill.getContainerIndex() - for (const { color, width } of strokes) { + for (const { width, color, opacity } of strokes) { const object = new Circle(this.container, this.texture, index) - this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y)) + this.objects.push(object.update(color, opacity).resize(this.increment(width)).moveTo(this.x, this.y)) } return this diff --git a/src/renderers/webgl/objects/line/LineSegment.ts b/src/renderers/webgl/objects/line/LineSegment.ts new file mode 100644 index 0000000..c025fe0 --- /dev/null +++ b/src/renderers/webgl/objects/line/LineSegment.ts @@ -0,0 +1,109 @@ +import { Container, Sprite, Texture } from 'pixi.js' +import { Dimensions, RenderObject, Stroke } from '../../../../types' +import { DEFAULT_FILL, DEFAULT_OPACITY, DEFAULT_STROKE_STYLE, DEFAULT_STROKE_WIDTH, HALF_PI } from '../../../../utils/constants' +import { isNumber } from '../../../../utils/helpers' + +// TODO -> let LineSegment own arrow rendering +export default class LineSegment implements RenderObject { + mounted = false + + private x = 0 + private y = 0 + private length = 0 + private object: Sprite + private style: Required = DEFAULT_STROKE_STYLE + + constructor(private container: Container) { + this.container = container + this.object = this.create() + } + + update(color = DEFAULT_FILL, width = DEFAULT_STROKE_WIDTH, opacity = DEFAULT_OPACITY) { + this.style = { color, width, opacity } + this.object.tint = color + this.object.width = width + this.object.alpha = opacity + + return this + } + + rotate(angle: number) { + this.object.rotation = angle + HALF_PI + return this + } + + resize(length: number) { + if (length !== this.length) { + this.length = length + this.object.height = length + } + + return this + } + + moveTo(x: number, y: number) { + if (x !== this.x) { + this.x = x + this.object.x = x + } + if (y !== this.y) { + this.y = y + this.object.y = y + } + + return this + } + + mount(index?: number) { + if (!this.mounted) { + this.mounted = true + + if (isNumber(index)) { + this.container.addChildAt(this.object, index) + } else { + this.container.addChild(this.object) + } + } + + return this + } + + unmount() { + if (this.mounted) { + this.mounted = false + this.container.removeChild(this.object) + } + + return this + } + + delete() { + this.unmount() + this.object.destroy() + + return undefined + } + + getContainerIndex() { + if (this.mounted) { + return this.container.getChildIndex(this.object) + } + + return -1 + } + + get size(): Dimensions { + return { width: this.style.width, height: this.length } + } + + private create() { + const object = new Sprite(Texture.WHITE) + object.tint = this.style.color + object.alpha = this.style.opacity + object.width = this.style.width + object.height = this.length + object.anchor.set(0.5, 0) + + return object + } +} diff --git a/src/renderers/webgl/objects/line/LineStrokes.ts b/src/renderers/webgl/objects/line/LineStrokes.ts new file mode 100644 index 0000000..49dbfeb --- /dev/null +++ b/src/renderers/webgl/objects/line/LineStrokes.ts @@ -0,0 +1,149 @@ +import { RenderObject, Stroke } from '../../../../types' +import { Container } from 'pixi.js' +import { equals } from '../../../../utils/api' +import LineSegment from './LineSegment' + +export default class LineStrokes implements RenderObject { + mounted = false + + private x = 0 + private y = 0 + private length = 0 + private angle = 0 + private minWidth = 0 + private maxWidth = 0 + private objects: LineSegment[] = [] + private strokes: Stroke[] = [] + + constructor( + private container: Container, + private fill: LineSegment + ) { + this.container = container + this.fill = fill + this.minWidth = this.fill.size.width + this.maxWidth = this.minWidth + } + + update(strokes: Stroke[] = []) { + if (!equals(this.strokes, strokes)) { + const isMounted = this.mounted + + this.delete() + this.applyStrokes(strokes) + + if (isMounted) { + this.mount() + } + } else if (this.fill.size.width !== this.minWidth) { + this.minWidth = this.fill.size.width + this.maxWidth = this.minWidth + + for (let i = 0; i < this.strokes.length; i += 1) { + const { color, width, opacity } = this.strokes[i] + this.objects[i].update(color, this.increment(width), opacity) + } + } + + return this + } + + moveTo(x: number, y: number) { + const dirty = x !== this.x || y !== this.y + + if (dirty) { + this.x = x + this.y = y + + for (const object of this.objects) { + object.moveTo(x, y) + } + } + + return this + } + + resize(length: number) { + if (length !== this.length) { + this.length = length + + for (const object of this.objects) { + object.resize(length) + } + } + + return this + } + + rotate(angle: number) { + if (angle !== this.angle) { + this.angle = angle + for (const object of this.objects) { + object.rotate(angle) + } + } + return this + } + + mount() { + if (!this.mounted) { + this.mounted = true + + const index = this.fill.getContainerIndex() + for (const object of this.objects) { + object.mount(index) + } + } + + return this + } + + unmount() { + if (this.mounted) { + this.mounted = false + + for (const object of this.objects) { + object.unmount() + } + } + + return this + } + + delete() { + this.mounted = false + + for (const object of this.objects) { + object.delete() + } + + this.strokes = [] + this.objects = [] + this.maxWidth = this.minWidth + + return undefined + } + + get width() { + return this.maxWidth + } + + private increment(value: number) { + this.maxWidth += value + return this.maxWidth + } + + private applyStrokes(strokes: Stroke[]) { + this.objects = [] + this.strokes = strokes + this.minWidth = this.fill.size.width + this.maxWidth = this.minWidth + + for (const { color, width, opacity } of strokes) { + const object = new LineSegment(this.container) + this.objects.push(object.update(color, this.increment(width), opacity).rotate(this.angle).resize(this.length).moveTo(this.x, this.y)) + } + + return this + } +} diff --git a/src/renderers/webgl/objects/lineSegment.ts b/src/renderers/webgl/objects/lineSegment.ts deleted file mode 100644 index a8f5596..0000000 --- a/src/renderers/webgl/objects/lineSegment.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Container, Sprite, Texture } from 'pixi.js' -import { HALF_PI } from '../utils' -import { angle, distance } from '../../../utils/api' - -export class LineSegment { - mounted = false - - private container: Container - private lineSegment: Sprite - - constructor(container: Container) { - this.container = container - this.lineSegment = new Sprite(Texture.WHITE) - } - - update(x0: number, y0: number, x1: number, y1: number, width: number, color: string | number, opacity: number) { - this.lineSegment.x = x0 - this.lineSegment.y = y0 - this.lineSegment.width = width - this.lineSegment.height = distance(x0, y0, x1, y1) - this.lineSegment.rotation = angle(x0, y0, x1, y1) + HALF_PI - this.lineSegment.tint = color - this.lineSegment.alpha = opacity - this.lineSegment.anchor.set(0.5, 0) - - return this - } - - mount() { - if (!this.mounted) { - this.container.addChild(this.lineSegment) - this.mounted = true - } - - return this - } - - unmount() { - if (this.mounted) { - this.container.removeChild(this.lineSegment) - this.mounted = false - } - - return this - } - - delete() { - this.unmount() - this.lineSegment.destroy() - - return undefined - } -} diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts index ddf2474..4221982 100644 --- a/src/renderers/webgl/objects/rectangle/Rectangle.ts +++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts @@ -1,6 +1,6 @@ import { Dimensions, FillStyle, RenderObject } from '../../../../types' import { Container, Sprite } from 'pixi.js' -import { DEFAULT_FILL_STYLE } from '../../../../utils/constants' +import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../../utils/constants' import { isNumber } from '../../../../utils/helpers' import RectangleTexture from '../../textures/RectangleTexture' @@ -24,16 +24,10 @@ export default class Rectangle implements RenderObject { this.object = this.create(index) } - update(color = DEFAULT_FILL_STYLE.color, opacity = DEFAULT_FILL_STYLE.opacity) { - if (color !== this.style.color) { - this.style.color = color - this.object.tint = color - } - - if (opacity !== this.style.opacity) { - this.style.opacity = opacity - this.object.alpha = opacity - } + update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) { + this.style = { color, opacity } + this.object.tint = color + this.object.alpha = opacity return this } diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts index 5113ec9..69e67ec 100644 --- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts +++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts @@ -125,9 +125,9 @@ export default class RectangleStrokes implements RenderObject { const index = this.fill.getContainerIndex() - for (const { color, width } of strokes) { + for (const { width, color, opacity } of strokes) { const object = new Rectangle(this.container, this.texture, index) - this.objects.push(object.update(color).resize(this.increment(width)).moveTo(this.x, this.y)) + this.objects.push(object.update(color, opacity).resize(this.increment(width)).moveTo(this.x, this.y)) } return this diff --git a/src/renderers/webgl/objects/text/Text.ts b/src/renderers/webgl/objects/text/Text.ts index b523e39..8acff5e 100644 --- a/src/renderers/webgl/objects/text/Text.ts +++ b/src/renderers/webgl/objects/text/Text.ts @@ -1,4 +1,4 @@ -import type { Bounds, TextStyle, TextObject, FontWeight, RenderObject } from '../../../../types' +import type { Bounds, TextStyle, TextObject, FontWeight, RenderObject, PointTuple } from '../../../../types' import { BitmapText, Container, Text as PixiText } from 'pixi.js' import { equals } from '../../../../utils/api' import TextTexture, { TextTextureOptions } from '../../textures/TextTexture' @@ -169,15 +169,10 @@ export default class Text implements RenderObject { this.font = undefined } - get rotation() { - return this.object.rotation - } - - set rotation(rotation: number) { + rotate(rotation: number) { this.object.rotation = rotation - if (this.highlight) { - this.highlight.rotation = rotation - } + this.highlight?.rotate(rotation) + return this } get rect() { @@ -264,7 +259,7 @@ export default class Text implements RenderObject { return this } - private offsetPosition(x: number, y: number, offset: number): [x: number, y: number] { + private offsetPosition(x: number, y: number, offset: number): PointTuple { switch (this.style.position) { case 'bottom': return [x, y + offset] diff --git a/src/renderers/webgl/objects/text/TextHighlight.ts b/src/renderers/webgl/objects/text/TextHighlight.ts index d7ecba1..b5d6b33 100644 --- a/src/renderers/webgl/objects/text/TextHighlight.ts +++ b/src/renderers/webgl/objects/text/TextHighlight.ts @@ -84,6 +84,11 @@ export default class TextHighlight implements RenderObject { return undefined } + rotate(rotation: number) { + this.object.rotation = rotation + return this + } + get text() { return this.textObject } @@ -96,14 +101,6 @@ export default class TextHighlight implements RenderObject { return this.object.anchor } - get rotation() { - return this.object.rotation - } - - set rotation(rotation: number) { - this.object.rotation = rotation - } - private create() { const object = Sprite.from(Texture.WHITE) object.width = this.width diff --git a/src/renderers/webgl/textures/TextTexture.ts b/src/renderers/webgl/textures/TextTexture.ts index 9d09e9b..a19048e 100644 --- a/src/renderers/webgl/textures/TextTexture.ts +++ b/src/renderers/webgl/textures/TextTexture.ts @@ -3,6 +3,7 @@ import { TextStyle as PixiTextStyle, ITextStyle as IPixiTextStyle, BitmapFont, I import { DEFAULT_HIGHLIGHT_STYLE, DEFAULT_TEXT_STYLE, MIN_TEXTURE_ZOOM, DEFAULT_RESOLUTION } from '../../../utils/constants' import { isNumber } from '../../../utils/helpers' import { equals } from '../../../utils/api' +import { PointTuple } from '../../../types' export type TextTextureOptions = { defaultTextStyle?: Omit @@ -116,7 +117,7 @@ export default class TextTexture { } } - get anchor(): [x: number, y: number] { + get anchor(): PointTuple { switch (this.position) { case 'bottom': return [0.5, 0] diff --git a/src/renderers/webgl/utils.ts b/src/renderers/webgl/utils.ts index f96be92..412adaf 100644 --- a/src/renderers/webgl/utils.ts +++ b/src/renderers/webgl/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import type { Node } from '../../types' +import type { Node, PointTuple } from '../../types' export const HALF_PI = Math.PI / 2 @@ -19,11 +19,11 @@ export const logUnknownEdgeError = (source: Node | undefined, target: Node | und } } -export const movePoint = (x: number, y: number, angle: number, distance: number): [x: number, y: number] => [ +export const movePoint = (x: number, y: number, angle: number, distance: number): PointTuple => [ x + Math.cos(angle) * distance, y + Math.sin(angle) * distance ] -export const midPoint = (x0: number, y0: number, x1: number, y1: number): [x: number, y: number] => [(x0 + x1) / 2, (y0 + y1) / 2] +export const midPoint = (x0: number, y0: number, x1: number, y1: number): PointTuple => [(x0 + x1) / 2, (y0 + y1) / 2] export const length = (x0: number, y0: number, x1: number, y1: number) => Math.hypot(x1 - x0, y1 - y0) diff --git a/src/types/api.ts b/src/types/api.ts index 78aaae5..4adbed3 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -8,7 +8,7 @@ export type Viewport = { x: number; y: number; zoom: number } // style export type FillStyle = { color: string; opacity?: number } -export type Stroke = { color: string; width: number } +export type Stroke = FillStyle & { width: number } export type FontWeight = 'normal' | 'bold' | 'bolder' | 'lighter' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' @@ -58,8 +58,7 @@ export type TextIcon = IconBase<'textIcon'> & { export type IconStyle = ImageIcon | TextIcon // nodes -export type NodeStyle = { - color?: string +export type NodeStyle = Partial & { icon?: IconStyle stroke?: Stroke[] badge?: { @@ -96,10 +95,8 @@ export type EdgeLabelStyle = LabelStyle & { position?: Exclude } -export type EdgeStyle = { - width?: number - stroke?: string - strokeOpacity?: number +export type EdgeStyle = Partial & { + stroke?: Stroke[] arrow?: ArrowStyle label?: EdgeLabelStyle } diff --git a/src/types/internal.ts b/src/types/internal.ts index 71a61c1..cfc3fe1 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -6,6 +6,8 @@ export type Extend = { export type TextObject = PixiText | BitmapText +export type PointTuple = [x: number, y: number] + export type Texture = { get(...args: unknown[]): RenderTexture delete(): void diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 160cdda..75843c8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,3 +1,9 @@ +// math +export const HALF_PI = Math.PI / 2 +export const TWO_PI = Math.PI * 2 +export const THREE_HALF_PI = HALF_PI * 3 +export const RADIANS_PER_DEGREE = Math.PI / 180 + // zoom limits // TODO - extends to renderer options export const MIN_LABEL_ZOOM = 0.25 @@ -8,20 +14,29 @@ export const MIN_EDGES_ZOOM = 0.1 export const MIN_TEXTURE_ZOOM = 3 // style -export const DEFAULT_RESOLUTION = 2 -export const DEFAULT_OPACITY = 1 - export const COLORS = { BLACK: '#000000', WHITE: '#FFFFFF', GREY: '#AAAAAA' } +export const DEFAULT_RESOLUTION = 2 +export const DEFAULT_FILL = COLORS.GREY +export const DEFAULT_OPACITY = 1 +export const DEFAULT_STROKE_WIDTH = 1 +export const DEFAULT_ARROW = 'none' + export const DEFAULT_FILL_STYLE = { - color: COLORS.GREY, + color: DEFAULT_FILL, opacity: DEFAULT_OPACITY } +export const DEFAULT_STROKE_STYLE = { + color: DEFAULT_FILL, + opacity: DEFAULT_OPACITY, + width: DEFAULT_STROKE_WIDTH +} + export const DEFAULT_TEXT_STYLE = { margin: 2, fontSize: 10, From 3eb1d1e7669bba87e4515f2631567fea149ff3bc Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Wed, 14 Feb 2024 10:17:07 -0500 Subject: [PATCH 06/12] begin implementing AnnotationsRenderer --- examples/native/index.html | 3 + .../native/src/annotations/annotations.html | 14 ++ examples/native/src/annotations/index.ts | 79 +++++++ src/renderers/webgl/LifecycleManager.ts | 6 + .../webgl/annotations/AnnotationsRenderer.ts | 67 ++++++ .../annotations/CircleAnnotationRenderer.ts | 48 ++++ .../RectangleAnnotationRenderer.ts | 221 ++++++++++++++++++ src/renderers/webgl/edge.ts | 14 +- src/renderers/webgl/index.ts | 15 +- src/renderers/webgl/node.ts | 10 +- .../webgl/objects/rectangle/Rectangle.ts | 6 +- .../objects/rectangle/RectangleStrokes.ts | 16 +- src/renderers/webgl/textures/TextTexture.ts | 2 +- src/types/internal.ts | 2 + src/utils/constants.ts | 6 +- 15 files changed, 489 insertions(+), 20 deletions(-) create mode 100644 examples/native/src/annotations/annotations.html create mode 100644 examples/native/src/annotations/index.ts create mode 100644 src/renderers/webgl/annotations/AnnotationsRenderer.ts create mode 100644 src/renderers/webgl/annotations/CircleAnnotationRenderer.ts create mode 100644 src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts diff --git a/examples/native/index.html b/examples/native/index.html index fae189c..9cad059 100644 --- a/examples/native/index.html +++ b/examples/native/index.html @@ -16,6 +16,9 @@

Native Examples

  • Label Styles
  • +
  • + Annotations +
  • diff --git a/examples/native/src/annotations/annotations.html b/examples/native/src/annotations/annotations.html new file mode 100644 index 0000000..f9c4e1e --- /dev/null +++ b/examples/native/src/annotations/annotations.html @@ -0,0 +1,14 @@ + + + + + + + + Trellis Example: Annotations + + +
    + + + diff --git a/examples/native/src/annotations/index.ts b/examples/native/src/annotations/index.ts new file mode 100644 index 0000000..3e81416 --- /dev/null +++ b/examples/native/src/annotations/index.ts @@ -0,0 +1,79 @@ +import * as Renderer from '@trellis/renderers/webgl' +import * as Graph from '@trellis/index' + +const WHITE = '#FFF' +const GREEN = '#91AD49' +const GREEN_LIGHT = '#C6D336' +const DARK_GREEN = '#607330' + +const edges: Graph.Edge[] = [] +const nodes: Graph.Node[] = [] +const annotations: Graph.Annotation[] = [ + { + id: 'rect-anno-1', + type: 'rectangle', + width: 200, + height: 100, + x: 100, + y: 100, + style: { + color: GREEN, + stroke: [ + { width: 1, color: WHITE }, + { width: 2, color: GREEN_LIGHT } + ] + } + }, + { + id: 'text-anno-2', + type: 'text', + content: 'Hello, World!', + width: 200, + height: 100, + x: -200, + y: -100, + style: { + color: DARK_GREEN, + stroke: [ + { width: 1, color: WHITE }, + { width: 2, color: GREEN_LIGHT } + ], + text: { + fontSize: 14, + fontWeight: '400', + color: GREEN_LIGHT + } + } + } +] + +const container = document.querySelector('#graph') as HTMLDivElement + +const options: Renderer.Options = { + width: 1250, + height: 650, + x: 0, + y: 0, + zoom: 1, + minZoom: 0.025, + onViewportDrag: (event: Renderer.ViewportDragEvent | Renderer.ViewportDragDecelerateEvent) => { + // console.log('viewport drag', `x: ${event.dx}, y: ${event.dy}`) + options.x! += event.dx + options.y! += event.dy + renderer.update({ nodes, edges, annotations, options }) + }, + onViewportWheel: ({ dx, dy, dz }) => { + options.x! += dx + options.y! += dy + options.zoom! += dz + renderer.update({ nodes, edges, annotations, options }) + } +} + +const renderer = new Renderer.Renderer({ container, width: options.width, height: options.height, debug: true }).update({ + nodes, + edges, + annotations, + options +}) +;(window as any).renderer = renderer diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts index 6776b1b..32e0595 100644 --- a/src/renderers/webgl/LifecycleManager.ts +++ b/src/renderers/webgl/LifecycleManager.ts @@ -6,6 +6,8 @@ import Circle from './objects/circle/Circle' import Arrow from './objects/Arrow' import Icon from './objects/Icon' import Text from './objects/text/Text' +import RectangleStrokes from './objects/rectangle/RectangleStrokes' +import Rectangle from './objects/rectangle/Rectangle' export default class LifecycleManager { nodes = new ObjectManager(2000) @@ -14,6 +16,8 @@ export default class LifecycleManager { arrows = new ObjectManager(1000) labels = new ObjectManager(2000) interactions = new ObjectManager(2000) + annotations = new ObjectManager(2000) + text = new ObjectManager(1000) // interactions = new ObjectManager(2000) // TODO render() { @@ -23,5 +27,7 @@ export default class LifecycleManager { this.arrows.render() this.labels.render() this.interactions.render() + this.annotations.render() + this.text.render() } } diff --git a/src/renderers/webgl/annotations/AnnotationsRenderer.ts b/src/renderers/webgl/annotations/AnnotationsRenderer.ts new file mode 100644 index 0000000..eb782cb --- /dev/null +++ b/src/renderers/webgl/annotations/AnnotationsRenderer.ts @@ -0,0 +1,67 @@ +import { Renderer } from '..' +import { Annotation } from '../../../types' +import CircleAnnotationRenderer from './CircleAnnotationRenderer' +import RectangleAnnotationRenderer from './RectangleAnnotationRenderer' + +type AnnotationRendererLookup = Record + +export default class AnnotationsRenderer { + private annotations: Annotation[] = [] + private lookup: AnnotationRendererLookup = {} + + constructor(private renderer: Renderer) { + this.renderer = renderer + } + + update(annotations: Annotation[] = []) { + if (this.annotations === annotations || (this.annotations.length === 0 && annotations.length === 0)) { + return this + } + + const lookup: AnnotationRendererLookup = {} + + for (const annotation of annotations) { + const renderer = this.lookup[annotation.id] + + if (renderer === undefined) { + if (annotation.type === 'circle') { + lookup[annotation.id] = new CircleAnnotationRenderer(this.renderer, annotation) + } else { + lookup[annotation.id] = new RectangleAnnotationRenderer(this.renderer, annotation) + } + } else if (annotation.type === 'circle') { + if (renderer instanceof CircleAnnotationRenderer) { + renderer.update(annotation) + } else { + renderer.delete() + lookup[annotation.id] = new CircleAnnotationRenderer(this.renderer, annotation) + } + } else if (renderer instanceof RectangleAnnotationRenderer) { + renderer.update(annotation) + } else { + renderer.delete() + lookup[annotation.id] = new RectangleAnnotationRenderer(this.renderer, annotation) + } + } + + for (const annotation of this.annotations) { + if (lookup[annotation.id] === undefined && this.lookup[annotation.id]) { + // exit + this.lookup[annotation.id].delete() + } + } + + this.lookup = lookup + this.annotations = annotations + + return this + } + + render(dt: number) { + for (const id in this.lookup) { + this.lookup[id].render(dt) + } + + return this + } +} diff --git a/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts b/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts new file mode 100644 index 0000000..6e5c213 --- /dev/null +++ b/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Renderer } from '..' +import { CircleAnnotation } from '../../../types' + +export default class CircleAnnotationRenderer { + annotation: CircleAnnotation + + private x = 0 + private y = 0 + private radius = 0 + + constructor( + private renderer: Renderer, + annotation: CircleAnnotation + ) { + this.renderer = renderer + this.annotation = annotation + this.resize(annotation.radius).moveTo(annotation.x, annotation.y) + } + + update(annotation: CircleAnnotation) { + this.annotation = annotation + return this + } + + resize(radius: number) { + this.radius = radius + return this + } + + moveTo(x: number, y: number) { + this.x = x + this.y = y + return this + } + + render(dt: number) { + return this + } + + delete() { + return undefined + } + + private visible() { + return false + } +} diff --git a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts new file mode 100644 index 0000000..52183e8 --- /dev/null +++ b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts @@ -0,0 +1,221 @@ +import { InterpolateFn, RectangleAnnotation, TextAnnotation } from '../../../types' +import { DEFAULT_ANIMATE_RESIZE, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' +import { interpolate } from '../../../utils/helpers' +import { Renderer } from '..' +import RectangleStrokes from '../objects/rectangle/RectangleStrokes' +import Rectangle from '../objects/rectangle/Rectangle' +import Text from '../objects/text/Text' + +export default class RectangleAnnotationRenderer { + annotation: TextAnnotation | RectangleAnnotation + + private x = 0 + private y = 0 + private width = 0 + private height = 0 + + private fill: Rectangle + private strokes: RectangleStrokes + private text?: Text + + private interpolateX?: InterpolateFn + private interpolateY?: InterpolateFn + private interpolateWidth?: InterpolateFn + private interpolateHeight?: InterpolateFn + + constructor( + private renderer: Renderer, + annotation: TextAnnotation | RectangleAnnotation + ) { + this.renderer = renderer + this.fill = new Rectangle(renderer.annotationsContainer, renderer.rectangle, annotation.style) + this.strokes = new RectangleStrokes(renderer.annotationsContainer, renderer.rectangle, this.fill, annotation.style.stroke) + this.resize(annotation.width, annotation.height).moveTo(annotation.x, annotation.y) + this.annotation = annotation + } + + update(annotation: TextAnnotation | RectangleAnnotation) { + this.annotation = annotation + this.fill.update(annotation.style.color, annotation.style.opacity) + this.strokes.update(annotation.style.stroke) + + if (annotation.type === 'text' && this.text) { + this.text.update(annotation.content, annotation.style.text) + } else if (annotation.type !== 'text' && this.text) { + this.managers.text.delete(this.text) + this.text = undefined + } + + const x = annotation.x + const y = annotation.y + const width = annotation.width + const height = annotation.height + const xChanged = x !== this.x + const yChanged = y !== this.y + const widthChanged = width !== this.width + const heightChanged = height !== this.height + + if (xChanged || yChanged || widthChanged || heightChanged) { + // TODO -> enable in renderer options + if (xChanged) { + this.interpolateX = interpolate(this.x, x, DEFAULT_ANIMATE_RESIZE) + } + if (yChanged) { + this.interpolateY = interpolate(this.y, y, DEFAULT_ANIMATE_RESIZE) + } + if (widthChanged) { + this.interpolateWidth = interpolate(this.width, width, DEFAULT_ANIMATE_RESIZE) + } + if (heightChanged) { + this.interpolateHeight = interpolate(this.height, height, DEFAULT_ANIMATE_RESIZE) + } + } else { + this.resize(width, height).moveTo(x, y) + this.interpolateX = undefined + this.interpolateY = undefined + this.interpolateWidth = undefined + this.interpolateHeight = undefined + } + + return this + } + + render(dt: number) { + if (this.interpolateX || this.interpolateY) { + let x = this.x + let y = this.y + + if (this.interpolateX) { + const { value, done } = this.interpolateX(dt) + x = value + + if (done) { + this.interpolateX = undefined + } + } + + if (this.interpolateY) { + const { value, done } = this.interpolateY(dt) + y = value + + if (done) { + this.interpolateY = undefined + } + } + + this.moveTo(x, y) + } + + if (this.interpolateWidth || this.interpolateHeight) { + let width = this.width + let height = this.height + + if (this.interpolateWidth) { + const { value, done } = this.interpolateWidth(dt) + width = value + + if (done) { + this.interpolateWidth = undefined + } + } + + if (this.interpolateHeight) { + const { value, done } = this.interpolateHeight(dt) + height = value + + if (done) { + this.interpolateHeight = undefined + } + } + + this.resize(width, height) + } + + const isVisible = this.visible() + + if (isVisible && this.annotation.type === 'text' && !this.text) { + this.text = new Text( + this.renderer.assets, + this.renderer.annotationsContainer, + this.annotation.content, + this.annotation.style.text + ).moveTo(this.x, this.y) + } + + const fillMounted = this.managers.annotations.isMounted(this.fill) + + if (isVisible && !fillMounted) { + this.managers.annotations.mount(this.fill) + } else if (!isVisible && fillMounted) { + this.managers.annotations.unmount(this.fill) + } + + const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM + const strokesMounted = this.managers.annotations.isMounted(this.strokes) + + if (shouldStrokesMount && !strokesMounted) { + this.managers.annotations.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.annotations.unmount(this.strokes) + } + + if (this.text) { + const textMounted = this.managers.text.isMounted(this.text) + + if (isVisible && !textMounted) { + this.managers.text.mount(this.text) + } else if (!isVisible && textMounted) { + this.managers.text.unmount(this.text) + } + } + + return this + } + + delete() { + this.managers.annotations.delete(this.fill) + this.managers.annotations.delete(this.strokes) + if (this.text) { + this.managers.text.delete(this.text) + this.text = undefined + } + } + + resize(width: number, height: number) { + if (this.width !== width || this.height !== height) { + this.width = width + this.height = height + this.fill.resize({ width, height }) + this.strokes.resize({ width, height }) + } + + return this + } + + moveTo(x: number, y: number) { + if (x !== this.x || y !== this.y) { + this.x = x + this.y = y + this.fill.moveTo(x, y) + this.strokes.moveTo(x, y) + this.text?.moveTo(x, y) + } + console.log('anno', this.x, this.y, this.x + this.width / 2, this.y + this.height / 2) + return this + } + + private visible() { + const [left, right, top, bottom] = [this.x, this.x + this.width, this.y, this.y + this.height] + return ( + this.renderer.zoom > MIN_ANNOTATION_ZOOM && + right >= this.renderer.minX && + left <= this.renderer.maxX && + bottom >= this.renderer.minY && + top <= this.renderer.maxY + ) + } + + private get managers() { + return this.renderer.managers + } +} diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts index d6b6e2b..f8d43e1 100644 --- a/src/renderers/webgl/edge.ts +++ b/src/renderers/webgl/edge.ts @@ -1,4 +1,11 @@ -import { DEFAULT_ARROW, DEFAULT_LABEL_STYLE, MIN_EDGES_ZOOM, MIN_INTERACTION_ZOOM, MIN_LABEL_ZOOM } from '../../utils/constants' +import { + DEFAULT_ARROW, + DEFAULT_LABEL_STYLE, + MIN_EDGES_ZOOM, + MIN_INTERACTION_ZOOM, + MIN_LABEL_ZOOM, + MIN_STROKE_ZOOM +} from '../../utils/constants' import { type Renderer } from '.' import { midPoint } from './utils' import { movePoint } from './utils' @@ -180,10 +187,11 @@ export class EdgeRenderer { this.managers.edges.unmount(this.lineSegment) } + const strokesShouldMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM const strokesMounted = this.managers.edges.isMounted(this.strokes) - if (isVisible && !strokesMounted) { + if (strokesShouldMount && !strokesMounted) { this.managers.edges.mount(this.strokes) - } else if (!isVisible && strokesMounted) { + } else if (!strokesShouldMount && strokesMounted) { this.managers.edges.unmount(this.strokes) } diff --git a/src/renderers/webgl/index.ts b/src/renderers/webgl/index.ts index a42730d..959d5c8 100644 --- a/src/renderers/webgl/index.ts +++ b/src/renderers/webgl/index.ts @@ -15,6 +15,8 @@ import CircleTexture from './textures/CircleTexture' import TextIconTexture from './textures/TextIconTexture' import AssetManager from './loaders/AssetManager' import LifecycleManager from './LifecycleManager' +import RectangleTexture from './textures/RectangleTexture' +import AnnotationsRenderer from './annotations/AnnotationsRenderer' export type Keys = { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean } export type MousePosition = { x: number; y: number; clientX: number; clientY: number } @@ -127,12 +129,14 @@ export class Renderer { zoomInteraction = new Zoom(this) dragInteraction = new Drag(this) decelerateInteraction = new Decelerate(this) + annotationsContainer = new Container() managers = new LifecycleManager() eventSystem: EventSystem nodes: Node[] = [] nodeRenderersById: Record = {} edges: Edge[] = [] edgeRenderersById: Record = {} + annotations: AnnotationsRenderer renderedNodes = false dragInertia = defaultOptions.dragInertia animateViewport: number | false = defaultOptions.animateViewport @@ -141,6 +145,7 @@ export class Renderer { circle: CircleTexture arrow: ArrowTexture textIcon: TextIconTexture + rectangle: RectangleTexture draggedNode?: NodeRenderer hoveredNode?: NodeRenderer assets = new AssetManager() @@ -220,15 +225,18 @@ export class Renderer { this.circle = new CircleTexture(this) this.arrow = new ArrowTexture(this) this.textIcon = new TextIconTexture(this) + this.rectangle = new RectangleTexture(this) this.eventSystem = new EventSystem(this.app.renderer) this.eventSystem.domElement = view this.root.eventMode = 'static' // 'passive' // TODO - add viewport events to interactionContainer this.edgesContainer.eventMode = 'none' this.nodesContainer.eventMode = 'none' this.labelsContainer.eventMode = 'none' + this.annotationsContainer.eventMode = 'none' this.interactionContainer.eventMode = 'passive' const MIN_COORDINATE = Number.MIN_SAFE_INTEGER / 2 this.root.hitArea = new Rectangle(MIN_COORDINATE, MIN_COORDINATE, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) + this.root.addChild(this.annotationsContainer) this.root.addChild(this.edgesContainer) this.root.addChild(this.nodesContainer) this.root.addChild(this.labelsContainer) @@ -242,6 +250,8 @@ export class Renderer { this.root.addEventListener('pointerupoutside', this.pointerReleaseNode) view.addEventListener!('wheel', this.zoomInteraction.wheel, { passive: false }) + this.annotations = new AnnotationsRenderer(this) + if (debug) { // this.grid = new Grid(this, 24000, 24000, 100, { hideText: false }) this.stats = new Stats() @@ -256,7 +266,7 @@ export class Renderer { } } - update({ nodes, edges, options }: { nodes: Node[]; edges: Edge[]; annotations?: Annotation[]; options: Options }) { + update({ nodes, edges, annotations, options }: { nodes: Node[]; edges: Edge[]; annotations?: Annotation[]; options: Options }) { this.animateViewport = options.animateViewport === true || options.animateViewport === undefined ? defaultOptions.animateViewport : options.animateViewport this.animateNodePosition = @@ -434,6 +444,8 @@ export class Renderer { } } + this.annotations.update(annotations) + this.zoomInteraction.zooming = false return this @@ -500,6 +512,7 @@ export class Renderer { this.edgeRenderersById[edge.id].render() } + this.annotations.render(dt) this.managers.render() this.app.render() } diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index 2a72378..0fafd78 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -1,4 +1,4 @@ -import { DEFAULT_LABEL_STYLE, MIN_LABEL_ZOOM, MIN_INTERACTION_ZOOM, MIN_NODE_STROKE_ZOOM, MIN_NODE_ICON_ZOOM } from '../../utils/constants' +import { DEFAULT_LABEL_STYLE, MIN_LABEL_ZOOM, MIN_INTERACTION_ZOOM, MIN_STROKE_ZOOM, MIN_NODE_ICON_ZOOM } from '../../utils/constants' import { FederatedPointerEvent } from 'pixi.js' import { NodeHitArea } from './interaction/nodeHitArea' import { interpolate } from '../../utils/helpers' @@ -166,7 +166,7 @@ export class NodeRenderer { this.managers.nodes.unmount(this.fill) } - const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_NODE_STROKE_ZOOM + const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM const strokesMounted = this.managers.nodes.isMounted(this.strokes) if (shouldStrokesMount && !strokesMounted) { @@ -566,8 +566,10 @@ export class NodeRenderer { private applyIcon() { const icon = this.node.style?.icon if (icon !== undefined && this.icon === undefined) { - this.icon = new Icon(this.renderer.assets, this.renderer.textIcon, this.renderer.nodesContainer, this.fill, icon) - this.icon.moveTo(this.x, this.y) + this.icon = new Icon(this.renderer.assets, this.renderer.textIcon, this.renderer.nodesContainer, this.fill, icon).moveTo( + this.x, + this.y + ) } return this diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts index 4221982..5703562 100644 --- a/src/renderers/webgl/objects/rectangle/Rectangle.ts +++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts @@ -1,6 +1,6 @@ import { Dimensions, FillStyle, RenderObject } from '../../../../types' +import { DEFAULT_FILL, DEFAULT_OPACITY } from '../../../../utils/constants' import { Container, Sprite } from 'pixi.js' -import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../../utils/constants' import { isNumber } from '../../../../utils/helpers' import RectangleTexture from '../../textures/RectangleTexture' @@ -12,15 +12,17 @@ export default class Rectangle implements RenderObject { private width = 0 private height = 0 private object: Sprite - private style: Required = DEFAULT_FILL_STYLE + private style: Required constructor( private container: Container, private texture: RectangleTexture, + { color = DEFAULT_FILL, opacity = DEFAULT_OPACITY }: Partial = {}, index?: number ) { this.container = container this.texture = texture + this.style = { color, opacity } this.object = this.create(index) } diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts index 69e67ec..7324518 100644 --- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts +++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts @@ -13,18 +13,17 @@ export default class RectangleStrokes implements RenderObject { private minSize: Dimensions = { width: 0, height: 0 } private maxSize: Dimensions = { width: 0, height: 0 } private objects: Rectangle[] = [] - private strokes: Stroke[] = [] constructor( private container: Container, private texture: RectangleTexture, - private fill: Rectangle + private fill: Rectangle, + private strokes: Stroke[] = [] ) { this.container = container this.texture = texture this.fill = fill - this.minSize = this.fill.size - this.maxSize = this.minSize + this.applyStrokes(strokes) } update(strokes: Stroke[] = []) { @@ -57,7 +56,7 @@ export default class RectangleStrokes implements RenderObject { return this } - resize(width: number, height: number) { + resize({ width, height }: Dimensions) { if (width !== this.minSize.width || height !== this.minSize.height) { this.minSize = { width, height } this.maxSize = this.minSize @@ -121,13 +120,14 @@ export default class RectangleStrokes implements RenderObject { private applyStrokes(strokes: Stroke[]) { this.objects = [] this.strokes = strokes + this.minSize = this.fill.size this.maxSize = this.minSize const index = this.fill.getContainerIndex() - for (const { width, color, opacity } of strokes) { - const object = new Rectangle(this.container, this.texture, index) - this.objects.push(object.update(color, opacity).resize(this.increment(width)).moveTo(this.x, this.y)) + for (const { width, ...style } of strokes) { + const object = new Rectangle(this.container, this.texture, style, index) + this.objects.push(object.resize(this.increment(width)).moveTo(this.x, this.y)) } return this diff --git a/src/renderers/webgl/textures/TextTexture.ts b/src/renderers/webgl/textures/TextTexture.ts index a19048e..e2a50b5 100644 --- a/src/renderers/webgl/textures/TextTexture.ts +++ b/src/renderers/webgl/textures/TextTexture.ts @@ -128,7 +128,7 @@ export default class TextTexture { case 'right': return [0, 0.5] default: - return [0.5, 0.5] + return [0, 0] } } diff --git a/src/types/internal.ts b/src/types/internal.ts index cfc3fe1..3450b6d 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -23,3 +23,5 @@ export interface RenderObjectLifecycle { export interface RenderObject extends RenderObjectLifecycle { moveTo(...args: number[]): this } + +export type InterpolateFn = (dt: number) => { value: number; done: boolean } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 75843c8..fb22da3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -7,10 +7,11 @@ export const RADIANS_PER_DEGREE = Math.PI / 180 // zoom limits // TODO - extends to renderer options export const MIN_LABEL_ZOOM = 0.25 -export const MIN_NODE_STROKE_ZOOM = 0.3 +export const MIN_STROKE_ZOOM = 0.3 export const MIN_NODE_ICON_ZOOM = 0.3 export const MIN_INTERACTION_ZOOM = 0.15 export const MIN_EDGES_ZOOM = 0.1 +export const MIN_ANNOTATION_ZOOM = 0.25 export const MIN_TEXTURE_ZOOM = 3 // style @@ -62,3 +63,6 @@ export const DEFAULT_HIGHLIGHT_STYLE = { } export const GENERIC_FONT_FAMILIES = new Set(['serif', 'sans-serif', 'monospace', 'cursive']) + +// animations +export const DEFAULT_ANIMATE_RESIZE = 800 From 86b1d20b49fac65c3a883104374e1fb7fd44be2a Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Wed, 14 Feb 2024 13:20:07 -0500 Subject: [PATCH 07/12] RectangleAnnotationRenderer implemented w/o events --- examples/native/src/annotations/index.ts | 51 ++++++++++++++----- .../RectangleAnnotationRenderer.ts | 23 ++++++--- .../webgl/objects/rectangle/Rectangle.ts | 6 ++- .../objects/rectangle/RectangleStrokes.ts | 19 +++++-- src/renderers/webgl/objects/text/Text.ts | 4 ++ src/renderers/webgl/textures/TextTexture.ts | 2 +- 6 files changed, 80 insertions(+), 25 deletions(-) diff --git a/examples/native/src/annotations/index.ts b/examples/native/src/annotations/index.ts index 3e81416..f5c45a7 100644 --- a/examples/native/src/annotations/index.ts +++ b/examples/native/src/annotations/index.ts @@ -10,18 +10,45 @@ const edges: Graph.Edge[] = [] const nodes: Graph.Node[] = [] const annotations: Graph.Annotation[] = [ { - id: 'rect-anno-1', + id: 'rect-anno-0', type: 'rectangle', width: 200, - height: 100, - x: 100, - y: 100, + height: 200, + x: -100, + y: -100, + style: { color: GREEN } + }, + { + id: 'rect-anno-2', + type: 'rectangle', + width: 200, + height: 200, + x: 0, + y: -100, style: { - color: GREEN, - stroke: [ - { width: 1, color: WHITE }, - { width: 2, color: GREEN_LIGHT } - ] + color: GREEN_LIGHT + } + }, + { + id: 'rect-anno-3', + type: 'rectangle', + width: 200, + height: 200, + x: -100, + y: 0, + style: { + color: GREEN_LIGHT + } + }, + { + id: 'rect-anno-4', + type: 'rectangle', + width: 200, + height: 200, + x: 0, + y: 0, + style: { + color: GREEN } }, { @@ -30,8 +57,8 @@ const annotations: Graph.Annotation[] = [ content: 'Hello, World!', width: 200, height: 100, - x: -200, - y: -100, + x: -300, + y: -200, style: { color: DARK_GREEN, stroke: [ @@ -41,7 +68,7 @@ const annotations: Graph.Annotation[] = [ text: { fontSize: 14, fontWeight: '400', - color: GREEN_LIGHT + color: WHITE } } } diff --git a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts index 52183e8..f8587dd 100644 --- a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts @@ -1,5 +1,5 @@ -import { InterpolateFn, RectangleAnnotation, TextAnnotation } from '../../../types' -import { DEFAULT_ANIMATE_RESIZE, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' +import { InterpolateFn, PointTuple, RectangleAnnotation, TextAnnotation } from '../../../types' +import { DEFAULT_ANIMATE_RESIZE, DEFAULT_RESOLUTION, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' import { interpolate } from '../../../utils/helpers' import { Renderer } from '..' import RectangleStrokes from '../objects/rectangle/RectangleStrokes' @@ -134,12 +134,13 @@ export default class RectangleAnnotationRenderer { const isVisible = this.visible() if (isVisible && this.annotation.type === 'text' && !this.text) { + const [hw, hh] = this.halfSize this.text = new Text( this.renderer.assets, this.renderer.annotationsContainer, this.annotation.content, this.annotation.style.text - ).moveTo(this.x, this.y) + ).moveTo(this.x + hw / this.resolution, this.y + hh / this.resolution) } const fillMounted = this.managers.annotations.isMounted(this.fill) @@ -198,14 +199,15 @@ export default class RectangleAnnotationRenderer { this.y = y this.fill.moveTo(x, y) this.strokes.moveTo(x, y) - this.text?.moveTo(x, y) + const [hw, hh] = this.halfSize + this.text?.moveTo(x + hw / this.resolution, y + hh / this.resolution) } - console.log('anno', this.x, this.y, this.x + this.width / 2, this.y + this.height / 2) return this } private visible() { - const [left, right, top, bottom] = [this.x, this.x + this.width, this.y, this.y + this.height] + const [halfWidth, halfHeight] = this.halfSize + const [left, right, top, bottom] = [this.x - halfWidth, this.x + halfWidth, this.y - halfHeight, this.y + halfHeight] return ( this.renderer.zoom > MIN_ANNOTATION_ZOOM && right >= this.renderer.minX && @@ -215,7 +217,16 @@ export default class RectangleAnnotationRenderer { ) } + private get halfSize(): PointTuple { + return [this.width / 2, this.height / 2] + } + private get managers() { return this.renderer.managers } + + private get resolution() { + // TODO - implement resolution in options + return DEFAULT_RESOLUTION + } } diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts index 5703562..4b85953 100644 --- a/src/renderers/webgl/objects/rectangle/Rectangle.ts +++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts @@ -93,10 +93,14 @@ export default class Rectangle implements RenderObject { return { width: this.width, height: this.height } } + get anchor() { + return this.object.anchor + } + private create(index?: number) { const object = new Sprite(this.texture.get()) - object.anchor.set(0.5) + object.anchor.set(0.5, 0.5) object.x = this.x object.y = this.y object.visible = this.mounted diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts index 7324518..8b9f37c 100644 --- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts +++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts @@ -47,9 +47,13 @@ export default class RectangleStrokes implements RenderObject { if (dirty) { this.x = x this.y = y + let _x = x + let _y = y - for (const object of this.objects) { - object.moveTo(x, y) + for (let i = 0; i < this.strokes.length; i += 1) { + _x -= this.strokes[i].width / 2 + _y -= this.strokes[i].width / 2 + this.objects[i].moveTo(_x, _y) } } @@ -112,8 +116,8 @@ export default class RectangleStrokes implements RenderObject { } private increment(value: number) { - this.maxSize.height += value - this.maxSize.width += value + const { width, height } = this.size + this.maxSize = { width: width + value * 2, height: height + value * 2 } return this.maxSize } @@ -125,9 +129,14 @@ export default class RectangleStrokes implements RenderObject { const index = this.fill.getContainerIndex() + let x = this.x + let y = this.y + for (const { width, ...style } of strokes) { + x -= width / 2 + y -= width / 2 const object = new Rectangle(this.container, this.texture, style, index) - this.objects.push(object.resize(this.increment(width)).moveTo(this.x, this.y)) + this.objects.push(object.resize(this.increment(width)).moveTo(x, y)) } return this diff --git a/src/renderers/webgl/objects/text/Text.ts b/src/renderers/webgl/objects/text/Text.ts index 8acff5e..a07745a 100644 --- a/src/renderers/webgl/objects/text/Text.ts +++ b/src/renderers/webgl/objects/text/Text.ts @@ -179,6 +179,10 @@ export default class Text implements RenderObject { return this._rect } + get anchor() { + return this.object.anchor + } + private get size(): [width: number, height: number] { const [px, py] = this.style.getHighlightPadding() return [this.object.width + px * 2, this.object.height + py * 2] diff --git a/src/renderers/webgl/textures/TextTexture.ts b/src/renderers/webgl/textures/TextTexture.ts index e2a50b5..a19048e 100644 --- a/src/renderers/webgl/textures/TextTexture.ts +++ b/src/renderers/webgl/textures/TextTexture.ts @@ -128,7 +128,7 @@ export default class TextTexture { case 'right': return [0, 0.5] default: - return [0, 0] + return [0.5, 0.5] } } From b024d7a92f0f5aaaa6c8b378b2eb6fbc3362b4aa Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Wed, 14 Feb 2024 13:52:39 -0500 Subject: [PATCH 08/12] CircleAnnotationRenderer with text + no events --- examples/native/src/annotations/index.ts | 40 +++- .../webgl/annotations/AnnotationsRenderer.ts | 18 +- .../annotations/CircleAnnotationRenderer.ts | 181 ++++++++++++++++-- .../RectangleAnnotationRenderer.ts | 15 +- src/renderers/webgl/edge.ts | 4 +- src/renderers/webgl/node.ts | 45 +++-- src/renderers/webgl/objects/ObjectManager.ts | 3 + src/renderers/webgl/objects/circle/Circle.ts | 8 +- .../webgl/objects/circle/CircleStrokes.ts | 14 +- src/types/api.ts | 11 +- src/types/internal.ts | 2 +- src/utils/constants.ts | 6 +- 12 files changed, 268 insertions(+), 79 deletions(-) diff --git a/examples/native/src/annotations/index.ts b/examples/native/src/annotations/index.ts index f5c45a7..845f8b4 100644 --- a/examples/native/src/annotations/index.ts +++ b/examples/native/src/annotations/index.ts @@ -53,7 +53,7 @@ const annotations: Graph.Annotation[] = [ }, { id: 'text-anno-2', - type: 'text', + type: 'rectangle', content: 'Hello, World!', width: 200, height: 100, @@ -71,6 +71,44 @@ const annotations: Graph.Annotation[] = [ color: WHITE } } + }, + { + id: 'circle-anno-0', + type: 'circle', + radius: 50, + x: 250, + y: 250, + style: { + color: GREEN, + stroke: [ + { width: 1, color: WHITE }, + { width: 2, color: GREEN_LIGHT } + ] + } + }, + { + id: 'circle-anno-1', + type: 'circle', + radius: 50, + content: 'CIRCLE!', + x: -250, + y: 250, + style: { + color: GREEN, + stroke: [ + { width: 1, color: WHITE }, + { width: 2, color: GREEN_LIGHT } + ], + text: { + fontSize: 14, + fontWeight: '400', + color: WHITE, + highlight: { + color: DARK_GREEN, + padding: 0 + } + } + } } ] diff --git a/src/renderers/webgl/annotations/AnnotationsRenderer.ts b/src/renderers/webgl/annotations/AnnotationsRenderer.ts index eb782cb..60ea657 100644 --- a/src/renderers/webgl/annotations/AnnotationsRenderer.ts +++ b/src/renderers/webgl/annotations/AnnotationsRenderer.ts @@ -23,23 +23,15 @@ export default class AnnotationsRenderer { for (const annotation of annotations) { const renderer = this.lookup[annotation.id] - if (renderer === undefined) { - if (annotation.type === 'circle') { - lookup[annotation.id] = new CircleAnnotationRenderer(this.renderer, annotation) - } else { - lookup[annotation.id] = new RectangleAnnotationRenderer(this.renderer, annotation) - } + if (annotation.type === 'circle' && renderer instanceof CircleAnnotationRenderer) { + renderer.update(annotation) } else if (annotation.type === 'circle') { - if (renderer instanceof CircleAnnotationRenderer) { - renderer.update(annotation) - } else { - renderer.delete() - lookup[annotation.id] = new CircleAnnotationRenderer(this.renderer, annotation) - } + renderer?.delete() + lookup[annotation.id] = new CircleAnnotationRenderer(this.renderer, annotation) } else if (renderer instanceof RectangleAnnotationRenderer) { renderer.update(annotation) } else { - renderer.delete() + renderer?.delete() lookup[annotation.id] = new RectangleAnnotationRenderer(this.renderer, annotation) } } diff --git a/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts b/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts index 6e5c213..7ced5f6 100644 --- a/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts @@ -1,48 +1,201 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ +import { DEFAULT_ANIMATE_RESIZE, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' +import { CircleAnnotation, InterpolateFn } from '../../../types' +import { interpolate } from '../../../utils/helpers' import { Renderer } from '..' -import { CircleAnnotation } from '../../../types' +import CircleStrokes from '../objects/circle/CircleStrokes' +import Circle from '../objects/circle/Circle' +import Text from '../objects/text/Text' export default class CircleAnnotationRenderer { annotation: CircleAnnotation private x = 0 private y = 0 - private radius = 0 + private _radius = 0 + + private fill: Circle + private strokes: CircleStrokes + private text?: Text + + private interpolateX?: InterpolateFn + private interpolateY?: InterpolateFn + private interpolateRadius?: InterpolateFn constructor( private renderer: Renderer, annotation: CircleAnnotation ) { this.renderer = renderer - this.annotation = annotation + this.fill = new Circle(renderer.annotationsContainer, renderer.circle, annotation.style) + this.strokes = new CircleStrokes(renderer.annotationsContainer, renderer.circle, this.fill, annotation.style.stroke) this.resize(annotation.radius).moveTo(annotation.x, annotation.y) + this.annotation = annotation } update(annotation: CircleAnnotation) { this.annotation = annotation - return this - } - resize(radius: number) { - this.radius = radius - return this - } + this.fill.update(annotation.style.color, annotation.style.opacity) + this.strokes.update(annotation.style.stroke) + + if (annotation.content && this.text) { + this.text.update(annotation.content, annotation.style.text) + } else if (!annotation.content && this.text) { + this.text = this.managers.text.delete(this.text) + } + + const x = annotation.x + const y = annotation.y + const radius = annotation.radius + const xChanged = x !== this.x + const yChanged = y !== this.y + const radiusChanged = radius !== this._radius + + if (xChanged || yChanged || radiusChanged) { + if (xChanged) { + this.interpolateX = interpolate(this.x, x, DEFAULT_ANIMATE_RESIZE) + } + if (yChanged) { + this.interpolateY = interpolate(this.y, y, DEFAULT_ANIMATE_RESIZE) + } + if (radiusChanged) { + this.interpolateRadius = interpolate(this._radius, radius, DEFAULT_ANIMATE_RESIZE) + } + } else { + this.resize(radius).moveTo(x, y) + this.interpolateX = undefined + this.interpolateY = undefined + this.interpolateRadius = undefined + } - moveTo(x: number, y: number) { - this.x = x - this.y = y return this } render(dt: number) { + if (this.interpolateRadius) { + const { value, done } = this.interpolateRadius(dt) + + this.resize(value) + + if (done) { + this.interpolateRadius = undefined + } + } + + if (this.interpolateX || this.interpolateY) { + let x = this.x + let y = this.y + + if (this.interpolateX) { + const { value, done } = this.interpolateX(dt) + x = value + + if (done) { + this.interpolateX = undefined + } + } + + if (this.interpolateY) { + const { value, done } = this.interpolateY(dt) + y = value + + if (done) { + this.interpolateY = undefined + } + } + + this.moveTo(x, y) + } + + const isVisible = this.visible() + + if (isVisible && this.annotation.content && !this.text) { + this.text = new Text( + this.renderer.assets, + this.renderer.annotationsContainer, + this.annotation.content, + this.annotation.style.text + ).moveTo(this.x, this.y) + } + + const fillMounted = this.managers.annotations.isMounted(this.fill) + + if (isVisible && !fillMounted) { + this.managers.annotations.mount(this.fill) + } else if (!isVisible && fillMounted) { + this.managers.annotations.unmount(this.fill) + } + + const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM + const strokesMounted = this.managers.annotations.isMounted(this.strokes) + + if (shouldStrokesMount && !strokesMounted) { + this.managers.annotations.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.annotations.unmount(this.strokes) + } + + if (this.text) { + const textMounted = this.managers.text.isMounted(this.text) + + if (isVisible && !textMounted) { + this.managers.text.mount(this.text) + } else if (!isVisible && textMounted) { + this.managers.text.unmount(this.text) + } + } + return this } delete() { + this.managers.annotations.delete(this.fill) + this.managers.annotations.delete(this.strokes) + if (this.text) { + this.text = this.managers.text.delete(this.text) + } return undefined } + private get radius() { + return this.strokes.radius + } + + private get managers() { + return this.renderer.managers + } + + private resize(radius: number) { + if (radius !== this._radius) { + this._radius = radius + this.fill.resize(radius) + this.strokes.resize(radius) + } + + return this + } + + private moveTo(x: number, y: number) { + if (x !== this.x || y !== this.y) { + this.x = x + this.y = y + this.fill.moveTo(x, y) + this.strokes.moveTo(x, y) + this.text?.moveTo(x, y) + } + + return this + } + private visible() { - return false + const [left, right, top, bottom] = [this.x - this.radius, this.x + this.radius, this.y - this.radius, this.y + this.radius] + + return ( + this.renderer.zoom > MIN_ANNOTATION_ZOOM && + right >= this.renderer.minX && + left <= this.renderer.maxX && + bottom >= this.renderer.minY && + top <= this.renderer.maxY + ) } } diff --git a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts index f8587dd..7b7da6f 100644 --- a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts @@ -1,4 +1,4 @@ -import { InterpolateFn, PointTuple, RectangleAnnotation, TextAnnotation } from '../../../types' +import { InterpolateFn, PointTuple, RectangleAnnotation } from '../../../types' import { DEFAULT_ANIMATE_RESIZE, DEFAULT_RESOLUTION, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' import { interpolate } from '../../../utils/helpers' import { Renderer } from '..' @@ -7,7 +7,7 @@ import Rectangle from '../objects/rectangle/Rectangle' import Text from '../objects/text/Text' export default class RectangleAnnotationRenderer { - annotation: TextAnnotation | RectangleAnnotation + annotation: RectangleAnnotation private x = 0 private y = 0 @@ -25,7 +25,7 @@ export default class RectangleAnnotationRenderer { constructor( private renderer: Renderer, - annotation: TextAnnotation | RectangleAnnotation + annotation: RectangleAnnotation ) { this.renderer = renderer this.fill = new Rectangle(renderer.annotationsContainer, renderer.rectangle, annotation.style) @@ -34,14 +34,15 @@ export default class RectangleAnnotationRenderer { this.annotation = annotation } - update(annotation: TextAnnotation | RectangleAnnotation) { + update(annotation: RectangleAnnotation) { this.annotation = annotation + this.fill.update(annotation.style.color, annotation.style.opacity) this.strokes.update(annotation.style.stroke) - if (annotation.type === 'text' && this.text) { + if (annotation.content && this.text) { this.text.update(annotation.content, annotation.style.text) - } else if (annotation.type !== 'text' && this.text) { + } else if (!annotation.content && this.text) { this.managers.text.delete(this.text) this.text = undefined } @@ -133,7 +134,7 @@ export default class RectangleAnnotationRenderer { const isVisible = this.visible() - if (isVisible && this.annotation.type === 'text' && !this.text) { + if (isVisible && this.annotation.content && !this.text) { const [hw, hh] = this.halfSize this.text = new Text( this.renderer.assets, diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts index f8d43e1..8d0af73 100644 --- a/src/renderers/webgl/edge.ts +++ b/src/renderers/webgl/edge.ts @@ -105,8 +105,8 @@ export class EdgeRenderer { const y0 = this.source.y const x1 = this.target.x const y1 = this.target.y - const sourceRadius = this.source.strokes.radius - const targetRadius = this.target.strokes.radius + const sourceRadius = this.source.radius + const targetRadius = this.target.radius const isVisible = this.visible(x0, y0, x1, y1) diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index 0fafd78..b320902 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -10,15 +10,16 @@ import Text from './objects/text/Text' import Icon from './objects/Icon' export class NodeRenderer { - node: Node x = 0 y = 0 - radius = 0 + + node: Node fill: Circle strokes: CircleStrokes label?: Text icon?: Icon + private _radius = 0 private hitArea: NodeHitArea private renderer: Renderer private doubleClickTimeout: NodeJS.Timeout | undefined @@ -33,11 +34,11 @@ export class NodeRenderer { constructor(renderer: Renderer, node: Node) { this.renderer = renderer - this.fill = new Circle(renderer.nodesContainer, renderer.circle) - this.strokes = new CircleStrokes(renderer.nodesContainer, renderer.circle, this.fill) + this.fill = new Circle(renderer.nodesContainer, renderer.circle, node.style) + this.strokes = new CircleStrokes(renderer.nodesContainer, renderer.circle, this.fill, node.style?.stroke) this.hitArea = new NodeHitArea(this.renderer.interactionContainer, this) + this.resize(node.radius).moveTo(node.x ?? 0, node.y ?? 0) this.node = node - this.update(node) } update(node: Node) { @@ -77,7 +78,7 @@ export class NodeRenderer { const radius = node.radius const xChanged = x !== this.x const yChanged = y !== this.y - const radiusChanged = radius !== this.radius + const radiusChanged = radius !== this._radius if ( (xChanged || yChanged || radiusChanged) && @@ -92,7 +93,7 @@ export class NodeRenderer { this.interpolateY = interpolate(this.y, y, this.renderer.animateNodePosition) } if (radiusChanged && this.renderer.animateNodeRadius) { - this.interpolateRadius = interpolate(this.radius, radius, this.renderer.animateNodeRadius) + this.interpolateRadius = interpolate(this._radius, radius, this.renderer.animateNodeRadius) } } else { this.resize(radius).moveTo(x, y) @@ -221,6 +222,10 @@ export class NodeRenderer { } } + get radius() { + return this.strokes.radius + } + pointerEnter = (event: FederatedPointerEvent) => { if (this.renderer.draggedNode === this) { this.pointerLeftBeforeDragComplete = false @@ -498,13 +503,13 @@ export class NodeRenderer { } private resize(radius: number) { - if (radius !== this.radius) { - this.radius = radius + if (radius !== this._radius) { + this._radius = radius this.fill.resize(radius) this.strokes.resize(radius) if (this.label) { - this.label.offset = this.strokes.radius + this.label.offset = this.radius } this.hitArea.update(this.x, this.y, radius) @@ -521,7 +526,7 @@ export class NodeRenderer { this.strokes.moveTo(x, y) this.label?.moveTo(x, y) this.icon?.moveTo(x, y) - this.hitArea.update(x, y, this.radius) + this.hitArea.update(x, y, this._radius) } return this @@ -531,15 +536,15 @@ export class NodeRenderer { let left: number, right: number, top: number, bottom: number if (this.label) { - left = this.x - Math.max(this.strokes.radius, this.label.rect.left) - right = this.x + Math.max(this.strokes.radius, this.label.rect.right) - top = this.y - Math.max(this.strokes.radius, this.label.rect.top) - bottom = this.y + Math.max(this.strokes.radius, this.label.rect.bottom) + left = this.x - Math.max(this.radius, this.label.rect.left) + right = this.x + Math.max(this.radius, this.label.rect.right) + top = this.y - Math.max(this.radius, this.label.rect.top) + bottom = this.y + Math.max(this.radius, this.label.rect.bottom) } else { - left = this.x - this.strokes.radius - right = this.x + this.strokes.radius - top = this.y - this.strokes.radius - bottom = this.y + this.strokes.radius + left = this.x - this.radius + right = this.x + this.radius + top = this.y - this.radius + bottom = this.y + this.radius } const { minX, maxX, minY, maxY } = this.renderer @@ -556,7 +561,7 @@ export class NodeRenderer { const style = this.node.style?.label if (label !== undefined && label.trim() !== '' && this.label === undefined) { this.label = new Text(this.renderer.assets, this.renderer.labelsContainer, label, style, DEFAULT_LABEL_STYLE) - this.label.offset = this.strokes.radius + this.label.offset = this.radius this.label.moveTo(this.x, this.y) } diff --git a/src/renderers/webgl/objects/ObjectManager.ts b/src/renderers/webgl/objects/ObjectManager.ts index 6d754f9..87e56de 100644 --- a/src/renderers/webgl/objects/ObjectManager.ts +++ b/src/renderers/webgl/objects/ObjectManager.ts @@ -9,14 +9,17 @@ export default class ObjectManager { mount(object: T) { this.batch.set(object, 0) + return object } unmount(object: T) { this.batch.set(object, 1) + return object } delete(object: T) { this.batch.set(object, 2) + return undefined } render() { diff --git a/src/renderers/webgl/objects/circle/Circle.ts b/src/renderers/webgl/objects/circle/Circle.ts index 2a6e974..9ce6ce1 100644 --- a/src/renderers/webgl/objects/circle/Circle.ts +++ b/src/renderers/webgl/objects/circle/Circle.ts @@ -1,5 +1,5 @@ import { FillStyle, RenderObject } from '../../../../types' -import { DEFAULT_FILL, DEFAULT_FILL_STYLE, DEFAULT_OPACITY } from '../../../../utils/constants' +import { DEFAULT_FILL, DEFAULT_OPACITY } from '../../../../utils/constants' import { Container, Sprite } from 'pixi.js' import CircleTexture from '../../textures/CircleTexture' import { isNumber } from '../../../../utils/helpers' @@ -12,15 +12,17 @@ export default class Circle implements RenderObject { private _radius = 0 private object: Sprite - private style: Required = DEFAULT_FILL_STYLE + private style: Required constructor( private container: Container, private texture: CircleTexture, + { color = DEFAULT_FILL, opacity = DEFAULT_OPACITY }: Partial = {}, index?: number ) { this.container = container this.texture = texture + this.style = { color, opacity } this.object = this.create(index) } @@ -92,7 +94,7 @@ export default class Circle implements RenderObject { private create(index?: number) { const object = new Sprite(this.texture.get()) - object.anchor.set(0.5) + object.anchor.set(0.5, 0.5) object.x = this.x object.y = this.y object.visible = this.mounted diff --git a/src/renderers/webgl/objects/circle/CircleStrokes.ts b/src/renderers/webgl/objects/circle/CircleStrokes.ts index a6e5983..de1930d 100644 --- a/src/renderers/webgl/objects/circle/CircleStrokes.ts +++ b/src/renderers/webgl/objects/circle/CircleStrokes.ts @@ -12,18 +12,17 @@ export default class CircleStrokes implements RenderObject { private minRadius = 0 private maxRadius = 0 private objects: Circle[] = [] - private strokes: Stroke[] = [] constructor( private container: Container, private texture: CircleTexture, - private fill: Circle + private fill: Circle, + private strokes: Stroke[] = [] ) { this.container = container this.texture = texture this.fill = fill - this.minRadius = fill.radius - this.maxRadius = fill.radius + this.applyStrokes(strokes) } update(strokes: Stroke[] = []) { @@ -119,13 +118,14 @@ export default class CircleStrokes implements RenderObject { private applyStrokes(strokes: Stroke[]) { this.objects = [] this.strokes = strokes + this.minRadius = this.fill.radius this.maxRadius = this.minRadius const index = this.fill.getContainerIndex() - for (const { width, color, opacity } of strokes) { - const object = new Circle(this.container, this.texture, index) - this.objects.push(object.update(color, opacity).resize(this.increment(width)).moveTo(this.x, this.y)) + for (const { width, ...style } of strokes) { + const object = new Circle(this.container, this.texture, style, index) + this.objects.push(object.resize(this.increment(width)).moveTo(this.x, this.y)) } return this diff --git a/src/types/api.ts b/src/types/api.ts index 4adbed3..84f70e7 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -112,6 +112,7 @@ export type Edge = { // annotations export type AnnotationStyle = FillStyle & { stroke?: Stroke[] + text?: Omit } export type TextAnnotationStyle = AnnotationStyle & { @@ -125,6 +126,7 @@ type AnnotationBase = { x: number y: number resize?: boolean + content?: string } export type CircleAnnotation = AnnotationBase<'circle'> & { @@ -138,11 +140,4 @@ export type RectangleAnnotation = AnnotationBase<'rectangle'> & { style: AnnotationStyle } -export type TextAnnotation = AnnotationBase<'text'> & { - width: number - height: number - content: string - style: TextAnnotationStyle -} - -export type Annotation = CircleAnnotation | RectangleAnnotation | TextAnnotation +export type Annotation = CircleAnnotation | RectangleAnnotation diff --git a/src/types/internal.ts b/src/types/internal.ts index 3450b6d..4c9e775 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -17,7 +17,7 @@ export interface RenderObjectLifecycle { mounted: boolean mount(index?: number): this unmount(): this - delete(): void + delete(): undefined } export interface RenderObject extends RenderObjectLifecycle { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index fb22da3..49e650a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -39,13 +39,13 @@ export const DEFAULT_STROKE_STYLE = { } export const DEFAULT_TEXT_STYLE = { - margin: 2, + margin: 0, fontSize: 10, color: COLORS.BLACK, letterSpacing: 0.5, fontName: 'Font', fontFamily: 'sans-serif', - align: 'left' as const, + align: 'center' as const, wordWrap: false as const, position: 'center' as const, fontWeight: 'normal' as const, @@ -53,7 +53,7 @@ export const DEFAULT_TEXT_STYLE = { } export const DEFAULT_LABEL_STYLE = { - defaultTextStyle: { position: 'bottom' as const, align: 'center' as const } + defaultTextStyle: { position: 'bottom' as const, margin: 2 } } export const DEFAULT_HIGHLIGHT_STYLE = { From f9e937a1e74d1e2341d95e0226c14e7d6a178bf0 Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Thu, 15 Feb 2024 11:28:21 -0500 Subject: [PATCH 09/12] LineAnnotationRenderer without events --- examples/native/src/annotations/index.ts | 25 +++ src/renderers/webgl/LifecycleManager.ts | 2 +- .../webgl/annotations/AnnotationsRenderer.ts | 12 +- .../annotations/LineAnnotationRenderer.ts | 158 ++++++++++++++++++ src/renderers/webgl/edge.ts | 23 +-- src/renderers/webgl/index.ts | 2 +- .../webgl/interaction/edgeHitArea.ts | 3 +- .../webgl/objects/line/LineSegment.ts | 10 +- .../webgl/objects/line/LineStrokes.ts | 13 +- src/renderers/webgl/utils.ts | 29 ---- src/types/api.ts | 33 ++-- src/types/internal.ts | 2 + src/utils/api.ts | 2 +- src/utils/webgl.ts | 23 +++ 14 files changed, 267 insertions(+), 70 deletions(-) create mode 100644 src/renderers/webgl/annotations/LineAnnotationRenderer.ts delete mode 100644 src/renderers/webgl/utils.ts diff --git a/examples/native/src/annotations/index.ts b/examples/native/src/annotations/index.ts index 845f8b4..8e30f50 100644 --- a/examples/native/src/annotations/index.ts +++ b/examples/native/src/annotations/index.ts @@ -109,6 +109,31 @@ const annotations: Graph.Annotation[] = [ } } } + }, + { + id: 'line-anno-0', + type: 'line', + points: [ + { x: 0, y: -400 }, + { x: 0, y: 400 } + ], + style: { + color: DARK_GREEN, + width: 2 + } + }, + { + id: 'line-anno-1', + type: 'line', + content: 'LINE!', + points: [ + { x: -400, y: 0 }, + { x: 400, y: 0 } + ], + style: { + color: GREEN_LIGHT, + width: 2 + } } ] diff --git a/src/renderers/webgl/LifecycleManager.ts b/src/renderers/webgl/LifecycleManager.ts index 32e0595..c4f1b03 100644 --- a/src/renderers/webgl/LifecycleManager.ts +++ b/src/renderers/webgl/LifecycleManager.ts @@ -16,7 +16,7 @@ export default class LifecycleManager { arrows = new ObjectManager(1000) labels = new ObjectManager(2000) interactions = new ObjectManager(2000) - annotations = new ObjectManager(2000) + annotations = new ObjectManager(2000) text = new ObjectManager(1000) // interactions = new ObjectManager(2000) // TODO diff --git a/src/renderers/webgl/annotations/AnnotationsRenderer.ts b/src/renderers/webgl/annotations/AnnotationsRenderer.ts index 60ea657..71adfaf 100644 --- a/src/renderers/webgl/annotations/AnnotationsRenderer.ts +++ b/src/renderers/webgl/annotations/AnnotationsRenderer.ts @@ -1,9 +1,10 @@ import { Renderer } from '..' import { Annotation } from '../../../types' import CircleAnnotationRenderer from './CircleAnnotationRenderer' +import LineAnnotationRenderer from './LineAnnotationRenderer' import RectangleAnnotationRenderer from './RectangleAnnotationRenderer' -type AnnotationRendererLookup = Record +type AnnotationRendererLookup = Record export default class AnnotationsRenderer { private annotations: Annotation[] = [] @@ -28,11 +29,16 @@ export default class AnnotationsRenderer { } else if (annotation.type === 'circle') { renderer?.delete() lookup[annotation.id] = new CircleAnnotationRenderer(this.renderer, annotation) - } else if (renderer instanceof RectangleAnnotationRenderer) { + } else if (annotation.type === 'rectangle' && renderer instanceof RectangleAnnotationRenderer) { renderer.update(annotation) - } else { + } else if (annotation.type === 'rectangle') { renderer?.delete() lookup[annotation.id] = new RectangleAnnotationRenderer(this.renderer, annotation) + } else if (annotation.type === 'line' && renderer instanceof LineAnnotationRenderer) { + renderer.update(annotation) + } else if (annotation.type === 'line') { + renderer?.delete() + lookup[annotation.id] = new LineAnnotationRenderer(this.renderer, annotation) } } diff --git a/src/renderers/webgl/annotations/LineAnnotationRenderer.ts b/src/renderers/webgl/annotations/LineAnnotationRenderer.ts new file mode 100644 index 0000000..8c13e0a --- /dev/null +++ b/src/renderers/webgl/annotations/LineAnnotationRenderer.ts @@ -0,0 +1,158 @@ +import { PointTuple, LineAnnotation, Coordinates, LinePoints } from '../../../types' +import { DEFAULT_LABEL_STYLE, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' +import { angle, distance } from '../../../utils/api' +import { midPoint } from '../../../utils/webgl' +import { Renderer } from '..' +import LineSegment from '../objects/line/LineSegment' +import LineStrokes from '../objects/line/LineStrokes' +import Text from '../objects/text/Text' + +export default class LineAnnotationRenderer { + annotation: LineAnnotation + + private _points: LinePoints = [0, 0, 0, 0] + private center: PointTuple = [0, 0] + private length = 0 + private theta = 0 + + private fill: LineSegment + private strokes: LineStrokes + private text?: Text + + constructor( + private renderer: Renderer, + annotation: LineAnnotation + ) { + this.renderer = renderer + this.fill = new LineSegment(renderer.annotationsContainer, annotation.style) + this.strokes = new LineStrokes(renderer.annotationsContainer, this.fill, annotation.style.stroke) + this.annotation = annotation + this.setPoints(annotation.points) + } + + update(annotation: LineAnnotation) { + this.annotation = annotation + + this.fill.update(annotation.style.color, annotation.style.width, annotation.style.opacity) + this.strokes.update(annotation.style.stroke) + + if (this.text) { + if (annotation.content) { + this.text.update(annotation.content, annotation.style.text) + } else { + this.text = this.managers.text.delete(this.text) + } + } + + if ( + this._points[0] !== annotation.points[0].x || + this._points[1] !== annotation.points[0].y || + this._points[2] !== annotation.points[1].x || + this._points[3] !== annotation.points[1].y + ) { + this.setPoints(annotation.points) + } + + return this + } + + render() { + const isVisible = this.visible() + + if (isVisible && this.annotation.content && !this.text) { + this.text = new Text( + this.renderer.assets, + this.renderer.annotationsContainer, + this.annotation.content, + this.annotation.style.text, + DEFAULT_LABEL_STYLE + ) + this.text.offset = this.width + this.text.rotate(this.theta).moveTo(...this.center) + } + + const fillMounted = this.managers.annotations.isMounted(this.fill) + + if (isVisible && !fillMounted) { + this.managers.annotations.mount(this.fill) + } else if (!isVisible && fillMounted) { + this.managers.annotations.unmount(this.fill) + } + + const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM + const strokesMounted = this.managers.annotations.isMounted(this.strokes) + + if (shouldStrokesMount && !strokesMounted) { + this.managers.annotations.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.annotations.unmount(this.strokes) + } + + if (this.text) { + const textMounted = this.managers.text.isMounted(this.text) + + if (isVisible && !textMounted) { + this.managers.text.mount(this.text) + } else if (!isVisible && textMounted) { + this.managers.text.unmount(this.text) + } + } + + return this + } + + delete() { + this.managers.annotations.delete(this.fill) + this.managers.annotations.delete(this.strokes) + if (this.text) { + this.managers.text.delete(this.text) + this.text = undefined + } + } + + get x() { + return this._points[0] + } + + get y() { + return this._points[1] + } + + get width() { + return this.strokes.width + } + + private setPoints([a, b]: [Coordinates, Coordinates]) { + const points = a.x > b.x ? [a, b] : [b, a] + this._points = [points[0].x, points[0].y, points[1].x, points[1].y] + this.theta = angle(...this._points) + this.center = midPoint(...this._points) + this.length = distance(...this._points) + + this.fill.rotate(this.theta).resize(this.length).moveTo(this.x, this.y) + this.strokes.rotate(this.theta).resize(this.length).moveTo(this.x, this.y) + + if (this.text) { + this.text.offset = this.width + this.text.rotate(this.theta).moveTo(...this.center) + } + + return this + } + + private visible() { + const [x0, y0, x1, y1] = this._points + const [minX, minY, maxX, maxY] = [Math.min(x0, x1), Math.min(y0, y1), Math.max(x0, x1), Math.max(y0, y1)] + return ( + this.renderer.zoom > MIN_ANNOTATION_ZOOM && + maxX >= this.renderer.minX && + minX <= this.renderer.maxX && + maxY >= this.renderer.minY && + minY <= this.renderer.maxY + ) + } + + private get managers() { + return this.renderer.managers + } +} diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts index 8d0af73..4fb06e5 100644 --- a/src/renderers/webgl/edge.ts +++ b/src/renderers/webgl/edge.ts @@ -6,17 +6,16 @@ import { MIN_LABEL_ZOOM, MIN_STROKE_ZOOM } from '../../utils/constants' -import { type Renderer } from '.' -import { midPoint } from './utils' -import { movePoint } from './utils' -import { NodeRenderer } from './node' import type { ArrowStyle, Edge, PointTuple } from '../../types' +import { FederatedPointerEvent } from 'pixi.js' +import { midPoint, movePoint } from '../../utils/webgl' +import { angle, distance } from '../../utils/api' +import { NodeRenderer } from './node' +import { type Renderer } from '.' +import { EdgeHitArea } from './interaction/edgeHitArea' import Arrow from './objects/Arrow' import LineSegment from './objects/line/LineSegment' import LineStrokes from './objects/line/LineStrokes' -import { FederatedPointerEvent } from 'pixi.js' -import { EdgeHitArea } from './interaction/edgeHitArea' -import { angle, distance } from '../../utils/api' import Text from './objects/text/Text' export class EdgeRenderer { @@ -90,8 +89,7 @@ export class EdgeRenderer { if (this.label) { if (edge.label === undefined || edge.label.trim() === '') { - this.managers.labels.delete(this.label) - this.label = undefined + this.label = this.managers.labels.delete(this.label) } else { this.label.update(edge.label, edge.style?.label) } @@ -235,11 +233,14 @@ export class EdgeRenderer { this.managers.edges.delete(this.lineSegment) this.managers.edges.delete(this.strokes) this.managers.interactions.delete(this.hitArea) + if (this.label) { + this.label = this.managers.labels.delete(this.label) + } if (this.forwardArrow) { - this.managers.arrows.delete(this.forwardArrow) + this.forwardArrow = this.managers.arrows.delete(this.forwardArrow) } if (this.reverseArrow) { - this.managers.arrows.delete(this.reverseArrow) + this.reverseArrow = this.managers.arrows.delete(this.reverseArrow) } } diff --git a/src/renderers/webgl/index.ts b/src/renderers/webgl/index.ts index 959d5c8..0935ad4 100644 --- a/src/renderers/webgl/index.ts +++ b/src/renderers/webgl/index.ts @@ -9,7 +9,7 @@ import { Grid } from './grid' import { NodeRenderer } from './node' import { EdgeRenderer } from './edge' import { interpolate } from '../../utils/helpers' -import { logUnknownEdgeError } from './utils' +import { logUnknownEdgeError } from '../../utils/webgl' import ArrowTexture from './textures/ArrowTexture' import CircleTexture from './textures/CircleTexture' import TextIconTexture from './textures/TextIconTexture' diff --git a/src/renderers/webgl/interaction/edgeHitArea.ts b/src/renderers/webgl/interaction/edgeHitArea.ts index 5bb2f18..64b7d9b 100644 --- a/src/renderers/webgl/interaction/edgeHitArea.ts +++ b/src/renderers/webgl/interaction/edgeHitArea.ts @@ -1,6 +1,7 @@ import { Container, Polygon } from 'pixi.js' -import { HALF_PI, movePoint } from '../utils' +import { movePoint } from '../../../utils/webgl' import { EdgeRenderer } from '../edge' +import { HALF_PI } from '../../../utils/constants' const MIN_LINE_HOVER_RADIUS = 2 diff --git a/src/renderers/webgl/objects/line/LineSegment.ts b/src/renderers/webgl/objects/line/LineSegment.ts index c025fe0..2809451 100644 --- a/src/renderers/webgl/objects/line/LineSegment.ts +++ b/src/renderers/webgl/objects/line/LineSegment.ts @@ -1,6 +1,6 @@ import { Container, Sprite, Texture } from 'pixi.js' import { Dimensions, RenderObject, Stroke } from '../../../../types' -import { DEFAULT_FILL, DEFAULT_OPACITY, DEFAULT_STROKE_STYLE, DEFAULT_STROKE_WIDTH, HALF_PI } from '../../../../utils/constants' +import { DEFAULT_FILL, DEFAULT_OPACITY, DEFAULT_STROKE_WIDTH, HALF_PI } from '../../../../utils/constants' import { isNumber } from '../../../../utils/helpers' // TODO -> let LineSegment own arrow rendering @@ -11,10 +11,14 @@ export default class LineSegment implements RenderObject { private y = 0 private length = 0 private object: Sprite - private style: Required = DEFAULT_STROKE_STYLE + private style: Required - constructor(private container: Container) { + constructor( + private container: Container, + { color = DEFAULT_FILL, width = DEFAULT_STROKE_WIDTH, opacity = DEFAULT_OPACITY }: Partial = {} + ) { this.container = container + this.style = { color, width, opacity } this.object = this.create() } diff --git a/src/renderers/webgl/objects/line/LineStrokes.ts b/src/renderers/webgl/objects/line/LineStrokes.ts index 49dbfeb..76d7af3 100644 --- a/src/renderers/webgl/objects/line/LineStrokes.ts +++ b/src/renderers/webgl/objects/line/LineStrokes.ts @@ -13,16 +13,15 @@ export default class LineStrokes implements RenderObject { private minWidth = 0 private maxWidth = 0 private objects: LineSegment[] = [] - private strokes: Stroke[] = [] constructor( private container: Container, - private fill: LineSegment + private fill: LineSegment, + private strokes: Stroke[] = [] ) { this.container = container this.fill = fill - this.minWidth = this.fill.size.width - this.maxWidth = this.minWidth + this.applyStrokes(strokes) } update(strokes: Stroke[] = []) { @@ -139,9 +138,9 @@ export default class LineStrokes implements RenderObject { this.minWidth = this.fill.size.width this.maxWidth = this.minWidth - for (const { color, width, opacity } of strokes) { - const object = new LineSegment(this.container) - this.objects.push(object.update(color, this.increment(width), opacity).rotate(this.angle).resize(this.length).moveTo(this.x, this.y)) + for (const stroke of strokes) { + const object = new LineSegment(this.container, { ...stroke, width: this.increment(stroke.width) }) + this.objects.push(object.rotate(this.angle).resize(this.length).moveTo(this.x, this.y)) } return this diff --git a/src/renderers/webgl/utils.ts b/src/renderers/webgl/utils.ts deleted file mode 100644 index 412adaf..0000000 --- a/src/renderers/webgl/utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable no-console */ -import type { Node, PointTuple } from '../../types' - -export const HALF_PI = Math.PI / 2 - -export const TWO_PI = Math.PI * 2 - -export const THREE_HALF_PI = HALF_PI * 3 - -export const RADIANS_PER_DEGREE = Math.PI / 180 - -export const logUnknownEdgeError = (source: Node | undefined, target: Node | undefined) => { - if (source === undefined && target === undefined) { - console.error(`Error: Cannot render edge between unknown nodes ${source} and ${target}`) - } else if (source === undefined) { - console.error(`Error: Cannot render edge from unknown node ${source}`) - } else if (target === undefined) { - console.error(`Error: Cannot render edge to unknown node ${target}`) - } -} - -export const movePoint = (x: number, y: number, angle: number, distance: number): PointTuple => [ - x + Math.cos(angle) * distance, - y + Math.sin(angle) * distance -] - -export const midPoint = (x0: number, y0: number, x1: number, y1: number): PointTuple => [(x0 + x1) / 2, (y0 + y1) / 2] - -export const length = (x0: number, y0: number, x1: number, y1: number) => Math.hypot(x1 - x0, y1 - y0) diff --git a/src/types/api.ts b/src/types/api.ts index 84f70e7..7e08b95 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -3,6 +3,8 @@ export type Bounds = { left: number; top: number; right: number; bottom: number export type Dimensions = { width: number; height: number } +export type Coordinates = { x: number; y: number } + export type Viewport = { x: number; y: number; zoom: number } // style @@ -35,7 +37,7 @@ export type TextStyle = Partial<{ align: TextAlign }> -export type LabelStyle = Omit & { +export type LabelStyle = Omit & { position?: Exclude } @@ -110,23 +112,18 @@ export type Edge = { } // annotations -export type AnnotationStyle = FillStyle & { - stroke?: Stroke[] - text?: Omit -} - -export type TextAnnotationStyle = AnnotationStyle & { - text?: Omit - padding?: number | [px: number, py: number] -} - type AnnotationBase = { type: Type id: string x: number y: number - resize?: boolean content?: string + resize?: boolean +} + +export type AnnotationStyle = FillStyle & { + stroke?: Stroke[] + text?: Omit } export type CircleAnnotation = AnnotationBase<'circle'> & { @@ -140,4 +137,14 @@ export type RectangleAnnotation = AnnotationBase<'rectangle'> & { style: AnnotationStyle } -export type Annotation = CircleAnnotation | RectangleAnnotation +export type LineAnnotationStyle = Stroke & { + stroke?: Stroke[] + text?: EdgeLabelStyle +} + +export type LineAnnotation = Omit, 'x' | 'y'> & { + points: [{ x: number; y: number }, { x: number; y: number }] + style: LineAnnotationStyle +} + +export type Annotation = CircleAnnotation | RectangleAnnotation | LineAnnotation diff --git a/src/types/internal.ts b/src/types/internal.ts index 4c9e775..7780398 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -8,6 +8,8 @@ export type TextObject = PixiText | BitmapText export type PointTuple = [x: number, y: number] +export type LinePoints = [x0: number, y0: number, x1: number, y1: number] + export type Texture = { get(...args: unknown[]): RenderTexture delete(): void diff --git a/src/utils/api.ts b/src/utils/api.ts index 987211b..017c8b5 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,4 +1,4 @@ -import { TWO_PI } from '../renderers/webgl/utils' +import { TWO_PI } from './constants' import type { Node, Annotation, Edge, Bounds, Viewport, Dimensions } from '../types' export const getSelectionBounds = (elements: (Node | Annotation)[], padding: number = 0): Bounds => { diff --git a/src/utils/webgl.ts b/src/utils/webgl.ts index 8f04e7c..132ec0f 100644 --- a/src/utils/webgl.ts +++ b/src/utils/webgl.ts @@ -1,5 +1,28 @@ import { Application, Renderer, Container, Matrix, RenderTexture, IBaseTextureOptions, MSAA_QUALITY, SCALE_MODES } from 'pixi.js' import { DEFAULT_RESOLUTION } from './constants' +import { Node, PointTuple } from '../types' + +export const logUnknownEdgeError = (source: Node | undefined, target: Node | undefined) => { + if (source === undefined && target === undefined) { + // eslint-disable-next-line no-console + console.error(`Error: Cannot render edge between unknown nodes ${source} and ${target}`) + } else if (source === undefined) { + // eslint-disable-next-line no-console + console.error(`Error: Cannot render edge from unknown node ${source}`) + } else if (target === undefined) { + // eslint-disable-next-line no-console + console.error(`Error: Cannot render edge to unknown node ${target}`) + } +} + +export const movePoint = (x: number, y: number, angle: number, distance: number): PointTuple => [ + x + Math.cos(angle) * distance, + y + Math.sin(angle) * distance +] + +export const midPoint = (x0: number, y0: number, x1: number, y1: number): PointTuple => [(x0 + x1) / 2, (y0 + y1) / 2] + +export const length = (x0: number, y0: number, x1: number, y1: number) => Math.hypot(x1 - x0, y1 - y0) export const createRenderTexture = ( app: A, From 39befbb4fb89bf619ab4a3a03e56ea00052652d9 Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Thu, 15 Feb 2024 11:59:25 -0500 Subject: [PATCH 10/12] use basic texture for rectangles --- examples/native/src/annotations/index.ts | 16 +++---- .../RectangleAnnotationRenderer.ts | 22 +++++----- .../webgl/objects/rectangle/Rectangle.ts | 43 +++++++++---------- .../objects/rectangle/RectangleStrokes.ts | 19 ++++---- 4 files changed, 46 insertions(+), 54 deletions(-) diff --git a/examples/native/src/annotations/index.ts b/examples/native/src/annotations/index.ts index 8e30f50..dcc65bb 100644 --- a/examples/native/src/annotations/index.ts +++ b/examples/native/src/annotations/index.ts @@ -12,8 +12,8 @@ const annotations: Graph.Annotation[] = [ { id: 'rect-anno-0', type: 'rectangle', - width: 200, - height: 200, + width: 100, + height: 100, x: -100, y: -100, style: { color: GREEN } @@ -21,8 +21,8 @@ const annotations: Graph.Annotation[] = [ { id: 'rect-anno-2', type: 'rectangle', - width: 200, - height: 200, + width: 100, + height: 100, x: 0, y: -100, style: { @@ -32,8 +32,8 @@ const annotations: Graph.Annotation[] = [ { id: 'rect-anno-3', type: 'rectangle', - width: 200, - height: 200, + width: 100, + height: 100, x: -100, y: 0, style: { @@ -43,8 +43,8 @@ const annotations: Graph.Annotation[] = [ { id: 'rect-anno-4', type: 'rectangle', - width: 200, - height: 200, + width: 100, + height: 100, x: 0, y: 0, style: { diff --git a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts index 7b7da6f..eb83107 100644 --- a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts @@ -1,5 +1,5 @@ import { InterpolateFn, PointTuple, RectangleAnnotation } from '../../../types' -import { DEFAULT_ANIMATE_RESIZE, DEFAULT_RESOLUTION, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' +import { DEFAULT_ANIMATE_RESIZE, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' import { interpolate } from '../../../utils/helpers' import { Renderer } from '..' import RectangleStrokes from '../objects/rectangle/RectangleStrokes' @@ -28,8 +28,8 @@ export default class RectangleAnnotationRenderer { annotation: RectangleAnnotation ) { this.renderer = renderer - this.fill = new Rectangle(renderer.annotationsContainer, renderer.rectangle, annotation.style) - this.strokes = new RectangleStrokes(renderer.annotationsContainer, renderer.rectangle, this.fill, annotation.style.stroke) + this.fill = new Rectangle(renderer.annotationsContainer, annotation.style) + this.strokes = new RectangleStrokes(renderer.annotationsContainer, this.fill, annotation.style.stroke) this.resize(annotation.width, annotation.height).moveTo(annotation.x, annotation.y) this.annotation = annotation } @@ -135,13 +135,12 @@ export default class RectangleAnnotationRenderer { const isVisible = this.visible() if (isVisible && this.annotation.content && !this.text) { - const [hw, hh] = this.halfSize this.text = new Text( this.renderer.assets, this.renderer.annotationsContainer, this.annotation.content, this.annotation.style.text - ).moveTo(this.x + hw / this.resolution, this.y + hh / this.resolution) + ).moveTo(...this.center) } const fillMounted = this.managers.annotations.isMounted(this.fill) @@ -200,8 +199,7 @@ export default class RectangleAnnotationRenderer { this.y = y this.fill.moveTo(x, y) this.strokes.moveTo(x, y) - const [hw, hh] = this.halfSize - this.text?.moveTo(x + hw / this.resolution, y + hh / this.resolution) + this.text?.moveTo(...this.center) } return this } @@ -222,12 +220,12 @@ export default class RectangleAnnotationRenderer { return [this.width / 2, this.height / 2] } - private get managers() { - return this.renderer.managers + private get center(): PointTuple { + const [halfWidth, halfHeight] = this.halfSize + return [this.x + halfWidth, this.y + halfHeight] } - private get resolution() { - // TODO - implement resolution in options - return DEFAULT_RESOLUTION + private get managers() { + return this.renderer.managers } } diff --git a/src/renderers/webgl/objects/rectangle/Rectangle.ts b/src/renderers/webgl/objects/rectangle/Rectangle.ts index 4b85953..d93a618 100644 --- a/src/renderers/webgl/objects/rectangle/Rectangle.ts +++ b/src/renderers/webgl/objects/rectangle/Rectangle.ts @@ -1,8 +1,7 @@ import { Dimensions, FillStyle, RenderObject } from '../../../../types' import { DEFAULT_FILL, DEFAULT_OPACITY } from '../../../../utils/constants' -import { Container, Sprite } from 'pixi.js' +import { Container, Sprite, Texture } from 'pixi.js' import { isNumber } from '../../../../utils/helpers' -import RectangleTexture from '../../textures/RectangleTexture' export default class Rectangle implements RenderObject { mounted = false @@ -16,14 +15,11 @@ export default class Rectangle implements RenderObject { constructor( private container: Container, - private texture: RectangleTexture, - { color = DEFAULT_FILL, opacity = DEFAULT_OPACITY }: Partial = {}, - index?: number + { color = DEFAULT_FILL, opacity = DEFAULT_OPACITY }: Partial = {} ) { this.container = container - this.texture = texture this.style = { color, opacity } - this.object = this.create(index) + this.object = this.create() } update(color = DEFAULT_FILL, opacity = DEFAULT_OPACITY) { @@ -49,20 +45,28 @@ export default class Rectangle implements RenderObject { } resize({ width, height }: Dimensions) { - if (this.width !== width || this.height !== height) { + if (this.width !== width) { this.width = width + this.object.width = width + } + + if (this.height !== height) { this.height = height - this.object.scale.set(width / this.texture.scaleFactor, height / this.texture.scaleFactor) + this.object.height = height } return this } - mount() { - // TODO - why is mounting/unmouting fill Sprite less efficient? + mount(index?: number) { if (!this.mounted) { this.mounted = true - this.object.visible = this.mounted + + if (isNumber(index)) { + this.container.addChildAt(this.object, index) + } else { + this.container.addChild(this.object) + } } return this @@ -71,7 +75,7 @@ export default class Rectangle implements RenderObject { unmount() { if (this.mounted) { this.mounted = false - this.object.visible = this.mounted + this.container.removeChild(this.object) } return this @@ -97,22 +101,15 @@ export default class Rectangle implements RenderObject { return this.object.anchor } - private create(index?: number) { - const object = new Sprite(this.texture.get()) + private create() { + const object = new Sprite(Texture.WHITE) - object.anchor.set(0.5, 0.5) + object.anchor.set(0, 0) object.x = this.x object.y = this.y - object.visible = this.mounted object.tint = this.style.color object.alpha = this.style.opacity - if (isNumber(index)) { - this.container.addChildAt(object, index) - } else { - this.container.addChild(object) - } - return object } } diff --git a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts index 8b9f37c..7cfbdc5 100644 --- a/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts +++ b/src/renderers/webgl/objects/rectangle/RectangleStrokes.ts @@ -1,7 +1,6 @@ import { Dimensions, RenderObject, Stroke } from '../../../../types' import { Container } from 'pixi.js' import { equals } from '../../../../utils/api' -import RectangleTexture from '../../textures/RectangleTexture' import Rectangle from './Rectangle' export default class RectangleStrokes implements RenderObject { @@ -16,12 +15,10 @@ export default class RectangleStrokes implements RenderObject { constructor( private container: Container, - private texture: RectangleTexture, private fill: Rectangle, private strokes: Stroke[] = [] ) { this.container = container - this.texture = texture this.fill = fill this.applyStrokes(strokes) } @@ -51,8 +48,8 @@ export default class RectangleStrokes implements RenderObject { let _y = y for (let i = 0; i < this.strokes.length; i += 1) { - _x -= this.strokes[i].width / 2 - _y -= this.strokes[i].width / 2 + _x -= this.strokes[i].width + _y -= this.strokes[i].width this.objects[i].moveTo(_x, _y) } } @@ -77,8 +74,10 @@ export default class RectangleStrokes implements RenderObject { if (!this.mounted) { this.mounted = true + const index = this.fill.getContainerIndex() + for (const object of this.objects) { - object.mount() + object.mount(index) } } @@ -127,15 +126,13 @@ export default class RectangleStrokes implements RenderObject { this.minSize = this.fill.size this.maxSize = this.minSize - const index = this.fill.getContainerIndex() - let x = this.x let y = this.y for (const { width, ...style } of strokes) { - x -= width / 2 - y -= width / 2 - const object = new Rectangle(this.container, this.texture, style, index) + x -= width + y -= width + const object = new Rectangle(this.container, style) this.objects.push(object.resize(this.increment(width)).moveTo(x, y)) } From fe23965951f27041270241e617989d1c6637b60c Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Thu, 15 Feb 2024 13:59:34 -0500 Subject: [PATCH 11/12] lazily instantiate strokes when visible --- .../annotations/CircleAnnotationRenderer.ts | 38 +++++--- .../annotations/LineAnnotationRenderer.ts | 86 ++++++++++++------- .../RectangleAnnotationRenderer.ts | 34 +++++--- src/renderers/webgl/edge.ts | 44 +++++++--- src/renderers/webgl/node.ts | 40 ++++++--- .../webgl/objects/line/LineSegment.ts | 18 ++-- .../webgl/objects/line/LineStrokes.ts | 7 +- 7 files changed, 177 insertions(+), 90 deletions(-) diff --git a/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts b/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts index 7ced5f6..f8951c8 100644 --- a/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/CircleAnnotationRenderer.ts @@ -14,7 +14,7 @@ export default class CircleAnnotationRenderer { private _radius = 0 private fill: Circle - private strokes: CircleStrokes + private strokes?: CircleStrokes private text?: Text private interpolateX?: InterpolateFn @@ -27,7 +27,6 @@ export default class CircleAnnotationRenderer { ) { this.renderer = renderer this.fill = new Circle(renderer.annotationsContainer, renderer.circle, annotation.style) - this.strokes = new CircleStrokes(renderer.annotationsContainer, renderer.circle, this.fill, annotation.style.stroke) this.resize(annotation.radius).moveTo(annotation.x, annotation.y) this.annotation = annotation } @@ -36,7 +35,7 @@ export default class CircleAnnotationRenderer { this.annotation = annotation this.fill.update(annotation.style.color, annotation.style.opacity) - this.strokes.update(annotation.style.stroke) + this.strokes?.update(annotation.style.stroke) if (annotation.content && this.text) { this.text.update(annotation.content, annotation.style.text) @@ -127,12 +126,23 @@ export default class CircleAnnotationRenderer { } const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM - const strokesMounted = this.managers.annotations.isMounted(this.strokes) + if (shouldStrokesMount && !this.strokes && this.annotation.style.stroke) { + this.strokes = new CircleStrokes( + this.renderer.annotationsContainer, + this.renderer.circle, + this.fill, + this.annotation.style.stroke + ).moveTo(this.x, this.y) + } + + if (this.strokes) { + const strokesMounted = this.managers.annotations.isMounted(this.strokes) - if (shouldStrokesMount && !strokesMounted) { - this.managers.annotations.mount(this.strokes) - } else if (!shouldStrokesMount && strokesMounted) { - this.managers.annotations.unmount(this.strokes) + if (shouldStrokesMount && !strokesMounted) { + this.managers.annotations.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.annotations.unmount(this.strokes) + } } if (this.text) { @@ -150,7 +160,11 @@ export default class CircleAnnotationRenderer { delete() { this.managers.annotations.delete(this.fill) - this.managers.annotations.delete(this.strokes) + + if (this.strokes) { + this.strokes = this.managers.annotations.delete(this.strokes) + } + if (this.text) { this.text = this.managers.text.delete(this.text) } @@ -158,7 +172,7 @@ export default class CircleAnnotationRenderer { } private get radius() { - return this.strokes.radius + return this.strokes?.radius ?? this._radius } private get managers() { @@ -169,7 +183,7 @@ export default class CircleAnnotationRenderer { if (radius !== this._radius) { this._radius = radius this.fill.resize(radius) - this.strokes.resize(radius) + this.strokes?.resize(radius) } return this @@ -180,7 +194,7 @@ export default class CircleAnnotationRenderer { this.x = x this.y = y this.fill.moveTo(x, y) - this.strokes.moveTo(x, y) + this.strokes?.moveTo(x, y) this.text?.moveTo(x, y) } diff --git a/src/renderers/webgl/annotations/LineAnnotationRenderer.ts b/src/renderers/webgl/annotations/LineAnnotationRenderer.ts index 8c13e0a..32eabc1 100644 --- a/src/renderers/webgl/annotations/LineAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/LineAnnotationRenderer.ts @@ -1,4 +1,4 @@ -import { PointTuple, LineAnnotation, Coordinates, LinePoints } from '../../../types' +import { LineAnnotation, Coordinates, LinePoints } from '../../../types' import { DEFAULT_LABEL_STYLE, MIN_ANNOTATION_ZOOM, MIN_STROKE_ZOOM } from '../../../utils/constants' import { angle, distance } from '../../../utils/api' import { midPoint } from '../../../utils/webgl' @@ -11,12 +11,11 @@ export default class LineAnnotationRenderer { annotation: LineAnnotation private _points: LinePoints = [0, 0, 0, 0] - private center: PointTuple = [0, 0] private length = 0 private theta = 0 private fill: LineSegment - private strokes: LineStrokes + private strokes?: LineStrokes private text?: Text constructor( @@ -24,9 +23,8 @@ export default class LineAnnotationRenderer { annotation: LineAnnotation ) { this.renderer = renderer - this.fill = new LineSegment(renderer.annotationsContainer, annotation.style) - this.strokes = new LineStrokes(renderer.annotationsContainer, this.fill, annotation.style.stroke) this.annotation = annotation + this.fill = new LineSegment(renderer.annotationsContainer, annotation.style) this.setPoints(annotation.points) } @@ -34,7 +32,10 @@ export default class LineAnnotationRenderer { this.annotation = annotation this.fill.update(annotation.style.color, annotation.style.width, annotation.style.opacity) - this.strokes.update(annotation.style.stroke) + + if (this.strokes) { + this.strokes.update(annotation.style.stroke) + } if (this.text) { if (annotation.content) { @@ -44,14 +45,7 @@ export default class LineAnnotationRenderer { } } - if ( - this._points[0] !== annotation.points[0].x || - this._points[1] !== annotation.points[0].y || - this._points[2] !== annotation.points[1].x || - this._points[3] !== annotation.points[1].y - ) { - this.setPoints(annotation.points) - } + this.setPoints(annotation.points) return this } @@ -68,7 +62,7 @@ export default class LineAnnotationRenderer { DEFAULT_LABEL_STYLE ) this.text.offset = this.width - this.text.rotate(this.theta).moveTo(...this.center) + this.text.rotate(this.theta).moveTo(...this.getCenter()) } const fillMounted = this.managers.annotations.isMounted(this.fill) @@ -80,12 +74,18 @@ export default class LineAnnotationRenderer { } const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM - const strokesMounted = this.managers.annotations.isMounted(this.strokes) - if (shouldStrokesMount && !strokesMounted) { - this.managers.annotations.mount(this.strokes) - } else if (!shouldStrokesMount && strokesMounted) { - this.managers.annotations.unmount(this.strokes) + if (shouldStrokesMount && !this.strokes && this.annotation.style.stroke) { + this.strokes = new LineStrokes(this.renderer.annotationsContainer, this.fill, this.annotation.style.stroke).moveTo(this.x, this.y) + } + + if (this.strokes) { + const strokesMounted = this.managers.annotations.isMounted(this.strokes) + if (shouldStrokesMount && !strokesMounted) { + this.managers.annotations.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.annotations.unmount(this.strokes) + } } if (this.text) { @@ -103,7 +103,11 @@ export default class LineAnnotationRenderer { delete() { this.managers.annotations.delete(this.fill) - this.managers.annotations.delete(this.strokes) + + if (this.strokes) { + this.strokes = this.managers.annotations.delete(this.strokes) + } + if (this.text) { this.managers.text.delete(this.text) this.text = undefined @@ -119,22 +123,36 @@ export default class LineAnnotationRenderer { } get width() { - return this.strokes.width + return this.strokes?.width ?? this.fill.width + } + + private compare(points: [Coordinates, Coordinates]) { + return ( + this._points[0] === points[0].x && + this._points[1] === points[0].y && + this._points[2] === points[1].x && + this._points[3] === points[1].y + ) } private setPoints([a, b]: [Coordinates, Coordinates]) { - const points = a.x > b.x ? [a, b] : [b, a] - this._points = [points[0].x, points[0].y, points[1].x, points[1].y] - this.theta = angle(...this._points) - this.center = midPoint(...this._points) - this.length = distance(...this._points) + const points: [Coordinates, Coordinates] = a.x > b.x ? [a, b] : [b, a] - this.fill.rotate(this.theta).resize(this.length).moveTo(this.x, this.y) - this.strokes.rotate(this.theta).resize(this.length).moveTo(this.x, this.y) + if (!this.compare(points)) { + this._points = [points[0].x, points[0].y, points[1].x, points[1].y] - if (this.text) { - this.text.offset = this.width - this.text.rotate(this.theta).moveTo(...this.center) + this.theta = angle(...this._points) + this.length = distance(...this._points) + this.fill.rotate(this.theta).resize(this.length).moveTo(this.x, this.y) + + if (this.strokes) { + this.strokes.rotate(this.theta).resize(this.length).moveTo(this.x, this.y) + } + + if (this.text) { + this.text.offset = this.width + this.text.rotate(this.theta).moveTo(...this.getCenter()) + } } return this @@ -152,6 +170,10 @@ export default class LineAnnotationRenderer { ) } + private getCenter() { + return midPoint(...this._points) + } + private get managers() { return this.renderer.managers } diff --git a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts index eb83107..7d55fdb 100644 --- a/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts +++ b/src/renderers/webgl/annotations/RectangleAnnotationRenderer.ts @@ -15,7 +15,7 @@ export default class RectangleAnnotationRenderer { private height = 0 private fill: Rectangle - private strokes: RectangleStrokes + private strokes?: RectangleStrokes private text?: Text private interpolateX?: InterpolateFn @@ -29,7 +29,6 @@ export default class RectangleAnnotationRenderer { ) { this.renderer = renderer this.fill = new Rectangle(renderer.annotationsContainer, annotation.style) - this.strokes = new RectangleStrokes(renderer.annotationsContainer, this.fill, annotation.style.stroke) this.resize(annotation.width, annotation.height).moveTo(annotation.x, annotation.y) this.annotation = annotation } @@ -38,7 +37,7 @@ export default class RectangleAnnotationRenderer { this.annotation = annotation this.fill.update(annotation.style.color, annotation.style.opacity) - this.strokes.update(annotation.style.stroke) + this.strokes?.update(annotation.style.stroke) if (annotation.content && this.text) { this.text.update(annotation.content, annotation.style.text) @@ -152,12 +151,21 @@ export default class RectangleAnnotationRenderer { } const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM - const strokesMounted = this.managers.annotations.isMounted(this.strokes) + if (shouldStrokesMount && !this.strokes && this.annotation.style.stroke) { + this.strokes = new RectangleStrokes(this.renderer.annotationsContainer, this.fill, this.annotation.style.stroke).moveTo( + this.x, + this.y + ) + } + + if (this.strokes) { + const strokesMounted = this.managers.annotations.isMounted(this.strokes) - if (shouldStrokesMount && !strokesMounted) { - this.managers.annotations.mount(this.strokes) - } else if (!shouldStrokesMount && strokesMounted) { - this.managers.annotations.unmount(this.strokes) + if (shouldStrokesMount && !strokesMounted) { + this.managers.annotations.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.annotations.unmount(this.strokes) + } } if (this.text) { @@ -175,7 +183,11 @@ export default class RectangleAnnotationRenderer { delete() { this.managers.annotations.delete(this.fill) - this.managers.annotations.delete(this.strokes) + + if (this.strokes) { + this.strokes = this.managers.annotations.delete(this.strokes) + } + if (this.text) { this.managers.text.delete(this.text) this.text = undefined @@ -187,7 +199,7 @@ export default class RectangleAnnotationRenderer { this.width = width this.height = height this.fill.resize({ width, height }) - this.strokes.resize({ width, height }) + this.strokes?.resize({ width, height }) } return this @@ -198,7 +210,7 @@ export default class RectangleAnnotationRenderer { this.x = x this.y = y this.fill.moveTo(x, y) - this.strokes.moveTo(x, y) + this.strokes?.moveTo(x, y) this.text?.moveTo(...this.center) } return this diff --git a/src/renderers/webgl/edge.ts b/src/renderers/webgl/edge.ts index 4fb06e5..6bdaa65 100644 --- a/src/renderers/webgl/edge.ts +++ b/src/renderers/webgl/edge.ts @@ -33,8 +33,8 @@ export class EdgeRenderer { private theta = 0 private center: PointTuple = [0, 0] private lineSegment: LineSegment - private strokes: LineStrokes private hitArea: EdgeHitArea + private strokes?: LineStrokes private label?: Text private forwardArrow?: Arrow private reverseArrow?: Arrow @@ -49,7 +49,6 @@ export class EdgeRenderer { ) { this.renderer = renderer this.lineSegment = new LineSegment(this.renderer.edgesContainer) - this.strokes = new LineStrokes(this.renderer.edgesContainer, this.lineSegment) this.hitArea = new EdgeHitArea(this.renderer.interactionContainer, this) this.update(edge, source, target) } @@ -60,7 +59,10 @@ export class EdgeRenderer { this.target = target this.lineSegment.update(edge.style?.color, edge.style?.width, edge.style?.opacity) - this.strokes.update(edge.style?.stroke) + + if (this.strokes) { + this.strokes.update(edge.style?.stroke) + } const arrow = edge.style?.arrow ?? DEFAULT_ARROW @@ -158,11 +160,13 @@ export class EdgeRenderer { // TODO -> draw hitArea/strokes over arrows this.lineSegment.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0) - this.strokes.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0) - this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.strokes.width, this.theta) + + this.strokes?.rotate(this.theta).resize(this.length).moveTo(edgeX0, edgeY0) + + this.hitArea.update(edgeX0, edgeY0, edgeX1, edgeY1, this.width, this.theta) if (this.label) { - this.label.offset = this.strokes.width + this.label.offset = this.width this.label.rotate(this.theta).moveTo(...this.center) } } @@ -186,11 +190,18 @@ export class EdgeRenderer { } const strokesShouldMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM - const strokesMounted = this.managers.edges.isMounted(this.strokes) - if (strokesShouldMount && !strokesMounted) { - this.managers.edges.mount(this.strokes) - } else if (!strokesShouldMount && strokesMounted) { - this.managers.edges.unmount(this.strokes) + + if (strokesShouldMount && !this.strokes && this.edge.style?.stroke) { + this.strokes = new LineStrokes(this.renderer.edgesContainer, this.lineSegment, this.edge.style.stroke).moveTo(this.x0, this.y0) + } + + if (this.strokes) { + const strokesMounted = this.managers.edges.isMounted(this.strokes) + if (strokesShouldMount && !strokesMounted) { + this.managers.edges.mount(this.strokes) + } else if (!strokesShouldMount && strokesMounted) { + this.managers.edges.unmount(this.strokes) + } } if (this.forwardArrow) { @@ -231,8 +242,11 @@ export class EdgeRenderer { clearTimeout(this.doubleClickTimeout) this.managers.edges.delete(this.lineSegment) - this.managers.edges.delete(this.strokes) this.managers.interactions.delete(this.hitArea) + + if (this.strokes) { + this.strokes = this.managers.edges.delete(this.strokes) + } if (this.label) { this.label = this.managers.labels.delete(this.label) } @@ -244,6 +258,10 @@ export class EdgeRenderer { } } + get width() { + return this.strokes?.width ?? this.lineSegment.width + } + pointerEnter = (event: FederatedPointerEvent) => { if (this.renderer.draggedNode || this.renderer.dragInteraction.dragging || this.renderer.zoomInteraction.zooming) { return @@ -444,7 +462,7 @@ export class EdgeRenderer { const style = this.edge.style?.label if (label !== undefined && label.trim() !== '' && this.label === undefined) { this.label = new Text(this.renderer.assets, this.renderer.labelsContainer, label, style, DEFAULT_LABEL_STYLE) - this.label.offset = this.strokes.width + this.label.offset = this.width this.label.rotate(this.theta).moveTo(...this.center) } diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index b320902..b333408 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -15,7 +15,7 @@ export class NodeRenderer { node: Node fill: Circle - strokes: CircleStrokes + strokes?: CircleStrokes label?: Text icon?: Icon @@ -35,7 +35,6 @@ export class NodeRenderer { constructor(renderer: Renderer, node: Node) { this.renderer = renderer this.fill = new Circle(renderer.nodesContainer, renderer.circle, node.style) - this.strokes = new CircleStrokes(renderer.nodesContainer, renderer.circle, this.fill, node.style?.stroke) this.hitArea = new NodeHitArea(this.renderer.interactionContainer, this) this.resize(node.radius).moveTo(node.x ?? 0, node.y ?? 0) this.node = node @@ -45,7 +44,10 @@ export class NodeRenderer { this.node = node this.fill.update(node.style?.color, node.style?.opacity) - this.strokes.update(node.style?.stroke) + + if (this.strokes) { + this.strokes.update(node.style?.stroke) + } if (this.label) { if (node.label === undefined || node.label.trim() === '') { @@ -168,12 +170,22 @@ export class NodeRenderer { } const shouldStrokesMount = isVisible && this.renderer.zoom > MIN_STROKE_ZOOM - const strokesMounted = this.managers.nodes.isMounted(this.strokes) - if (shouldStrokesMount && !strokesMounted) { - this.managers.nodes.mount(this.strokes) - } else if (!shouldStrokesMount && strokesMounted) { - this.managers.nodes.unmount(this.strokes) + if (shouldStrokesMount && !this.strokes && this.node.style?.stroke) { + this.strokes = new CircleStrokes(this.renderer.nodesContainer, this.renderer.circle, this.fill, this.node.style.stroke).moveTo( + this.x, + this.y + ) + } + + if (this.strokes) { + const strokesMounted = this.managers.nodes.isMounted(this.strokes) + + if (shouldStrokesMount && !strokesMounted) { + this.managers.nodes.mount(this.strokes) + } else if (!shouldStrokesMount && strokesMounted) { + this.managers.nodes.unmount(this.strokes) + } } const shouldLabelMount = isVisible && this.renderer.zoom > MIN_LABEL_ZOOM @@ -211,9 +223,13 @@ export class NodeRenderer { clearTimeout(this.doubleClickTimeout) this.managers.nodes.delete(this.fill) - this.managers.nodes.delete(this.strokes) + this.managers.interactions.delete(this.hitArea) + if (this.strokes) { + this.strokes = this.managers.nodes.delete(this.strokes) + } + if (this.label) { this.managers.labels.delete(this.label) } @@ -223,7 +239,7 @@ export class NodeRenderer { } get radius() { - return this.strokes.radius + return this.strokes?.radius ?? this._radius } pointerEnter = (event: FederatedPointerEvent) => { @@ -506,7 +522,7 @@ export class NodeRenderer { if (radius !== this._radius) { this._radius = radius this.fill.resize(radius) - this.strokes.resize(radius) + this.strokes?.resize(radius) if (this.label) { this.label.offset = this.radius @@ -523,7 +539,7 @@ export class NodeRenderer { this.x = x this.y = y this.fill.moveTo(x, y) - this.strokes.moveTo(x, y) + this.strokes?.moveTo(x, y) this.label?.moveTo(x, y) this.icon?.moveTo(x, y) this.hitArea.update(x, y, this._radius) diff --git a/src/renderers/webgl/objects/line/LineSegment.ts b/src/renderers/webgl/objects/line/LineSegment.ts index 2809451..92eefeb 100644 --- a/src/renderers/webgl/objects/line/LineSegment.ts +++ b/src/renderers/webgl/objects/line/LineSegment.ts @@ -1,5 +1,5 @@ import { Container, Sprite, Texture } from 'pixi.js' -import { Dimensions, RenderObject, Stroke } from '../../../../types' +import { RenderObject, Stroke } from '../../../../types' import { DEFAULT_FILL, DEFAULT_OPACITY, DEFAULT_STROKE_WIDTH, HALF_PI } from '../../../../utils/constants' import { isNumber } from '../../../../utils/helpers' @@ -9,7 +9,7 @@ export default class LineSegment implements RenderObject { private x = 0 private y = 0 - private length = 0 + private _length = 0 private object: Sprite private style: Required @@ -37,8 +37,8 @@ export default class LineSegment implements RenderObject { } resize(length: number) { - if (length !== this.length) { - this.length = length + if (length !== this._length) { + this._length = length this.object.height = length } @@ -96,8 +96,12 @@ export default class LineSegment implements RenderObject { return -1 } - get size(): Dimensions { - return { width: this.style.width, height: this.length } + get width() { + return this.style.width + } + + get length() { + return this._length } private create() { @@ -105,7 +109,7 @@ export default class LineSegment implements RenderObject { object.tint = this.style.color object.alpha = this.style.opacity object.width = this.style.width - object.height = this.length + object.height = this._length object.anchor.set(0.5, 0) return object diff --git a/src/renderers/webgl/objects/line/LineStrokes.ts b/src/renderers/webgl/objects/line/LineStrokes.ts index 76d7af3..99dac0e 100644 --- a/src/renderers/webgl/objects/line/LineStrokes.ts +++ b/src/renderers/webgl/objects/line/LineStrokes.ts @@ -21,6 +21,7 @@ export default class LineStrokes implements RenderObject { ) { this.container = container this.fill = fill + this.length = fill.length this.applyStrokes(strokes) } @@ -34,8 +35,8 @@ export default class LineStrokes implements RenderObject { if (isMounted) { this.mount() } - } else if (this.fill.size.width !== this.minWidth) { - this.minWidth = this.fill.size.width + } else if (this.fill.width !== this.minWidth) { + this.minWidth = this.fill.width this.maxWidth = this.minWidth for (let i = 0; i < this.strokes.length; i += 1) { @@ -135,7 +136,7 @@ export default class LineStrokes implements RenderObject { private applyStrokes(strokes: Stroke[]) { this.objects = [] this.strokes = strokes - this.minWidth = this.fill.size.width + this.minWidth = this.fill.width this.maxWidth = this.minWidth for (const stroke of strokes) { From a30b7bb6425e9677ef284e100b6cbafa848b9a4b Mon Sep 17 00:00:00 2001 From: Mikey Gower <35551399+mggower@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:03:58 -0500 Subject: [PATCH 12/12] Rename arrow.ts to Arrow.ts --- src/renderers/webgl/objects/{arrow.ts => Arrow.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/renderers/webgl/objects/{arrow.ts => Arrow.ts} (100%) diff --git a/src/renderers/webgl/objects/arrow.ts b/src/renderers/webgl/objects/Arrow.ts similarity index 100% rename from src/renderers/webgl/objects/arrow.ts rename to src/renderers/webgl/objects/Arrow.ts