Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add n-gon glyph #14027

Merged
merged 6 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions bokehjs/src/lib/core/hittest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,40 @@ export function check_2_segments_intersect(
return {hit: (a > 0 && a < 1) && (b > 0 && b < 1), x, y}
}
}

// Given two polygons, is any vertex of one inside the other
export function vertex_overlap(x0: Arrayable<number>, y0: Arrayable<number>, x1: Arrayable<number>, y1: Arrayable<number>): boolean {
// need to check "both directions" to handle total inclusion cases
for (let i = 0; i < x0.length; i++) {
if (point_in_poly(x0[i], y0[i], x1, y1)) {
return true
}
}
for (let i = 0; i < x1.length; i++) {
if (point_in_poly(x1[i], y1[i], x0, y0)) {
return true
}
}
return false
}

// Given two polygons, do any pair of edges intersect
export function edge_intersection(x0: Arrayable<number>, y0: Arrayable<number>, x1: Arrayable<number>, y1: Arrayable<number>): boolean {
for (let i = 0; i < x0.length-1; i++) {
for (let j = 0; j < x1.length-1; j++) {
if (check_2_segments_intersect(x0[i], y0[i], x0[i+1], y0[i+1], x1[j], y1[j], x1[j+1], y1[j+1]).hit) {
return true
}
}
// consider x1, y1 "closing" segment
if (check_2_segments_intersect(x0[i], y0[i], x0[i+1], y0[i+1], x1[x1.length-1], y1[x1.length-1], x1[0], y1[0]).hit) {
return true
}
}
// consider x0, y0, "closing" segment
if (check_2_segments_intersect(x0[x0.length-1], y0[x0.length-1], x0[0], y0[0], x1[x1.length-1], y1[x1.length-1], x1[0], y1[0]).hit) {
return true
}

return false
}
110 changes: 7 additions & 103 deletions bokehjs/src/lib/models/glyphs/circle.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import {XYGlyph, XYGlyphView} from "./xy_glyph"
import {inherit} from "./glyph"
import {RadialGlyph, RadialGlyphView} from "./radial_glyph"
import type {PointGeometry, SpanGeometry, RectGeometry, PolyGeometry} from "core/geometry"
import {LineVector, FillVector, HatchVector} from "core/property_mixins"
import type * as visuals from "core/visuals"
import type {Rect, Indices} from "core/types"
import {to_screen} from "core/types"
import {RadiusDimension} from "core/enums"
import * as hittest from "core/hittest"
import * as p from "core/properties"
import type {SpatialIndex} from "core/util/spatial"
import {elementwise} from "core/util/array"
import type * as p from "core/properties"
import {minmax2} from "core/util/arrayable"
import type {Context2d} from "core/util/canvas"
import {Selection} from "../selections/selection"
import type {Range1d} from "../ranges/range1d"
import type {CircleGL} from "./webgl/circle"

export interface CircleView extends Circle.Data {}

