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 support for vtkCornerAnnotations #2257

Merged
merged 2 commits into from
Apr 27, 2021
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
3 changes: 3 additions & 0 deletions panel/models/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ def __js_skip__(cls):

width = Override(default=300)

annotations = List(Dict(String, Any))



class VTKSynchronizedPlot(AbstractVTKPlot):
"""
Expand Down
16 changes: 16 additions & 0 deletions panel/models/vtk/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ export type ColorMapper = {
export const Interpolation = Enum("fast_linear", "linear", "nearest")
export type Interpolation = typeof Interpolation["__type__"]

export type Annotation = {
id: string
viewport: number[]
fontSize: number
fontFamily: string
color: number[]
LowerLeft?: string
LowerRight?: string
UpperLeft?: string
UpperRight?: string
LowerEdge?: string
RightEdge?: string
LeftEdge?: string
UpperEdge?: string
}

export declare type CSSProperties = {[key: string]: string}

export declare type VolumeType = {
Expand Down
112 changes: 103 additions & 9 deletions panel/models/vtk/vtklayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {div, canvas} from "@bokehjs/core/dom"
import {HTMLBox} from "@bokehjs/models/layouts/html_box"
import {clone} from "@bokehjs/core/util/object"
import {ColorMapper} from "@bokehjs/models/mappers/color_mapper"
import {Enum} from "@bokehjs/core/kinds"

import {PanelHTMLBoxView, set_size} from "../layout"
import {vtkns, VolumeType, majorAxis, applyStyle, CSSProperties} from "./util"
import {vtkns, VolumeType, majorAxis, applyStyle, CSSProperties, Annotation} from "./util"
import {VTKColorBar} from "./vtkcolorbar"
import {VTKAxes} from "./vtkaxes"

Expand All @@ -25,6 +26,8 @@ const INFO_DIV_STYLE: CSSProperties = {
position: "absolute",
}

const textPositions = Enum("LowerLeft", "LowerRight", "UpperLeft", "UpperRight", "LowerEdge", "RightEdge", "LeftEdge", "UpperEdge")

export abstract class AbstractVTKView extends PanelHTMLBoxView {
model: AbstractVTKPlot
protected _axes: any
Expand All @@ -35,6 +38,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView {
protected _vtk_container: HTMLDivElement
protected _vtk_renwin: any
protected _widgetManager: any
protected _annotations_container: HTMLDivElement

initialize(): void {
super.initialize()
Expand Down Expand Up @@ -84,6 +88,90 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView {

info_div.click()
}

_init_annotations_container(): void {
if (!this._annotations_container) {
this._annotations_container = document.createElement("div")
this._annotations_container.style.position = "absolute"
this._annotations_container.style.width = "100%"
this._annotations_container.style.height = "100%"
this._annotations_container.style.top = "0"
this._annotations_container.style.left = "0"
this._annotations_container.style.pointerEvents = "none"
}
}

_clean_annotations(): void {
if (this._annotations_container) {
while (this._annotations_container.firstElementChild) {
this._annotations_container.firstElementChild.remove()
}
}
}

_add_annotations(): void {
this._clean_annotations()
const {annotations} = this.model
if (annotations != null) {
for (let annotation of annotations) {
const {viewport, color, fontSize, fontFamily} = annotation
textPositions.values.forEach((pos) => {
const text = annotation[pos]
if (text) {
const div = document.createElement("div")
div.textContent = text
const {style} = div
style.position = "absolute"
style.color = `rgb(${color.map((val)=>255*val).join(",")})`
style.fontSize = `${fontSize}px`
style.padding = "5px"
style.fontFamily = fontFamily
style.width = "fit-content"

if (pos == "UpperLeft") {
style.top = `${(1 - viewport[3])*100}%`
style.left = `${viewport[0]*100}%`
}
if (pos == "UpperRight") {
style.top = `${(1 - viewport[3])*100}%`
style.right = `${(1-viewport[2])*100}%`
}
if (pos == "LowerLeft") {
style.bottom = `${viewport[1]*100}%`
style.left = `${viewport[0]*100}%`
}
if (pos == "LowerRight") {
style.bottom = `${viewport[1]*100}%`
style.right = `${(1-viewport[2])*100}%`
}
if (pos == "UpperEdge") {
style.top = `${(1 - viewport[3])*100}%`
style.left = `${(viewport[0] + (viewport[2] - viewport[0])/2) *100}%`
style.transform = "translateX(-50%)"
}
if (pos == "LowerEdge") {
style.bottom = `${viewport[1]*100}%`
style.left = `${(viewport[0] + (viewport[2] - viewport[0])/2) *100}%`
style.transform = "translateX(-50%)"
}
if (pos == "LeftEdge") {
style.left = `${viewport[0]*100}%`
style.top = `${(1 - viewport[3] + (viewport[3] - viewport[1])/2) *100}%`
style.transform = "translateY(-50%)"
}
if (pos == "RightEdge") {
style.right = `${(1-viewport[2])*100}%`
style.top = `${(1 - viewport[3] + (viewport[3] - viewport[1])/2) *100}%`
style.transform = "translateY(-50%)"
}
this._annotations_container.appendChild(div)
}
}
)
}
}
console.log(this.model.annotations)
}

connect_signals(): void {
super.connect_signals()
Expand All @@ -99,6 +187,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView {
this._vtk_render()
})
this.on_change(this.model.properties.color_mappers, () => this._add_colorbars())
this.on_change(this.model.properties.annotations, () => this._add_annotations())
}

render(): void {
Expand All @@ -108,6 +197,7 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView {
this._axes = null
this._vtk_container = div()
this.init_vtk_renwin()
this._init_annotations_container()
set_size(this._vtk_container, this.model)
this.el.appendChild(this._vtk_container)
// update camera model state only at the end of the interaction
Expand All @@ -117,13 +207,15 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView {
this._bind_key_events()
this.plot()
this._add_colorbars()
this._add_annotations()
this.model.renderer_el = this._vtk_renwin
} else {
set_size(this._vtk_container, this.model)
// warning if _vtk_renwin contain controllers or other elements
// we must attach them to the new el
this.el.appendChild(this._vtk_container)
}
this.el.appendChild(this._annotations_container)
}

after_layout(): void {
Expand Down Expand Up @@ -387,13 +479,14 @@ export abstract class AbstractVTKView extends PanelHTMLBoxView {
export namespace AbstractVTKPlot {
export type Attrs = p.AttrsOf<Props>
export type Props = HTMLBox.Props & {
axes: p.Property<VTKAxes>
axes: p.Property<VTKAxes | null>
camera: p.Property<any>
data: p.Property<string | VolumeType | null>
enable_keybindings: p.Property<boolean>
orientation_widget: p.Property<boolean>
color_mappers: p.Property<ColorMapper[]>
interactive_orientation_widget: p.Property<boolean>
annotations: p.Property<Annotation[] | null>
}
}

Expand All @@ -414,13 +507,14 @@ export abstract class AbstractVTKPlot extends HTMLBox {
}

static init_AbstractVTKPlot(): void {
this.define<AbstractVTKPlot.Props>({
axes: [ p.Instance ],
camera: [ p.Instance ],
color_mappers: [ p.Array, [] ],
orientation_widget: [ p.Boolean, false ],
interactive_orientation_widget: [ p.Boolean, false ],
})
this.define<AbstractVTKPlot.Props>(({Any, Ref, Array, Boolean, Nullable}) => ({
axes: [ Nullable(Ref(VTKAxes)), null ],
camera: [ Any ],
color_mappers: [ Array(Ref(ColorMapper)), [] ],
orientation_widget: [ Boolean, false ],
interactive_orientation_widget: [ Boolean, false ],
annotations: [ Nullable(Array(Any)), null ],
}))

this.override<AbstractVTKPlot.Props>({
height: 300,
Expand Down
11 changes: 11 additions & 0 deletions panel/pane/vtk/enums.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
from enum import Enum
from collections import namedtuple

class TextPosition(Enum):
LowerLeft = 0
LowerRight = 1
UpperLeft = 2
UpperRight = 3
LowerEdge = 4
RightEdge = 5
LeftEdge = 6
UpperEdge = 7

SCALAR_MODE = namedtuple("SCALAR_MODE",
"Default UsePointData UseCellData UsePointFieldData UseCellFieldData UseFieldData"
)(0, 1, 2, 3, 4, 5)
Expand Down
32 changes: 32 additions & 0 deletions panel/pane/vtk/synchronizable_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from vtk.vtkRenderingCore import vtkColorTransferFunction
from vtk.vtkCommonDataModel import vtkDataObject

from .enums import TextPosition

# -----------------------------------------------------------------------------
# Python compatibility handling 2.6, 2.7, 3+
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -176,13 +178,32 @@ def __init__(self, id_root=None, serialize_all_data_arrays=False, debug=False):
self.idRoot = id_root
self.debugSerializers = debug
self.debugAll = debug
self.annotations = {}

def getReferenceId(self, instance):
if not self.idRoot or (hasattr(instance, 'IsA') and instance.IsA('vtkCamera')):
return getReferenceId(instance)
else:
return self.idRoot + getReferenceId(instance)

def addAnnotation(self, parent, prop, propId):
if prop.GetClassName() == "vtkCornerAnnotation":
annotation = {
"id": propId,
"viewport": parent.GetViewport(),
"fontSize": prop.GetLinearFontScaleFactor() * 2,
"fontFamily": prop.GetTextProperty().GetFontFamilyAsString(),
"color": prop.GetTextProperty().GetColor(),
**{pos.name: prop.GetText(pos.value) for pos in TextPosition}
}
if self.annotations is None:
self.annotations = {propId: annotation}
else:
self.annotations.update({propId: annotation})

def getAnnotations(self):
return list(self.annotations.values())

def setIgnoreLastDependencies(self, force):
self.ingoreLastDependencies = force

Expand Down Expand Up @@ -291,6 +312,9 @@ def serializeInstance(parent, instance, instanceId, context, depth):


def initializeSerializers():
# Annotations
registerInstanceSerializer('vtkCornerAnnotation', annotationSerializer)

# Actors/viewProps
registerInstanceSerializer('vtkImageSlice', genericProp3DSerializer)
registerInstanceSerializer('vtkVolume', genericProp3DSerializer)
Expand Down Expand Up @@ -557,6 +581,14 @@ def extractRequiredFields(extractedFields, parent, dataset, context, requestedFi
# Concrete instance serializers
# -----------------------------------------------------------------------------

def annotationSerializer(parent, prop, propId, context, depth):
if context.debugSerializers:
print('%s!!!Annotations are not handled directly by vtk.js but by bokeh model' % pad(depth))

context.addAnnotation(parent, prop, propId)

return None

def genericPropSerializer(parent, prop, popId, context, depth):
# This kind of actor has two "children" of interest, a property and a
# mapper (optionnaly a texture)
Expand Down
21 changes: 10 additions & 11 deletions panel/pane/vtk/vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,13 @@ def export_scene(self, filename='vtk_scene', all_data_arrays=False):
filename += '.synch'
import panel.pane.vtk.synchronizable_serializer as rws
context = rws.SynchronizationContext(serialize_all_data_arrays=all_data_arrays, debug=self._debug_serializer)
scene, arrays = self._serialize_ren_win(self.object, context, binary=True, compression=False)
scene, arrays, annotations = self._serialize_ren_win(self.object, context, binary=True, compression=False)

with zipfile.ZipFile(filename, mode='w') as zf:
zf.writestr('index.json', json.dumps(scene))
for name, data in arrays.items():
zf.writestr('data/%s' % name, data, zipfile.ZIP_DEFLATED)
zf.writestr('annotations.json', json.dumps(annotations))
return filename

def _update_color_mappers(self):
Expand All @@ -321,7 +322,8 @@ def _serialize_ren_win(self, ren_win, context, binary=False, compression=True, e
arrays = {name: context.getCachedDataArray(name, binary=binary, compression=compression)
for name in context.dataArrayCache.keys()
if name not in exclude_arrays}
return scene, arrays
annotations = context.getAnnotations()
return scene, arrays, annotations

@staticmethod
def _rgb2hex(r, g, b):
Expand Down Expand Up @@ -379,14 +381,12 @@ def _update(self, ref=None, model=None):
serialize_all_data_arrays=self.serialize_all_data_arrays,
debug=self._debug_serializer
)
self._scene, self._arrays = self._serialize_ren_win(
self._scene, self._arrays, self._annotations = self._serialize_ren_win(
self.object,
context,
)
if model is not None:
model.update(rebuild=True)
model.update(arrays=self._arrays)
model.update(scene=self._scene)
model.update(rebuild=True, arrays=self._arrays, scene=self._scene, annotations=self._annotations)


class VTKRenderWindowSynchronized(BaseVTKRenderWindow, SyncHelpers):
Expand Down Expand Up @@ -427,10 +427,10 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
serialize_all_data_arrays=self.serialize_all_data_arrays,
debug=self._debug_serializer
)
scene, arrays = self._serialize_ren_win(self.object, context)
scene, arrays, annotations = self._serialize_ren_win(self.object, context)
self._update_color_mappers()
props = self._process_param_change(self._init_params())
props.update(scene=scene, arrays=arrays, color_mappers=self.color_mappers)
props.update(scene=scene, arrays=arrays, annotations=annotations, color_mappers=self.color_mappers)
model = VTKSynchronizedPlot(**props)

if root is None:
Expand All @@ -450,14 +450,13 @@ def _cleanup(self, root):

def _update(self, ref=None, model=None):
context = self._contexts[model.id]
scene, arrays = self._serialize_ren_win(
scene, arrays, annotations = self._serialize_ren_win(
self.object,
context,
exclude_arrays=model.arrays_processed
)
context.checkForArraysToRelease()
model.update(arrays=arrays)
model.update(scene=scene)
model.update(arrays=arrays, scene=scene, annotations=annotations)

def synchronize(self):
self.param.trigger('object')
Expand Down