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 export type Props = Model.Props & { origin: p.Property xticker: p.Property yticker: p.Property zticker: p.Property digits: p.Property show_grid: p.Property grid_opacity: p.Property axes_opacity: p.Property fontsize: p.Property } } export interface VTKAxes extends VTKAxes.Attrs {} export class VTKAxes extends Model { declare properties: VTKAxes.Props constructor(attrs?: Partial) { super(attrs) } static override __module__ = "panel.models.vtk" static { this.define(({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} } }