export class CircleView extends XYGlyphView {
export class CircleView extends RadialGlyphView {
declare model: Circle
declare visuals: Circle.Visuals

Expand All @@ -30,67 +21,6 @@ export class CircleView extends XYGlyphView {
return CircleGL
}

protected override _index_data(index: SpatialIndex): void {
const {x, y, radius, data_size} = this
for (let i = 0; i < data_size; i++) {
const x_i = x[i]
const y_i = y[i]
const r_i = radius.get(i)
index.add_rect(x_i - r_i, y_i - r_i, x_i + r_i, y_i + r_i)
}
}

protected override _map_data(): void {
this._define_or_inherit_attr<Circle.Data>("sradius", () => {
if (this.model.properties.radius.units == "data") {
const sradius_x = () => this.sdist(this.renderer.xscale, this.x, this.radius)
const sradius_y = () => this.sdist(this.renderer.yscale, this.y, this.radius)

const {radius_dimension} = this.model
switch (radius_dimension) {
case "x": {
return this.inherited_x && this.inherited_radius ? inherit : sradius_x()
}
case "y": {
return this.inherited_y && this.inherited_radius ? inherit : sradius_y()
}
case "min":
case "max": {
if (this.inherited_x && this.inherited_y && this.inherited_radius) {
return inherit
} else {
return elementwise(sradius_x(), sradius_y(), Math[radius_dimension])
}
}
}
} else {
return this.inherited_sradius ? inherit : to_screen(this.radius)
}
})
}

protected override _mask_data(): Indices {
const {frame} = this.renderer.plot_view

const shr = frame.x_target
const svr = frame.y_target

let hr: Range1d
let vr: Range1d
if (this.model.properties.radius.units == "data") {
hr = shr.map((x) => this.renderer.xscale.invert(x)).widen(this.max_radius)
vr = svr.map((y) => this.renderer.yscale.invert(y)).widen(this.max_radius)
} else {
hr = shr.widen(this.max_radius).map((x) => this.renderer.xscale.invert(x))
vr = svr.widen(this.max_radius).map((y) => this.renderer.yscale.invert(y))
}

return this.index.indices({
x0: hr.start, x1: hr.end,
y0: vr.start, y1: vr.end,
})
}

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

Expand Down Expand Up @@ -232,45 +162,23 @@ export class CircleView extends XYGlyphView {

return new Selection({indices})
}

// circle does not inherit from marker (since it also accepts radius) so we
// must supply a draw_legend for it here
override draw_legend_for_index(ctx: Context2d, {x0, y0, x1, y1}: Rect, index: number): void {
// using objects like this seems a little wonky, since the keys are coerced to
// stings, but it works
const len = index + 1

const sx: number[] = new Array(len)
sx[index] = (x0 + x1)/2
const sy: number[] = new Array(len)
sy[index] = (y0 + y1)/2

const sradius: number[] = new Array(len)
sradius[index] = Math.min(Math.abs(x1 - x0), Math.abs(y1 - y0))*0.2

this._paint(ctx, [index], {sx, sy, sradius})
}
}

export namespace Circle {
export type Attrs = p.AttrsOf<Props>

export type Props = XYGlyph.Props & {
radius: p.DistanceSpec
radius_dimension: p.Property<RadiusDimension>
export type Props = RadialGlyph.Props & {
hit_dilation: p.Property<number>
} & Mixins

export type Mixins = LineVector & FillVector & HatchVector
}

export type Visuals = XYGlyph.Visuals & {line: visuals.LineVector, fill: visuals.FillVector, hatch: visuals.HatchVector}
export type Visuals = RadialGlyph.Visuals

export type Data = p.GlyphDataOf<Props>
}

export interface Circle extends Circle.Attrs {}

export class Circle extends XYGlyph {
export class Circle extends RadialGlyph {
declare properties: Circle.Props
declare __view_type__: CircleView

Expand All @@ -281,11 +189,7 @@ export class Circle extends XYGlyph {
static {
this.prototype.default_view = CircleView

this.mixins<Circle.Mixins>([LineVector, FillVector, HatchVector])

this.define<Circle.Props>(({Float}) => ({
radius: [ p.DistanceSpec, {field: "radius"} ],
radius_dimension: [ RadiusDimension, "x" ],
hit_dilation: [ Float, 1.0 ],
}))
}
Expand Down
1 change: 1 addition & 0 deletions bokehjs/src/lib/models/glyphs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {Line} from "./line"
export {MathMLGlyph} from "./mathml_glyph"
export {MultiLine} from "./multi_line"
export {MultiPolygons} from "./multi_polygons"
export {Ngon} from "./ngon"
export {Patch} from "./patch"
export {Patches} from "./patches"
export {Quad} from "./quad"
Expand Down
178 changes: 178 additions & 0 deletions bokehjs/src/lib/models/glyphs/ngon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {RadialGlyph, RadialGlyphView} from "./radial_glyph"
import type {PointGeometry, PolyGeometry, RectGeometry, SpanGeometry} from "core/geometry"
import {minmax2} from "core/util/arrayable"
import {edge_intersection, point_in_poly, vertex_overlap} from "core/hittest"
import * as p from "core/properties"
import type {Arrayable} from "core/types"
import type {Context2d} from "core/util/canvas"
import {Selection} from "../selections/selection"

export interface NgonView extends Ngon.Data {}

function ngon(x: number, y: number, r: number, n: number, angle: number): [Arrayable<number>, Arrayable<number>] {
const xs = new Float32Array(n)
const ys = new Float32Array(n)
const alpha_i = 2*Math.PI / n
for (let i = 0; i < n; i++) {
const alpha = i * alpha_i + angle
xs[i] = x + r * Math.sin(alpha)
ys[i] = y + r * -Math.cos(alpha)
}
return [xs, ys]
}

export class NgonView extends RadialGlyphView {
declare model: Ngon
declare visuals: Ngon.Visuals

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

for (const i of indices) {
const sx_i = sx[i]
const sy_i = sy[i]
const sradius_i = sradius[i]
const angle_i = angle.get(i)
const n_i = n.get(i)

if (n_i < 3 || !isFinite(sx_i + sy_i + sradius_i + angle_i + n_i)) {
continue
}

const [sxs, sys] = ngon(sx_i, sy_i, sradius_i, n_i, angle_i)

ctx.beginPath()
ctx.moveTo(sxs[0], sys[0])
for (let i = 1; i <= n_i; i++) {
ctx.lineTo(sxs[i], sys[i])
}
ctx.closePath()

this.visuals.fill.apply(ctx, i)
this.visuals.hatch.apply(ctx, i)
this.visuals.line.apply(ctx, i)
}
}

protected _ngon(index: number) {
const {sx, sy, sradius, angle, n} = {...this}
const sx_i = sx[index]
const sy_i = sy[index]
const sradius_i = sradius[index]
const angle_i = angle.get(index)
const n_i = n.get(index)
return ngon(sx_i, sy_i, sradius_i, n_i, angle_i)
}

protected override _hit_point(geometry: PointGeometry): Selection {
const x = this.renderer.xscale.invert(geometry.sx)
const y = this.renderer.yscale.invert(geometry.sy)
const candidates = this.index.indices({x0: x, y0: y, x1: x, y1: y})

const indices = []
for (const index of candidates) {
const [sxs, sys] = this._ngon(index)
if (point_in_poly(geometry.sx, geometry.sy, sxs, sys)) {
indices.push(index)
}
}
return new Selection({indices})
}

protected override _hit_span(geometry: SpanGeometry): Selection {
const {sx, sy} = geometry
const {x0, x1, y0, y1} = this.bounds()

const [val, dim, candidates] = (() => {
switch (geometry.direction) {
case "v": {
const y = this.renderer.yscale.invert(sy)
const candidates = this.index.indices({x0, y0: y, x1, y1: y})
return [sy, 1, candidates]
}
case "h": {
const x = this.renderer.xscale.invert(sx)
const candidates = this.index.indices({x0: x, y0, x1: x, y1})
return [sx, 0, candidates]
}
}
})()

const indices = []
for (const index of candidates) {
const coords = this._ngon(index)[dim]
for (let i = 0; i < coords.length-1; i++) {
if ((coords[i] <= val && val <= coords[i+1]) || (coords[i+1] <= val && val <= coords[i])) {
indices.push(index)
break
}
}
}
return new Selection({indices})
}
protected override _hit_poly(geometry: PolyGeometry): Selection {
const {sx: gsx, sy: gsy} = geometry

const candidates = (() => {
const xs = this.renderer.xscale.v_invert(gsx)
const ys = this.renderer.yscale.v_invert(gsy)
const [x0, x1, y0, y1] = minmax2(xs, ys)
return this.index.indices({x0, x1, y0, y1})
})()

const indices = []
for (const index of candidates) {
const [sxs, sys] = this._ngon(index)
if (vertex_overlap(sxs, sys, gsx, gsy)) {
indices.push(index)
continue
}
if (edge_intersection(sxs, sys, gsx, gsy)) {
indices.push(index)
continue
}
}

return new Selection({indices})
}

protected override _hit_rect(geometry: RectGeometry): Selection {
const {sx0, sx1, sy0, sy1} = geometry
const sxs = [sx0, sx1, sx1, sx0]
const sys = [sy0, sy0, sy1, sy1]
return this._hit_poly({type: "poly", sx: sxs, sy: sys})
}
}

export namespace Ngon {
export type Attrs = p.AttrsOf<Props>

export type Props = RadialGlyph.Props & {
angle: p.AngleSpec
n: p.NumberSpec
}

export type Visuals = RadialGlyph.Visuals

export type Data = p.GlyphDataOf<Props>
}

export interface Ngon extends Ngon.Attrs {}

export class Ngon extends RadialGlyph {
declare properties: Ngon.Props
declare __view_type__: NgonView

constructor(attrs?: Partial<Ngon.Attrs>) {
super(attrs)
}

static {
this.prototype.default_view = NgonView

this.define<Ngon.Props>(() => ({
angle: [ p.AngleSpec, 0 ],
n: [ p.NumberSpec, {field: "n"} ],
}))
}
}
Loading
Loading