254 lines
7.6 KiB
TypeScript
254 lines
7.6 KiB
TypeScript
import type * as p from "@bokehjs/core/properties"
|
|
|
|
import {Model} from "@bokehjs/model"
|
|
import {mat4, vec3} from "gl-matrix"
|
|
|
|
import {cartesian_product, vtkns} from "./util"
|
|
|
|
type VTKTicker = {
|
|
ticks: number[]
|
|
labels: string[]
|
|
}
|
|
|
|
export namespace VTKAxes {
|
|
export type Attrs = p.AttrsOf<Props>
|
|
|
|
export type Props = Model.Props & {
|
|
origin: p.Property<number[]>
|
|
xticker: p.Property<VTKTicker | null>
|
|
yticker: p.Property<VTKTicker | null>
|
|
zticker: p.Property<VTKTicker | null>
|
|
digits: p.Property<number>
|
|
show_grid: p.Property<boolean>
|
|
grid_opacity: p.Property<number>
|
|
axes_opacity: p.Property<number>
|
|
fontsize: p.Property<number>
|
|
}
|
|
}
|
|
|
|
export interface VTKAxes extends VTKAxes.Attrs {}
|
|
|
|
export class VTKAxes extends Model {
|
|
declare properties: VTKAxes.Props
|
|
|
|
constructor(attrs?: Partial<VTKAxes.Attrs>) {
|
|
super(attrs)
|
|
}
|
|
|
|
static override __module__ = "panel.models.vtk"
|
|
|
|
static {
|
|
this.define<VTKAxes.Props>(({Any, Array, Boolean, Number}) => ({
|
|
origin: [Array(Number), [0, 0, 0] ],
|
|
xticker: [Any, null],
|
|
yticker: [Any, null],
|
|
zticker: [Any, null],
|
|
digits: [Number, 1],
|
|
show_grid: [Boolean, true],
|
|
grid_opacity: [Number, 0.1],
|
|
axes_opacity: [Number, 1],
|
|
fontsize: [Number, 12],
|
|
}))
|
|
}
|
|
|
|
get xticks(): number[] {
|
|
if (this.xticker) {
|
|
return this.xticker.ticks
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
get yticks(): number[] {
|
|
if (this.yticker) {
|
|
return this.yticker.ticks
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
get zticks(): number[] {
|
|
if (this.zticker) {
|
|
return this.zticker.ticks
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
get xlabels(): string[] {
|
|
return this.xticker?.labels
|
|
? this.xticker.labels
|
|
: this.xticks.map((elem) => elem.toFixed(this.digits))
|
|
}
|
|
|
|
get ylabels(): string[] {
|
|
return this.yticker?.labels
|
|
? this.yticker.labels
|
|
: this.yticks.map((elem) => elem.toFixed(this.digits))
|
|
}
|
|
|
|
get zlabels(): string[] {
|
|
return this.zticker?.labels
|
|
? this.zticker.labels
|
|
: this.zticks.map((elem) => elem.toFixed(this.digits))
|
|
}
|
|
|
|
_make_grid_lines(n: number, m: number, offset: number): number[][] {
|
|
const out = []
|
|
for (let i = 0; i < n - 1; i++) {
|
|
for (let j = 0; j < m - 1; j++) {
|
|
const v0 = i * m + j + offset
|
|
const v1 = i * m + j + 1 + offset
|
|
const v2 = (i + 1) * m + j + 1 + offset
|
|
const v3 = (i + 1) * m + j + offset
|
|
const line = [5, v0, v1, v2, v3, v0]
|
|
out.push(line)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
_create_grid_axes(): any {
|
|
const pts = []
|
|
pts.push(cartesian_product(this.xticks, this.yticks, [this.origin[2]])) //xy
|
|
pts.push(cartesian_product([this.origin[0]], this.yticks, this.zticks)) //yz
|
|
pts.push(cartesian_product(this.xticks, [this.origin[1]], this.zticks)) //xz
|
|
|
|
const polys = []
|
|
let offset = 0
|
|
polys.push(this._make_grid_lines(this.xticks.length, this.yticks.length, offset)) //xy
|
|
offset += this.xticks.length * this.yticks.length
|
|
polys.push(this._make_grid_lines(this.yticks.length, this.zticks.length, offset)) //yz
|
|
offset += this.yticks.length * this.zticks.length
|
|
polys.push(this._make_grid_lines(this.xticks.length, this.zticks.length, offset)) //xz
|
|
const gridPolyData = (window as any).vtk({
|
|
vtkClass: "vtkPolyData",
|
|
points: {
|
|
vtkClass: "vtkPoints",
|
|
dataType: "Float32Array",
|
|
numberOfComponents: 3,
|
|
values: (pts as any).flat(2),
|
|
},
|
|
lines: {
|
|
vtkClass: "vtkCellArray",
|
|
dataType: "Uint32Array",
|
|
values: (polys as any).flat(2),
|
|
},
|
|
})
|
|
|
|
const gridMapper = vtkns.Mapper.newInstance()
|
|
const gridActor = vtkns.Actor.newInstance()
|
|
gridMapper.setInputData(gridPolyData)
|
|
gridActor.setMapper(gridMapper)
|
|
gridActor.getProperty().setOpacity(this.grid_opacity)
|
|
gridActor.setVisibility(this.show_grid)
|
|
|
|
return gridActor
|
|
}
|
|
|
|
create_axes(canvas: HTMLCanvasElement): any {
|
|
if (this.origin == null) {
|
|
return {psActor: null, axesActor: null, gridActor: null}
|
|
}
|
|
const points = ([this.xticks, this.yticks, this.zticks].map((arr, axis) => {
|
|
let coords = null
|
|
switch (axis) {
|
|
case 0:
|
|
coords = cartesian_product(arr, [this.origin[1]], [this.origin[2]])
|
|
break
|
|
case 1:
|
|
coords = cartesian_product([this.origin[0]], arr, [this.origin[2]])
|
|
break
|
|
case 2:
|
|
coords = cartesian_product([this.origin[0]], [this.origin[1]], arr)
|
|
break
|
|
}
|
|
return coords
|
|
}) as any).flat(2)
|
|
const axesPolyData = (window as any).vtk({
|
|
vtkClass: "vtkPolyData",
|
|
points: {
|
|
vtkClass: "vtkPoints",
|
|
dataType: "Float32Array",
|
|
numberOfComponents: 3,
|
|
values: points,
|
|
},
|
|
lines: {
|
|
vtkClass: "vtkCellArray",
|
|
dataType: "Uint32Array",
|
|
values: [
|
|
2,
|
|
0,
|
|
this.xticks.length - 1,
|
|
2,
|
|
this.xticks.length,
|
|
this.xticks.length + this.yticks.length - 1,
|
|
2,
|
|
this.xticks.length + this.yticks.length,
|
|
this.xticks.length + this.yticks.length + this.zticks.length - 1,
|
|
],
|
|
},
|
|
})
|
|
const psMapper = vtkns.PixelSpaceCallbackMapper.newInstance()
|
|
psMapper.setInputData(axesPolyData)
|
|
psMapper.setUseZValues(true)
|
|
psMapper.setCallback((coordsList: number[][], camera: any, aspect: any) => {
|
|
const textCtx = canvas.getContext("2d")
|
|
if (textCtx) {
|
|
const dims = {
|
|
height: canvas.clientHeight * window.devicePixelRatio,
|
|
width: canvas.clientWidth * window.devicePixelRatio,
|
|
}
|
|
const dataPoints = psMapper.getInputData().getPoints()
|
|
const viewMatrix = camera.getViewMatrix()
|
|
mat4.transpose(viewMatrix, viewMatrix)
|
|
const projMatrix = camera.getProjectionMatrix(aspect, -1, 1)
|
|
mat4.transpose(projMatrix, projMatrix)
|
|
textCtx.clearRect(0, 0, dims.width, dims.height)
|
|
coordsList.forEach((xy, idx) => {
|
|
const pdPoint = dataPoints.getPoint(idx)
|
|
const vc = vec3.fromValues(pdPoint[0], pdPoint[1], pdPoint[2])
|
|
vec3.transformMat4(vc, vc, viewMatrix)
|
|
vc[2] += 0.05 // sensibility
|
|
vec3.transformMat4(vc, vc, projMatrix)
|
|
if (vc[2] - 0.001 < xy[3]) {
|
|
textCtx.font = "30px serif"
|
|
textCtx.textAlign = "center"
|
|
textCtx.textBaseline = "alphabetic"
|
|
textCtx.fillText(".", xy[0], dims.height - xy[1] + 2)
|
|
textCtx.font = `${this.fontsize * window.devicePixelRatio}px serif`
|
|
textCtx.textAlign = "right"
|
|
textCtx.textBaseline = "top"
|
|
let label
|
|
if (idx < this.xticks.length) {
|
|
label = this.xlabels[idx]
|
|
} else if (
|
|
idx >= this.xticks.length &&
|
|
idx < this.xticks.length + this.yticks.length
|
|
) {
|
|
label = this.ylabels[idx - this.xticks.length]
|
|
} else {
|
|
label = this.zlabels[
|
|
idx - (this.xticks.length + this.yticks.length)
|
|
]
|
|
}
|
|
textCtx.fillText(`${label}`, xy[0], dims.height - xy[1])
|
|
}
|
|
})
|
|
}
|
|
})
|
|
const psActor = vtkns.Actor.newInstance() //PixelSpace
|
|
psActor.setMapper(psMapper)
|
|
|
|
const axesMapper = vtkns.Mapper.newInstance()
|
|
axesMapper.setInputData(axesPolyData)
|
|
const axesActor = vtkns.Actor.newInstance()
|
|
axesActor.setMapper(axesMapper)
|
|
axesActor.getProperty().setOpacity(this.axes_opacity)
|
|
|
|
const gridActor = this._create_grid_axes()
|
|
|
|
return {psActor, axesActor, gridActor}
|
|
}
|
|
}
|