Skip to content

Commit

Permalink
WebGL rendering of Ngon glyph (bokeh#14044)
Browse files Browse the repository at this point in the history
* WebGL rendering of Ngon glyph

* Linting
  • Loading branch information
ianthomas23 authored Aug 28, 2024
1 parent c7368dc commit a28073d
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 24 deletions.
13 changes: 13 additions & 0 deletions bokehjs/src/lib/api/glyph_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
MathMLGlyph as MathML,
MultiLine,
MultiPolygons,
Ngon,
Patch,
Patches,
Quad,
Expand Down Expand Up @@ -167,6 +168,7 @@ export type MarkerArgs = GlyphArgs<Marker.Props> & AuxLine & AuxFi
export type MathMLArgs = GlyphArgs<MathML.Props> & AuxText
export type MultiLineArgs = GlyphArgs<MultiLine.Props> & AuxLine
export type MultiPolygonsArgs = GlyphArgs<MultiPolygons.Props> & AuxLine & AuxFill & AuxHatch
export type NgonArgs = GlyphArgs<Ngon.Props> & AuxLine & AuxFill & AuxHatch
export type PatchArgs = GlyphArgs<Patch.Props> & AuxLine & AuxFill & AuxHatch
export type PatchesArgs = GlyphArgs<Patches.Props> & AuxLine & AuxFill & AuxHatch
export type QuadArgs = GlyphArgs<Quad.Props> & AuxLine & AuxFill & AuxHatch
Expand Down Expand Up @@ -436,6 +438,17 @@ export abstract class GlyphAPI {
return this._glyph(MultiPolygons, "multi_polygons", ["xs", "ys"], args)
}

ngon(): GlyphRenderer<Ngon>
ngon(args: Partial<NgonArgs>): GlyphRenderer<Ngon>
ngon(
x: NgonArgs["x"],
y: NgonArgs["y"],
radius: NgonArgs["radius"],
args?: Partial<NgonArgs>): GlyphRenderer<Ngon>
ngon(...args: unknown[]): GlyphRenderer<Ngon> {
return this._glyph(Ngon, "ngon", ["x", "y", "radius"], args)
}

patch(): GlyphRenderer<Patch>
patch(args: Partial<PatchArgs>): GlyphRenderer<Patch>
patch(
Expand Down
9 changes: 9 additions & 0 deletions bokehjs/src/lib/models/glyphs/ngon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as p from "core/properties"
import type {Arrayable} from "core/types"
import type {Context2d} from "core/util/canvas"
import {Selection} from "../selections/selection"
import type {NgonGL} from "./webgl/ngon"

export interface NgonView extends Ngon.Data {}

Expand All @@ -25,6 +26,14 @@ export class NgonView extends RadialGlyphView {
declare model: Ngon
declare visuals: Ngon.Visuals

/** @internal */
declare glglyph?: NgonGL

override async load_glglyph() {
const {NgonGL} = await import("./webgl/ngon")
return NgonGL
}

protected _paint(ctx: Context2d, indices: number[], data?: Partial<Ngon.Data>): void {
const {sx, sy, sradius, angle, n} = {...this, ...data}

Expand Down
21 changes: 2 additions & 19 deletions bokehjs/src/lib/models/glyphs/webgl/circle.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type {ReglWrapper} from "./regl_wrap"
import type {Float32Buffer} from "./buffer"
import {SXSYGlyphGL} from "./sxsy"
import {RadialGL} from "./radial"
import type {GLMarkerType} from "./types"
import type {CircleView} from "../circle"
import {mul} from "core/util/arrayable"

export class CircleGL extends SXSYGlyphGL {
export class CircleGL extends RadialGL {
constructor(regl_wrapper: ReglWrapper, override readonly glyph: CircleView) {
super(regl_wrapper, glyph)
}
Expand All @@ -14,23 +12,8 @@ export class CircleGL extends SXSYGlyphGL {
return "circle"
}

// TODO: should be 'radius'
get size(): Float32Buffer {
return this._widths
}

protected override _set_data(): void {
super._set_data()

// Ideally we wouldn't multiply here, but currently handling of
// circle glyph and scatter with circle marker is handled with
// a single code path.
this.size.set_from_array(mul(this.glyph.sradius, 2.0))
}

protected override _set_once(): void {
super._set_once()
this._heights.set_from_scalar(0)
this._angles.set_from_scalar(0)
}
}
1 change: 1 addition & 0 deletions bokehjs/src/lib/models/glyphs/webgl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./line_gl"
export * from "./lrtb"
export * from "./multi_line"
export * from "./multi_marker"
export * from "./ngon"
export * from "./rect"
export * from "./single_line"
export * from "./single_marker"
Expand Down
31 changes: 30 additions & 1 deletion bokehjs/src/lib/models/glyphs/webgl/marker.frag
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ varying float v_start_angle;
varying float v_end_angle;
#endif

#ifdef USE_CIRCLE
#if defined(USE_CIRCLE) || defined(USE_NGON)
varying float v_radius;
#endif

#ifdef USE_NGON
varying float v_n;
#endif

#ifdef USE_ROUND_RECT
varying vec4 v_border_radius;
#endif
Expand Down Expand Up @@ -419,6 +423,31 @@ float marker_distance(in vec2 p, in int line_cap, in int line_join)
}
#endif

#ifdef USE_NGON
float marker_distance(in vec2 p, in int line_cap, in int line_join)
{
float side_angle = 2.0*PI / v_n; // Angle subtended by 1 side of ngon at center.

// Use symmetry to transform p around center into first half of first side of ngon.
p.y = -p.y;
float angle = mod(atan(p.x, p.y), side_angle);
angle = min(angle, side_angle - angle);
p = length(p)*vec2(sin(angle), cos(angle));

float half_angle = 0.5*side_angle;
float cos_half_angle = cos(half_angle);
vec2 unit_normal = vec2(sin(half_angle), cos_half_angle);
vec2 corner = vec2(0.0, v_size.y/2.0);
float dist = dot(p - corner, unit_normal);

if (line_join != miter_join) {
dist = max(dist, line_join_distance_no_miter(
p, corner, vec2(0.0, 1.0), 0.5*v_linewidth*cos_half_angle, line_join));
}
return dist;
}
#endif

#ifdef USE_PLUS
float marker_distance(in vec2 p, in int line_cap, in int line_join)
{
Expand Down
18 changes: 15 additions & 3 deletions bokehjs/src/lib/models/glyphs/webgl/marker.vert
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ varying float v_start_angle;
varying float v_end_angle;
#endif

#ifdef USE_CIRCLE
#if defined(USE_CIRCLE) || defined(USE_NGON)
varying float v_radius;
#endif

#ifdef USE_NGON
varying float v_n;
#endif

varying float v_linewidth;
varying vec2 v_size; // 2D size for rects compared to 1D for markers.
varying vec4 v_line_color;
Expand Down Expand Up @@ -125,7 +129,15 @@ void main()
v_size = vec2(a_width, a_width);
#endif

if (a_show < 0.5 || v_size.x < 0.0 || v_size.y < 0.0 || (v_size.x == 0.0 && v_size.y == 0.0)) {
#ifdef USE_NGON
v_n = a_aux;
#endif

if (a_show < 0.5 || v_size.x < 0.0 || v_size.y < 0.0 || (v_size.x == 0.0 && v_size.y == 0.0)
#ifdef USE_NGON
|| v_n < 3.0
#endif
) {
// Do not show this marker.
gl_Position = vec4(-2.0, -2.0, 0.0, 1.0);
return;
Expand All @@ -149,7 +161,7 @@ void main()
v_end_angle = a_aux;
#endif

#ifdef USE_CIRCLE
#if defined(USE_CIRCLE) || defined(USE_NGON)
v_radius = 0.5*a_width;
#endif

Expand Down
20 changes: 20 additions & 0 deletions bokehjs/src/lib/models/glyphs/webgl/ngon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {ReglWrapper} from "./regl_wrap"
import {RadialGL} from "./radial"
import type {GLMarkerType} from "./types"
import type {NgonView} from "../ngon"

export class NgonGL extends RadialGL {
constructor(regl_wrapper: ReglWrapper, override readonly glyph: NgonView) {
super(regl_wrapper, glyph)
}

get marker_type(): GLMarkerType {
return "ngon"
}

protected override _set_data(): void {
super._set_data()
this._angles.set_from_prop(this.glyph.angle)
this._auxs.set_from_prop(this.glyph.n)
}
}
35 changes: 35 additions & 0 deletions bokehjs/src/lib/models/glyphs/webgl/radial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {ReglWrapper} from "./regl_wrap"
import type {Float32Buffer} from "./buffer"
import type {SXSYGlyphView} from "./sxsy"
import {SXSYGlyphGL} from "./sxsy"
import type {Arrayable} from "core/types"
import {mul} from "core/util/arrayable"

type RadialLikeGlyphView = SXSYGlyphView & {
sradius: Arrayable<number>
}

export abstract class RadialGL extends SXSYGlyphGL {
constructor(regl_wrapper: ReglWrapper, override readonly glyph: RadialLikeGlyphView) {
super(regl_wrapper, glyph)
}

// TODO: should be 'radius'
get size(): Float32Buffer {
return this._widths
}

protected override _set_data(): void {
super._set_data()

// Ideally we wouldn't multiply here, but currently handling of
// circle glyph and scatter with circle marker is handled with
// a single code path.
this.size.set_from_array(mul(this.glyph.sradius, 2.0))
}

protected override _set_once(): void {
super._set_once()
this._heights.set_from_scalar(0)
}
}
2 changes: 1 addition & 1 deletion bokehjs/src/lib/models/glyphs/webgl/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {Float32Buffer, NormalizedUint8Buffer, Uint8Buffer} from "./buffer"
import type {AttributeConfig, BoundingBox, Framebuffer2D, Texture2D, Vec2, Vec4} from "regl"

import type {MarkerType} from "core/enums"
export type GLMarkerType = MarkerType | "hex_tile" | "rect" | "round_rect" | "ellipse" | "annulus" | "wedge" | "annular_wedge"
export type GLMarkerType = MarkerType | "hex_tile" | "rect" | "round_rect" | "ellipse" | "annulus" | "wedge" | "annular_wedge" | "ngon"

// Props are used to pass properties from GL glyph classes to ReGL functions.
export type AccumulateProps = {
Expand Down
1 change: 1 addition & 0 deletions bokehjs/src/lib/models/glyphs/webgl/webgl_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export function marker_type_to_size_hint(marker_type: GLMarkerType): number {
case "square_pin":
return 5
case "inverted_triangle":
case "ngon":
case "triangle":
case "triangle_dot":
return 6
Expand Down
31 changes: 31 additions & 0 deletions bokehjs/test/baselines/linux/Glyph_models__should_support_Ngon.blf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Row bbox=[0, 0, 900, 300]
Figure bbox=[0, 0, 300, 300]
Canvas bbox=[0, 0, 300, 300]
CartesianFrame bbox=[29, 27, 266, 251]
GlyphRenderer bbox=[29, 27, 177, 251]
Ngon bbox=[29, 27, 177, 251]
GlyphRenderer bbox=[118, 33, 173, 245]
Ngon bbox=[118, 33, 173, 245]
LinearAxis bbox=[29, 278, 266, 22]
LinearAxis bbox=[0, 27, 29, 251]
Title bbox=[29, 0, 266, 27]
Figure bbox=[300, 0, 300, 300]
Canvas bbox=[0, 0, 300, 300]
CartesianFrame bbox=[29, 27, 266, 251]
GlyphRenderer bbox=[29, 27, 177, 251]
Ngon bbox=[29, 27, 177, 251]
GlyphRenderer bbox=[118, 33, 173, 245]
Ngon bbox=[118, 33, 173, 245]
LinearAxis bbox=[29, 278, 266, 22]
LinearAxis bbox=[0, 27, 29, 251]
Title bbox=[29, 0, 266, 27]
Figure bbox=[600, 0, 300, 300]
Canvas bbox=[0, 0, 300, 300]
CartesianFrame bbox=[29, 27, 266, 251]
GlyphRenderer bbox=[29, 27, 177, 251]
Ngon bbox=[29, 27, 177, 251]
GlyphRenderer bbox=[118, 33, 173, 245]
Ngon bbox=[118, 33, 173, 245]
LinearAxis bbox=[29, 278, 266, 22]
LinearAxis bbox=[0, 27, 29, 251]
Title bbox=[29, 0, 266, 27]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions bokehjs/test/integration/glyphs/glyphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,28 @@ describe("Glyph models", () => {
await display(row([p("canvas"), p("svg")]))
})

it("should support Ngon", async () => {
function p(output_backend: OutputBackend) {
const p = fig([300, 300], {x_range: [0, 6], y_range: [0, 4], output_backend, title: output_backend})
p.ngon({
x: [1, 2, 3], y,
radius: 1,
n: 3,
fill_color, alpha: 0.8,
line_width: 10, line_join: ["round", "miter", "bevel"],
angle: [0, 45, 90], angle_units: "deg",
})
p.ngon({
x: [3, 4, 5], y,
radius: [1, 0.6, 0.9],
n: [4, 5, 6],
fill_color, alpha: 0.6, hatch_pattern,
})
return p
}
await display(row([p("canvas"), p("svg"), p("webgl")]))
})

it("should support Rect", async () => {
function p(output_backend: OutputBackend) {
const p = fig([200, 300], {output_backend, title: output_backend})
Expand Down

0 comments on commit a28073d

Please sign in to comment.