init
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import hashlib, base64
|
||||
|
||||
arrayTypesMapping = [
|
||||
" ", # VTK_VOID 0
|
||||
" ", # VTK_BIT 1
|
||||
"b", # VTK_CHAR 2
|
||||
"B", # VTK_UNSIGNED_CHAR 3
|
||||
"h", # VTK_SHORT 4
|
||||
"H", # VTK_UNSIGNED_SHORT 5
|
||||
"i", # VTK_INT 6
|
||||
"I", # VTK_UNSIGNED_INT 7
|
||||
"l", # VTK_LONG 8
|
||||
"L", # VTK_UNSIGNED_LONG 9
|
||||
"f", # VTK_FLOAT 10
|
||||
"d", # VTK_DOUBLE 11
|
||||
"L", # VTK_ID_TYPE 12
|
||||
" ", # unspecified 13
|
||||
" ", # unspecified 14
|
||||
"b", # signed_char 15
|
||||
]
|
||||
|
||||
javascriptMapping = {
|
||||
"b": "Int8Array",
|
||||
"B": "Uint8Array",
|
||||
"h": "Int16Array",
|
||||
"H": "Int16Array",
|
||||
"i": "Int32Array",
|
||||
"I": "Uint32Array",
|
||||
"l": "Int32Array",
|
||||
"L": "Uint32Array",
|
||||
"f": "Float32Array",
|
||||
"d": "Float64Array",
|
||||
}
|
||||
|
||||
|
||||
def iteritems(d, **kwargs):
|
||||
return iter(d.items(**kwargs))
|
||||
|
||||
|
||||
def base64Encode(x):
|
||||
return base64.b64encode(x).decode("utf-8")
|
||||
|
||||
|
||||
def hashDataArray(dataArray):
|
||||
hashedBit = hashlib.md5(memoryview(dataArray)).hexdigest()
|
||||
typeCode = arrayTypesMapping[dataArray.GetDataType()]
|
||||
return "%s_%d%s" % (hashedBit, dataArray.GetSize(), typeCode)
|
||||
|
||||
|
||||
def getJSArrayType(dataArray):
|
||||
return javascriptMapping[arrayTypesMapping[dataArray.GetDataType()]]
|
||||
|
||||
|
||||
def getReferenceId(ref):
|
||||
if ref:
|
||||
try:
|
||||
return ref.__this__[1:17]
|
||||
except:
|
||||
idStr = str(ref)[-12:-1]
|
||||
# print('====> fallback ID %s for %s' % (idStr, ref))
|
||||
return idStr
|
||||
return "0x0"
|
||||
@@ -0,0 +1,640 @@
|
||||
from math import *
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Set of helper functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def normalize(vect, tolerance=0.00001):
|
||||
mag2 = sum(n * n for n in vect)
|
||||
if abs(mag2 - 1.0) > tolerance:
|
||||
mag = sqrt(mag2)
|
||||
vect = tuple(n / mag for n in vect)
|
||||
return vect
|
||||
|
||||
|
||||
def q_mult(q1, q2):
|
||||
w1, x1, y1, z1 = q1
|
||||
w2, x2, y2, z2 = q2
|
||||
w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
|
||||
x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
|
||||
y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
|
||||
z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2
|
||||
return w, x, y, z
|
||||
|
||||
|
||||
def q_conjugate(q):
|
||||
w, x, y, z = q
|
||||
return (w, -x, -y, -z)
|
||||
|
||||
|
||||
def qv_mult(q1, v1):
|
||||
q2 = (0.0,) + v1
|
||||
return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:]
|
||||
|
||||
|
||||
def axisangle_to_q(v, theta):
|
||||
v = normalize(v)
|
||||
x, y, z = v
|
||||
theta /= 2
|
||||
w = cos(theta)
|
||||
x = x * sin(theta)
|
||||
y = y * sin(theta)
|
||||
z = z * sin(theta)
|
||||
return w, x, y, z
|
||||
|
||||
|
||||
def vectProduct(axisA, axisB):
|
||||
xa, ya, za = axisA
|
||||
xb, yb, zb = axisB
|
||||
normalVect = (ya * zb - za * yb, za * xb - xa * zb, xa * yb - ya * xb)
|
||||
normalVect = normalize(normalVect)
|
||||
return normalVect
|
||||
|
||||
|
||||
def dotProduct(vecA, vecB):
|
||||
return (vecA[0] * vecB[0]) + (vecA[1] * vecB[1]) + (vecA[2] * vecB[2])
|
||||
|
||||
|
||||
def rotate(axis, angle, center, point):
|
||||
angleInRad = 3.141592654 * angle / 180.0
|
||||
rotation = axisangle_to_q(axis, angleInRad)
|
||||
tPoint = tuple((point[i] - center[i]) for i in range(3))
|
||||
rtPoint = qv_mult(rotation, tPoint)
|
||||
rPoint = tuple((rtPoint[i] + center[i]) for i in range(3))
|
||||
return rPoint
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Spherical Camera
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SphericalCamera(object):
|
||||
def __init__(
|
||||
self, dataHandler, focalPoint, position, phiAxis, phiAngles, thetaAngles
|
||||
):
|
||||
self.dataHandler = dataHandler
|
||||
self.cameraSettings = []
|
||||
self.thetaBind = {
|
||||
"mouse": {
|
||||
"drag": {"modifier": 0, "coordinate": 1, "step": 30, "orientation": 1}
|
||||
}
|
||||
}
|
||||
self.phiBind = {
|
||||
"mouse": {
|
||||
"drag": {"modifier": 0, "coordinate": 0, "step": 30, "orientation": 1}
|
||||
}
|
||||
}
|
||||
|
||||
# Convert to serializable type
|
||||
fp = tuple(i for i in focalPoint)
|
||||
|
||||
# Register arguments to the data handler
|
||||
if len(phiAngles) > 1 and phiAngles[-1] + phiAngles[1] == 360:
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0,
|
||||
name="phi",
|
||||
values=phiAngles,
|
||||
ui="slider",
|
||||
loop="modulo",
|
||||
bind=self.phiBind,
|
||||
)
|
||||
else:
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="phi", values=phiAngles, ui="slider", bind=self.phiBind
|
||||
)
|
||||
if thetaAngles[0] < 0 and thetaAngles[0] >= -90:
|
||||
idx = 0
|
||||
for theta in thetaAngles:
|
||||
if theta < 0:
|
||||
idx += 1
|
||||
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0,
|
||||
name="theta",
|
||||
values=[(x + 90) for x in thetaAngles],
|
||||
ui="slider",
|
||||
default=idx,
|
||||
bind=self.thetaBind,
|
||||
)
|
||||
else:
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0,
|
||||
name="theta",
|
||||
values=thetaAngles,
|
||||
ui="slider",
|
||||
bind=self.thetaBind,
|
||||
)
|
||||
|
||||
# Compute all camera settings
|
||||
for theta in thetaAngles:
|
||||
for phi in phiAngles:
|
||||
phiPos = rotate(phiAxis, -phi, fp, position)
|
||||
thetaAxis = vectProduct(
|
||||
phiAxis, tuple(fp[i] - phiPos[i] for i in range(3))
|
||||
)
|
||||
thetaPhiPos = rotate(thetaAxis, theta, fp, phiPos)
|
||||
viewUp = rotate(thetaAxis, theta, (0, 0, 0), phiAxis)
|
||||
|
||||
self.cameraSettings.append(
|
||||
{
|
||||
"theta": theta,
|
||||
"thetaIdx": thetaAngles.index(theta),
|
||||
"phi": phi,
|
||||
"phiIdx": phiAngles.index(phi),
|
||||
"focalPoint": fp,
|
||||
"position": thetaPhiPos,
|
||||
"viewUp": viewUp,
|
||||
}
|
||||
)
|
||||
|
||||
self.dataHandler.updateBasePattern()
|
||||
|
||||
def updatePriority(self, priorityList):
|
||||
keyList = ["theta", "phi"]
|
||||
for idx in range(min(len(priorityList), len(keyList))):
|
||||
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
|
||||
|
||||
def __iter__(self):
|
||||
for cameraData in self.cameraSettings:
|
||||
self.dataHandler.setArguments(
|
||||
phi=cameraData["phiIdx"], theta=cameraData["thetaIdx"]
|
||||
)
|
||||
yield cameraData
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Cylindrical Camera
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CylindricalCamera(object):
|
||||
def __init__(
|
||||
self,
|
||||
dataHandler,
|
||||
focalPoint,
|
||||
position,
|
||||
rotationAxis,
|
||||
phiAngles,
|
||||
translationValues,
|
||||
):
|
||||
self.dataHandler = dataHandler
|
||||
self.cameraSettings = []
|
||||
|
||||
# Register arguments to the data handler
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="phi", values=phiAngles, ui="slider", loop="modulo"
|
||||
)
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="n_pos", values=translationValues, ui="slider"
|
||||
)
|
||||
|
||||
# Compute all camera settings
|
||||
for translation in translationValues:
|
||||
for phi in phiAngles:
|
||||
phiPos = rotate(rotationAxis, phi, focalPoint, position)
|
||||
newfocalPoint = tuple(
|
||||
focalPoint[i] + (translation * rotationAxis[i]) for i in range(3)
|
||||
)
|
||||
transPhiPoint = tuple(
|
||||
phiPos[i] + (translation * rotationAxis[i]) for i in range(3)
|
||||
)
|
||||
|
||||
self.cameraSettings.append(
|
||||
{
|
||||
"n_pos": translation,
|
||||
"n_posIdx": translationValues.index(translation),
|
||||
"phi": phi,
|
||||
"phiIdx": phiAngles.index(phi),
|
||||
"focalPoint": newfocalPoint,
|
||||
"position": transPhiPoint,
|
||||
"viewUp": rotationAxis,
|
||||
}
|
||||
)
|
||||
|
||||
self.dataHandler.updateBasePattern()
|
||||
|
||||
def updatePriority(self, priorityList):
|
||||
keyList = ["n_pos", "phi"]
|
||||
for idx in range(min(len(priorityList), len(keyList))):
|
||||
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
|
||||
|
||||
def __iter__(self):
|
||||
for cameraData in self.cameraSettings:
|
||||
self.dataHandler.setArguments(
|
||||
phi=cameraData["phiIdx"], n_pos=cameraData["n_posIdx"]
|
||||
)
|
||||
yield cameraData
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MultiView Cube Camera
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CubeCamera(object):
|
||||
|
||||
# positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ]
|
||||
def __init__(self, dataHandler, viewForward, viewUp, positions):
|
||||
self.dataHandler = dataHandler
|
||||
self.cameraSettings = []
|
||||
self.viewForward = viewForward
|
||||
self.viewUp = viewUp
|
||||
self.rightDirection = vectProduct(viewForward, viewUp)
|
||||
self.positions = positions
|
||||
|
||||
# Register arguments to the data handler
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"]
|
||||
)
|
||||
|
||||
# Register arguments to id position
|
||||
self.args = {}
|
||||
for pos in positions:
|
||||
for key in pos["args"]:
|
||||
if key not in self.args:
|
||||
self.args[key] = {}
|
||||
self.args[key][pos["args"][key]] = True
|
||||
|
||||
for key in self.args:
|
||||
self.args[key] = sorted(self.args[key], key=lambda k: int(k))
|
||||
|
||||
self.keyList = self.args.keys()
|
||||
for key in self.args:
|
||||
self.dataHandler.registerArgument(
|
||||
priority=1, name=key, values=self.args[key]
|
||||
)
|
||||
|
||||
self.dataHandler.updateBasePattern()
|
||||
|
||||
def updatePriority(self, priorityList):
|
||||
keyList = ["orientation"]
|
||||
for idx in range(min(len(priorityList), len(keyList))):
|
||||
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
|
||||
|
||||
def __iter__(self):
|
||||
for pos in self.positions:
|
||||
cameraData = {
|
||||
"position": pos["position"],
|
||||
}
|
||||
|
||||
print("=" * 80)
|
||||
for key in pos["args"]:
|
||||
idx = self.args[key].index(pos["args"][key])
|
||||
self.dataHandler.setArguments(**{key: idx})
|
||||
print(key, idx)
|
||||
|
||||
print("position", cameraData["position"])
|
||||
|
||||
# front
|
||||
cameraData["focalPoint"] = [
|
||||
(cameraData["position"][i] + self.viewForward[i]) for i in range(3)
|
||||
]
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["orientation"] = "front"
|
||||
self.dataHandler.setArguments(orientation=0)
|
||||
yield cameraData
|
||||
|
||||
# back
|
||||
cameraData["focalPoint"] = [
|
||||
(cameraData["position"][i] - self.viewForward[i]) for i in range(3)
|
||||
]
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["orientation"] = "back"
|
||||
self.dataHandler.setArguments(orientation=1)
|
||||
yield cameraData
|
||||
|
||||
# right
|
||||
self.dataHandler.setArguments(orientation=2)
|
||||
cameraData["focalPoint"] = [
|
||||
(cameraData["position"][i] + self.rightDirection[i]) for i in range(3)
|
||||
]
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["orientation"] = "right"
|
||||
yield cameraData
|
||||
|
||||
# left
|
||||
self.dataHandler.setArguments(orientation=3)
|
||||
cameraData["focalPoint"] = [
|
||||
(cameraData["position"][i] - self.rightDirection[i]) for i in range(3)
|
||||
]
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["orientation"] = "left"
|
||||
yield cameraData
|
||||
|
||||
# up
|
||||
self.dataHandler.setArguments(orientation=4)
|
||||
cameraData["focalPoint"] = [
|
||||
(cameraData["position"][i] + self.viewUp[i]) for i in range(3)
|
||||
]
|
||||
cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
|
||||
cameraData["orientation"] = "up"
|
||||
yield cameraData
|
||||
|
||||
# doww
|
||||
self.dataHandler.setArguments(orientation=5)
|
||||
cameraData["focalPoint"] = [
|
||||
(cameraData["position"][i] - self.viewUp[i]) for i in range(3)
|
||||
]
|
||||
cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
|
||||
cameraData["orientation"] = "down"
|
||||
yield cameraData
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MultiView Cube Camera
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class StereoCubeCamera(object):
|
||||
|
||||
# positions = [ { position: [x,y,z], args: { i: 1, j: 0, k: 7 } }, ... ]
|
||||
def __init__(self, dataHandler, viewForward, viewUp, positions, eyeSpacing):
|
||||
self.dataHandler = dataHandler
|
||||
self.cameraSettings = []
|
||||
self.viewForward = viewForward
|
||||
self.viewUp = viewUp
|
||||
self.rightDirection = vectProduct(viewForward, viewUp)
|
||||
self.positions = positions
|
||||
self.eyeSpacing = eyeSpacing
|
||||
|
||||
# Register arguments to the data handler
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="orientation", values=["f", "b", "r", "l", "u", "d"]
|
||||
)
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="eye", values=["left", "right"]
|
||||
)
|
||||
|
||||
# Register arguments to id position
|
||||
self.args = {}
|
||||
for pos in positions:
|
||||
for key in pos["args"]:
|
||||
if key not in self.args:
|
||||
self.args[key] = {}
|
||||
self.args[key][pos["args"][key]] = True
|
||||
|
||||
for key in self.args:
|
||||
self.args[key] = sorted(self.args[key], key=lambda k: int(k))
|
||||
|
||||
self.keyList = self.args.keys()
|
||||
for key in self.args:
|
||||
self.dataHandler.registerArgument(
|
||||
priority=1, name=key, values=self.args[key]
|
||||
)
|
||||
|
||||
self.dataHandler.updateBasePattern()
|
||||
|
||||
def updatePriority(self, priorityList):
|
||||
keyList = ["orientation"]
|
||||
for idx in range(min(len(priorityList), len(keyList))):
|
||||
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
|
||||
|
||||
def __iter__(self):
|
||||
for pos in self.positions:
|
||||
cameraData = {}
|
||||
|
||||
for key in pos["args"]:
|
||||
idx = self.args[key].index(pos["args"][key])
|
||||
self.dataHandler.setArguments(**{key: idx})
|
||||
|
||||
# front
|
||||
cameraData["orientation"] = "front"
|
||||
self.dataHandler.setArguments(orientation=0)
|
||||
deltaVect = [
|
||||
(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
|
||||
]
|
||||
## Left-Eye
|
||||
self.dataHandler.setArguments(eye=0)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] + self.viewForward[i] - deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
## Right-Eye
|
||||
self.dataHandler.setArguments(eye=1)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] + self.viewForward[i] + deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
|
||||
# back
|
||||
cameraData["orientation"] = "back"
|
||||
self.dataHandler.setArguments(orientation=1)
|
||||
deltaVect = [
|
||||
-(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
|
||||
]
|
||||
## Left-Eye
|
||||
self.dataHandler.setArguments(eye=0)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] - self.viewForward[i] - deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
## Right-Eye
|
||||
self.dataHandler.setArguments(eye=1)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] - self.viewForward[i] + deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
|
||||
# right
|
||||
self.dataHandler.setArguments(orientation=2)
|
||||
cameraData["orientation"] = "right"
|
||||
deltaVect = [-(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward]
|
||||
## Left-Eye
|
||||
self.dataHandler.setArguments(eye=0)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] + self.rightDirection[i] - deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
## Right-Eye
|
||||
self.dataHandler.setArguments(eye=1)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] + self.rightDirection[i] + deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
|
||||
# left
|
||||
self.dataHandler.setArguments(orientation=3)
|
||||
cameraData["orientation"] = "left"
|
||||
deltaVect = [(v * float(self.eyeSpacing) * 0.5) for v in self.viewForward]
|
||||
## Left-Eye
|
||||
self.dataHandler.setArguments(eye=0)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] - self.rightDirection[i] - deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
## Right-Eye
|
||||
self.dataHandler.setArguments(eye=1)
|
||||
cameraData["viewUp"] = [self.viewUp[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] - self.rightDirection[i] + deltaVect[i])
|
||||
for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
|
||||
# up
|
||||
self.dataHandler.setArguments(orientation=4)
|
||||
cameraData["orientation"] = "up"
|
||||
deltaVect = [
|
||||
(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
|
||||
]
|
||||
## Left-Eye
|
||||
self.dataHandler.setArguments(eye=0)
|
||||
cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] + self.viewUp[i] - deltaVect[i]) for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
## Right-Eye
|
||||
self.dataHandler.setArguments(eye=1)
|
||||
cameraData["viewUp"] = [(-self.viewForward[i]) for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] + self.viewUp[i] + deltaVect[i]) for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
|
||||
# doww
|
||||
self.dataHandler.setArguments(orientation=5)
|
||||
cameraData["orientation"] = "down"
|
||||
deltaVect = [
|
||||
(v * float(self.eyeSpacing) * 0.5) for v in self.rightDirection
|
||||
]
|
||||
## Left-Eye
|
||||
self.dataHandler.setArguments(eye=0)
|
||||
cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] - deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] - self.viewUp[i] - deltaVect[i]) for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
## Right-Eye
|
||||
self.dataHandler.setArguments(eye=1)
|
||||
cameraData["viewUp"] = [self.viewForward[i] for i in range(3)]
|
||||
cameraData["position"] = [
|
||||
(pos["position"][idx] + deltaVect[idx]) for idx in range(3)
|
||||
]
|
||||
cameraData["focalPoint"] = [
|
||||
(pos["position"][i] - self.viewUp[i] + deltaVect[i]) for i in range(3)
|
||||
]
|
||||
yield cameraData
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MultiView Camera
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MultiViewCamera(object):
|
||||
def __init__(self, dataHandler):
|
||||
self.dataHandler = dataHandler
|
||||
self.cameraSettings = []
|
||||
self.positionNames = []
|
||||
|
||||
def registerViewPoint(self, name, focalPoint, position, viewUp):
|
||||
self.cameraSettings.append(
|
||||
{
|
||||
"name": name,
|
||||
"nameIdx": len(self.positionNames),
|
||||
"focalPoint": focalPoint,
|
||||
"position": position,
|
||||
"viewUp": viewUp,
|
||||
}
|
||||
)
|
||||
self.positionNames.append(name)
|
||||
self.dataHandler.registerArgument(
|
||||
priority=0, name="multiView", values=self.positionNames
|
||||
)
|
||||
self.dataHandler.updateBasePattern()
|
||||
|
||||
def updatePriority(self, priorityList):
|
||||
keyList = ["multiView"]
|
||||
for idx in range(min(len(priorityList), len(keyList))):
|
||||
self.dataHandler.updatePriority(keyList[idx], priorityList[idx])
|
||||
|
||||
def __iter__(self):
|
||||
for cameraData in self.cameraSettings:
|
||||
self.dataHandler.setArguments(multiView=cameraData["nameIdx"])
|
||||
yield cameraData
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helper methods
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def update_camera(renderer, cameraData):
|
||||
camera = renderer.GetActiveCamera()
|
||||
camera.SetPosition(cameraData["position"])
|
||||
camera.SetFocalPoint(cameraData["focalPoint"])
|
||||
camera.SetViewUp(cameraData["viewUp"])
|
||||
|
||||
|
||||
def create_spherical_camera(renderer, dataHandler, phiValues, thetaValues):
|
||||
camera = renderer.GetActiveCamera()
|
||||
return SphericalCamera(
|
||||
dataHandler,
|
||||
camera.GetFocalPoint(),
|
||||
camera.GetPosition(),
|
||||
camera.GetViewUp(),
|
||||
phiValues,
|
||||
thetaValues,
|
||||
)
|
||||
|
||||
|
||||
def create_cylindrical_camera(renderer, dataHandler, phiValues, translationValues):
|
||||
camera = renderer.GetActiveCamera()
|
||||
return CylindricalCamera(
|
||||
dataHandler,
|
||||
camera.GetFocalPoint(),
|
||||
camera.GetPosition(),
|
||||
camera.GetViewUp(),
|
||||
phiValues,
|
||||
translationValues,
|
||||
)
|
||||
@@ -0,0 +1,620 @@
|
||||
import json, os, gzip, shutil
|
||||
|
||||
from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter
|
||||
from vtkmodules.vtkIOImage import vtkPNGReader, vtkPNGWriter, vtkJPEGWriter
|
||||
from vtkmodules.vtkCommonDataModel import vtkImageData
|
||||
from vtkmodules.vtkCommonCore import vtkUnsignedCharArray
|
||||
from vtkmodules.vtkFiltersParallel import vtkPResampleFilter
|
||||
|
||||
from vtkmodules.web import iteritems, getJSArrayType
|
||||
from vtkmodules.web.camera import (
|
||||
update_camera,
|
||||
create_spherical_camera,
|
||||
create_cylindrical_camera,
|
||||
)
|
||||
from vtkmodules.web.query_data_model import DataHandler
|
||||
|
||||
# Global helper variables
|
||||
encode_codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Capture image from render window
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CaptureRenderWindow(object):
|
||||
def __init__(self, magnification=1):
|
||||
self.windowToImage = vtkWindowToImageFilter()
|
||||
self.windowToImage.SetScale(magnification)
|
||||
self.windowToImage.SetInputBufferTypeToRGB()
|
||||
self.windowToImage.ReadFrontBufferOn()
|
||||
self.writer = None
|
||||
|
||||
def SetRenderWindow(self, renderWindow):
|
||||
self.windowToImage.SetInput(renderWindow)
|
||||
|
||||
def SetFormat(self, mimeType):
|
||||
if mimeType == "image/png":
|
||||
self.writer = vtkPNGWriter()
|
||||
self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
|
||||
elif mimeType == "image/jpg":
|
||||
self.writer = vtkJPEGWriter()
|
||||
self.writer.SetInputConnection(self.windowToImage.GetOutputPort())
|
||||
|
||||
def writeImage(self, path):
|
||||
if self.writer:
|
||||
self.windowToImage.Modified()
|
||||
self.windowToImage.Update()
|
||||
self.writer.SetFileName(path)
|
||||
self.writer.Write()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Basic Dataset Builder
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DataSetBuilder(object):
|
||||
def __init__(self, location, camera_data, metadata={}, sections={}):
|
||||
self.dataHandler = DataHandler(location)
|
||||
self.cameraDescription = camera_data
|
||||
self.camera = None
|
||||
self.imageCapture = CaptureRenderWindow()
|
||||
|
||||
for key, value in iteritems(metadata):
|
||||
self.dataHandler.addMetaData(key, value)
|
||||
|
||||
for key, value in iteritems(sections):
|
||||
self.dataHandler.addSection(key, value)
|
||||
|
||||
def getDataHandler(self):
|
||||
return self.dataHandler
|
||||
|
||||
def getCamera(self):
|
||||
return self.camera
|
||||
|
||||
def updateCamera(self, camera):
|
||||
update_camera(self.renderer, camera)
|
||||
self.renderWindow.Render()
|
||||
|
||||
def start(self, renderWindow=None, renderer=None):
|
||||
if renderWindow:
|
||||
# Keep track of renderWindow and renderer
|
||||
self.renderWindow = renderWindow
|
||||
self.renderer = renderer
|
||||
|
||||
# Initialize image capture
|
||||
self.imageCapture.SetRenderWindow(renderWindow)
|
||||
|
||||
# Handle camera if any
|
||||
if self.cameraDescription:
|
||||
if self.cameraDescription["type"] == "spherical":
|
||||
self.camera = create_spherical_camera(
|
||||
renderer,
|
||||
self.dataHandler,
|
||||
self.cameraDescription["phi"],
|
||||
self.cameraDescription["theta"],
|
||||
)
|
||||
elif self.cameraDescription["type"] == "cylindrical":
|
||||
self.camera = create_cylindrical_camera(
|
||||
renderer,
|
||||
self.dataHandler,
|
||||
self.cameraDescription["phi"],
|
||||
self.cameraDescription["translation"],
|
||||
)
|
||||
|
||||
# Update background color
|
||||
bgColor = renderer.GetBackground()
|
||||
bgColorString = "rgb(%d, %d, %d)" % tuple(
|
||||
int(bgColor[i] * 255) for i in range(3)
|
||||
)
|
||||
self.dataHandler.addMetaData("backgroundColor", bgColorString)
|
||||
|
||||
# Update file patterns
|
||||
self.dataHandler.updateBasePattern()
|
||||
|
||||
def stop(self):
|
||||
self.dataHandler.writeDataDescriptor()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Image Dataset Builder
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImageDataSetBuilder(DataSetBuilder):
|
||||
def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
|
||||
DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
|
||||
imageExtenstion = "." + imageMimeType.split("/")[1]
|
||||
self.dataHandler.registerData(
|
||||
name="image", type="blob", mimeType=imageMimeType, fileName=imageExtenstion
|
||||
)
|
||||
self.imageCapture.SetFormat(imageMimeType)
|
||||
|
||||
def writeImage(self):
|
||||
self.imageCapture.writeImage(self.dataHandler.getDataAbsoluteFilePath("image"))
|
||||
|
||||
def writeImages(self):
|
||||
for cam in self.camera:
|
||||
update_camera(self.renderer, cam)
|
||||
self.renderWindow.Render()
|
||||
self.imageCapture.writeImage(
|
||||
self.dataHandler.getDataAbsoluteFilePath("image")
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Volume Composite Dataset Builder
|
||||
# -----------------------------------------------------------------------------
|
||||
class VolumeCompositeDataSetBuilder(DataSetBuilder):
|
||||
def __init__(self, location, imageMimeType, cameraInfo, metadata={}, sections={}):
|
||||
DataSetBuilder.__init__(self, location, cameraInfo, metadata, sections)
|
||||
|
||||
self.dataHandler.addTypes("volume-composite", "rgba+depth")
|
||||
|
||||
self.imageMimeType = imageMimeType
|
||||
self.imageExtenstion = "." + imageMimeType.split("/")[1]
|
||||
|
||||
if imageMimeType == "image/png":
|
||||
self.imageWriter = vtkPNGWriter()
|
||||
if imageMimeType == "image/jpg":
|
||||
self.imageWriter = vtkJPEGWriter()
|
||||
|
||||
self.imageDataColor = vtkImageData()
|
||||
self.imageWriter.SetInputData(self.imageDataColor)
|
||||
|
||||
self.imageDataDepth = vtkImageData()
|
||||
self.depthToWrite = None
|
||||
|
||||
self.layerInfo = {}
|
||||
self.colorByMapping = {}
|
||||
self.compositePipeline = {
|
||||
"layers": [],
|
||||
"dimensions": [],
|
||||
"fields": {},
|
||||
"layer_fields": {},
|
||||
"pipeline": [],
|
||||
}
|
||||
self.activeDepthKey = ""
|
||||
self.activeRGBKey = ""
|
||||
self.nodeWithChildren = {}
|
||||
|
||||
def _getColorCode(self, colorBy):
|
||||
if colorBy in self.colorByMapping:
|
||||
# The color code exist
|
||||
return self.colorByMapping[colorBy]
|
||||
else:
|
||||
# No color code assigned yet
|
||||
colorCode = encode_codes[len(self.colorByMapping)]
|
||||
# Assign color code
|
||||
self.colorByMapping[colorBy] = colorCode
|
||||
# Register color code with color by value
|
||||
self.compositePipeline["fields"][colorCode] = colorBy
|
||||
# Return the color code
|
||||
return colorCode
|
||||
|
||||
def _getLayerCode(self, parent, layerName):
|
||||
if layerName in self.layerInfo:
|
||||
# Layer already exist
|
||||
return (self.layerInfo[layerName]["code"], False)
|
||||
else:
|
||||
layerCode = encode_codes[len(self.layerInfo)]
|
||||
self.layerInfo[layerName] = {
|
||||
"code": layerCode,
|
||||
"name": layerName,
|
||||
"parent": parent,
|
||||
}
|
||||
self.compositePipeline["layers"].append(layerCode)
|
||||
self.compositePipeline["layer_fields"][layerCode] = []
|
||||
|
||||
# Let's register it in the pipeline
|
||||
if parent:
|
||||
if parent not in self.nodeWithChildren:
|
||||
# Need to create parent
|
||||
rootNode = {"name": parent, "ids": [], "children": []}
|
||||
self.nodeWithChildren[parent] = rootNode
|
||||
self.compositePipeline["pipeline"].append(rootNode)
|
||||
|
||||
# Add node to its parent
|
||||
self.nodeWithChildren[parent]["children"].append(
|
||||
{"name": layerName, "ids": [layerCode]}
|
||||
)
|
||||
self.nodeWithChildren[parent]["ids"].append(layerCode)
|
||||
|
||||
else:
|
||||
self.compositePipeline["pipeline"].append(
|
||||
{"name": layerName, "ids": [layerCode]}
|
||||
)
|
||||
|
||||
return (layerCode, True)
|
||||
|
||||
def _needToRegisterColor(self, layerCode, colorCode):
|
||||
if colorCode in self.compositePipeline["layer_fields"][layerCode]:
|
||||
return False
|
||||
else:
|
||||
self.compositePipeline["layer_fields"][layerCode].append(colorCode)
|
||||
return True
|
||||
|
||||
def activateLayer(self, parent, name, colorBy):
|
||||
layerCode, needToRegisterDepth = self._getLayerCode(parent, name)
|
||||
colorCode = self._getColorCode(colorBy)
|
||||
needToRegisterColor = self._needToRegisterColor(layerCode, colorCode)
|
||||
|
||||
# Update active keys
|
||||
self.activeDepthKey = "%s_depth" % layerCode
|
||||
self.activeRGBKey = "%s%s_rgb" % (layerCode, colorCode)
|
||||
|
||||
# Need to register data
|
||||
if needToRegisterDepth:
|
||||
self.dataHandler.registerData(
|
||||
name=self.activeDepthKey,
|
||||
type="array",
|
||||
fileName="/%s_depth.uint8" % layerCode,
|
||||
categories=[layerCode],
|
||||
)
|
||||
|
||||
if needToRegisterColor:
|
||||
self.dataHandler.registerData(
|
||||
name=self.activeRGBKey,
|
||||
type="blob",
|
||||
fileName="/%s%s_rgb%s" % (layerCode, colorCode, self.imageExtenstion),
|
||||
categories=["%s%s" % (layerCode, colorCode)],
|
||||
mimeType=self.imageMimeType,
|
||||
)
|
||||
|
||||
def writeData(self, mapper):
|
||||
width = self.renderWindow.GetSize()[0]
|
||||
height = self.renderWindow.GetSize()[1]
|
||||
|
||||
if not self.depthToWrite:
|
||||
self.depthToWrite = bytearray(width * height)
|
||||
|
||||
for cam in self.camera:
|
||||
self.updateCamera(cam)
|
||||
imagePath = self.dataHandler.getDataAbsoluteFilePath(self.activeRGBKey)
|
||||
depthPath = self.dataHandler.getDataAbsoluteFilePath(self.activeDepthKey)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Write Image
|
||||
# -----------------------------------------------------------------
|
||||
mapper.GetColorImage(self.imageDataColor)
|
||||
self.imageWriter.SetFileName(imagePath)
|
||||
self.imageWriter.Write()
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Write Depth
|
||||
# -----------------------------------------------------------------
|
||||
mapper.GetDepthImage(self.imageDataDepth)
|
||||
inputArray = self.imageDataDepth.GetPointData().GetArray(0)
|
||||
size = inputArray.GetNumberOfTuples()
|
||||
for idx in range(size):
|
||||
self.depthToWrite[idx] = int(inputArray.GetValue(idx))
|
||||
|
||||
with open(depthPath, "wb") as f:
|
||||
f.write(self.depthToWrite)
|
||||
|
||||
def start(self, renderWindow, renderer):
|
||||
DataSetBuilder.start(self, renderWindow, renderer)
|
||||
self.camera.updatePriority([2, 1])
|
||||
|
||||
def stop(self, compress=True):
|
||||
# Push metadata
|
||||
self.compositePipeline["dimensions"] = self.renderWindow.GetSize()
|
||||
self.compositePipeline["default_pipeline"] = (
|
||||
"A".join(self.compositePipeline["layers"]) + "A"
|
||||
)
|
||||
self.dataHandler.addSection("CompositePipeline", self.compositePipeline)
|
||||
|
||||
# Write metadata
|
||||
DataSetBuilder.stop(self)
|
||||
|
||||
if compress:
|
||||
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
|
||||
print("Compress", root)
|
||||
for name in files:
|
||||
if ".uint8" in name and ".gz" not in name:
|
||||
with open(os.path.join(root, name), "rb") as f_in:
|
||||
with gzip.open(
|
||||
os.path.join(root, name + ".gz"), "wb"
|
||||
) as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
os.remove(os.path.join(root, name))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Data Prober Dataset Builder
|
||||
# -----------------------------------------------------------------------------
|
||||
class DataProberDataSetBuilder(DataSetBuilder):
|
||||
def __init__(
|
||||
self,
|
||||
location,
|
||||
sampling_dimesions,
|
||||
fields_to_keep,
|
||||
custom_probing_bounds=None,
|
||||
metadata={},
|
||||
):
|
||||
DataSetBuilder.__init__(self, location, None, metadata)
|
||||
self.fieldsToWrite = fields_to_keep
|
||||
self.resamplerFilter = vtkPResampleFilter()
|
||||
self.resamplerFilter.SetSamplingDimension(sampling_dimesions)
|
||||
if custom_probing_bounds:
|
||||
self.resamplerFilter.SetUseInputBounds(0)
|
||||
self.resamplerFilter.SetCustomSamplingBounds(custom_probing_bounds)
|
||||
else:
|
||||
self.resamplerFilter.SetUseInputBounds(1)
|
||||
|
||||
# Register all fields
|
||||
self.dataHandler.addTypes("data-prober", "binary")
|
||||
self.DataProber = {
|
||||
"types": {},
|
||||
"dimensions": sampling_dimesions,
|
||||
"ranges": {},
|
||||
"spacing": [1, 1, 1],
|
||||
}
|
||||
for field in self.fieldsToWrite:
|
||||
self.dataHandler.registerData(
|
||||
name=field, type="array", fileName="/%s.array" % field
|
||||
)
|
||||
|
||||
def setDataToProbe(self, dataset):
|
||||
self.resamplerFilter.SetInputData(dataset)
|
||||
|
||||
def setSourceToProbe(self, source):
|
||||
self.resamplerFilter.SetInputConnection(source.GetOutputPort())
|
||||
|
||||
def writeData(self):
|
||||
self.resamplerFilter.Update()
|
||||
arrays = self.resamplerFilter.GetOutput().GetPointData()
|
||||
for field in self.fieldsToWrite:
|
||||
array = arrays.GetArray(field)
|
||||
if array:
|
||||
b = memoryview(array)
|
||||
with open(self.dataHandler.getDataAbsoluteFilePath(field), "wb") as f:
|
||||
f.write(b)
|
||||
|
||||
self.DataProber["types"][field] = getJSArrayType(array)
|
||||
if field in self.DataProber["ranges"]:
|
||||
dataRange = array.GetRange()
|
||||
if dataRange[0] < self.DataProber["ranges"][field][0]:
|
||||
self.DataProber["ranges"][field][0] = dataRange[0]
|
||||
if dataRange[1] > self.DataProber["ranges"][field][1]:
|
||||
self.DataProber["ranges"][field][1] = dataRange[1]
|
||||
else:
|
||||
self.DataProber["ranges"][field] = [
|
||||
array.GetRange()[0],
|
||||
array.GetRange()[1],
|
||||
]
|
||||
else:
|
||||
print("No array for", field)
|
||||
print(self.resamplerFilter.GetOutput())
|
||||
|
||||
def stop(self, compress=True):
|
||||
# Push metadata
|
||||
self.dataHandler.addSection("DataProber", self.DataProber)
|
||||
|
||||
# Write metadata
|
||||
DataSetBuilder.stop(self)
|
||||
|
||||
if compress:
|
||||
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
|
||||
print("Compress", root)
|
||||
for name in files:
|
||||
if ".array" in name and ".gz" not in name:
|
||||
with open(os.path.join(root, name), "rb") as f_in:
|
||||
with gzip.open(
|
||||
os.path.join(root, name + ".gz"), "wb"
|
||||
) as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
os.remove(os.path.join(root, name))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Sorted Composite Dataset Builder
|
||||
# -----------------------------------------------------------------------------
|
||||
class ConvertVolumeStackToSortedStack(object):
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.layers = 0
|
||||
|
||||
def convert(self, directory):
|
||||
imagePaths = {}
|
||||
depthPaths = {}
|
||||
layerNames = []
|
||||
for fileName in os.listdir(directory):
|
||||
if "_rgb" in fileName or "_depth" in fileName:
|
||||
fileId = fileName.split("_")[0][0]
|
||||
if "_rgb" in fileName:
|
||||
imagePaths[fileId] = os.path.join(directory, fileName)
|
||||
else:
|
||||
layerNames.append(fileId)
|
||||
depthPaths[fileId] = os.path.join(directory, fileName)
|
||||
|
||||
layerNames.sort()
|
||||
|
||||
if len(layerNames) == 0:
|
||||
return
|
||||
|
||||
# Load data in Memory
|
||||
depthArrays = []
|
||||
imageReader = vtkPNGReader()
|
||||
numberOfValues = self.width * self.height * len(layerNames)
|
||||
imageSize = self.width * self.height
|
||||
self.layers = len(layerNames)
|
||||
|
||||
# Write all images as single memoryview
|
||||
opacity = vtkUnsignedCharArray()
|
||||
opacity.SetNumberOfComponents(1)
|
||||
opacity.SetNumberOfTuples(numberOfValues)
|
||||
|
||||
intensity = vtkUnsignedCharArray()
|
||||
intensity.SetNumberOfComponents(1)
|
||||
intensity.SetNumberOfTuples(numberOfValues)
|
||||
|
||||
for layer in range(self.layers):
|
||||
imageReader.SetFileName(imagePaths[layerNames[layer]])
|
||||
imageReader.Update()
|
||||
|
||||
rgbaArray = imageReader.GetOutput().GetPointData().GetArray(0)
|
||||
|
||||
for idx in range(imageSize):
|
||||
intensity.SetValue(
|
||||
(layer * imageSize) + idx, rgbaArray.GetValue(idx * 4)
|
||||
)
|
||||
opacity.SetValue(
|
||||
(layer * imageSize) + idx, rgbaArray.GetValue(idx * 4 + 3)
|
||||
)
|
||||
|
||||
with open(depthPaths[layerNames[layer]], "rb") as depthFile:
|
||||
depthArrays.append(depthFile.read())
|
||||
|
||||
# Apply pixel sorting
|
||||
destOrder = vtkUnsignedCharArray()
|
||||
destOrder.SetNumberOfComponents(1)
|
||||
destOrder.SetNumberOfTuples(numberOfValues)
|
||||
|
||||
opacityOrder = vtkUnsignedCharArray()
|
||||
opacityOrder.SetNumberOfComponents(1)
|
||||
opacityOrder.SetNumberOfTuples(numberOfValues)
|
||||
|
||||
intensityOrder = vtkUnsignedCharArray()
|
||||
intensityOrder.SetNumberOfComponents(1)
|
||||
intensityOrder.SetNumberOfTuples(numberOfValues)
|
||||
|
||||
for pixelIdx in range(imageSize):
|
||||
depthStack = []
|
||||
for depthArray in depthArrays:
|
||||
depthStack.append((depthArray[pixelIdx], len(depthStack)))
|
||||
depthStack.sort(key=lambda tup: tup[0])
|
||||
|
||||
for destLayerIdx in range(len(depthStack)):
|
||||
sourceLayerIdx = depthStack[destLayerIdx][1]
|
||||
|
||||
# Copy Idx
|
||||
destOrder.SetValue(
|
||||
(imageSize * destLayerIdx) + pixelIdx, sourceLayerIdx
|
||||
)
|
||||
opacityOrder.SetValue(
|
||||
(imageSize * destLayerIdx) + pixelIdx,
|
||||
opacity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
|
||||
)
|
||||
intensityOrder.SetValue(
|
||||
(imageSize * destLayerIdx) + pixelIdx,
|
||||
intensity.GetValue((imageSize * sourceLayerIdx) + pixelIdx),
|
||||
)
|
||||
|
||||
with open(os.path.join(directory, "alpha.uint8"), "wb") as f:
|
||||
f.write(memoryview(opacityOrder))
|
||||
|
||||
with open(os.path.join(directory, "intensity.uint8"), "wb") as f:
|
||||
f.write(memoryview(intensityOrder))
|
||||
|
||||
with open(os.path.join(directory, "order.uint8"), "wb") as f:
|
||||
f.write(memoryview(destOrder))
|
||||
|
||||
|
||||
class SortedCompositeDataSetBuilder(VolumeCompositeDataSetBuilder):
|
||||
def __init__(self, location, cameraInfo, metadata={}, sections={}):
|
||||
VolumeCompositeDataSetBuilder.__init__(
|
||||
self, location, "image/png", cameraInfo, metadata, sections
|
||||
)
|
||||
self.dataHandler.addTypes("sorted-composite", "rgba")
|
||||
|
||||
# Register order and color textures
|
||||
self.layerScalars = []
|
||||
self.dataHandler.registerData(
|
||||
name="order", type="array", fileName="/order.uint8"
|
||||
)
|
||||
self.dataHandler.registerData(
|
||||
name="alpha", type="array", fileName="/alpha.uint8"
|
||||
)
|
||||
self.dataHandler.registerData(
|
||||
name="intensity",
|
||||
type="array",
|
||||
fileName="/intensity.uint8",
|
||||
categories=["intensity"],
|
||||
)
|
||||
|
||||
def start(self, renderWindow, renderer):
|
||||
VolumeCompositeDataSetBuilder.start(self, renderWindow, renderer)
|
||||
imageSize = self.renderWindow.GetSize()
|
||||
self.dataConverter = ConvertVolumeStackToSortedStack(imageSize[0], imageSize[1])
|
||||
|
||||
def activateLayer(self, colorBy, scalar):
|
||||
VolumeCompositeDataSetBuilder.activateLayer(
|
||||
self, "root", "%s" % scalar, colorBy
|
||||
)
|
||||
self.layerScalars.append(scalar)
|
||||
|
||||
def writeData(self, mapper):
|
||||
VolumeCompositeDataSetBuilder.writeData(self, mapper)
|
||||
|
||||
# Fill data pattern
|
||||
self.dataHandler.getDataAbsoluteFilePath("order")
|
||||
self.dataHandler.getDataAbsoluteFilePath("alpha")
|
||||
self.dataHandler.getDataAbsoluteFilePath("intensity")
|
||||
|
||||
def stop(self, clean=True, compress=True):
|
||||
VolumeCompositeDataSetBuilder.stop(self, compress=False)
|
||||
|
||||
# Go through all directories and convert them
|
||||
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
|
||||
for name in dirs:
|
||||
print("Process", os.path.join(root, name))
|
||||
self.dataConverter.convert(os.path.join(root, name))
|
||||
|
||||
# Rename index.json to info_origin.json
|
||||
os.rename(
|
||||
os.path.join(self.dataHandler.getBasePath(), "index.json"),
|
||||
os.path.join(self.dataHandler.getBasePath(), "index_origin.json"),
|
||||
)
|
||||
|
||||
# Update index.json
|
||||
with open(
|
||||
os.path.join(self.dataHandler.getBasePath(), "index_origin.json"), "r"
|
||||
) as infoFile:
|
||||
metadata = json.load(infoFile)
|
||||
metadata["SortedComposite"] = {
|
||||
"dimensions": metadata["CompositePipeline"]["dimensions"],
|
||||
"layers": self.dataConverter.layers,
|
||||
"scalars": self.layerScalars[0 : self.dataConverter.layers],
|
||||
}
|
||||
|
||||
# Clean metadata
|
||||
dataToKeep = []
|
||||
del metadata["CompositePipeline"]
|
||||
for item in metadata["data"]:
|
||||
if item["name"] in ["order", "alpha", "intensity"]:
|
||||
dataToKeep.append(item)
|
||||
metadata["data"] = dataToKeep
|
||||
metadata["type"] = ["tonic-query-data-model", "sorted-composite", "alpha"]
|
||||
|
||||
# Override index.json
|
||||
with open(
|
||||
os.path.join(self.dataHandler.getBasePath(), "index.json"), "w"
|
||||
) as newMetaFile:
|
||||
newMetaFile.write(json.dumps(metadata))
|
||||
|
||||
# Clean temporary data
|
||||
if clean:
|
||||
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
|
||||
print("Clean", root)
|
||||
for name in files:
|
||||
if (
|
||||
"_rgb.png" in name
|
||||
or "_depth.uint8" in name
|
||||
or name == "index_origin.json"
|
||||
):
|
||||
os.remove(os.path.join(root, name))
|
||||
|
||||
if compress:
|
||||
for root, dirs, files in os.walk(self.dataHandler.getBasePath()):
|
||||
print("Compress", root)
|
||||
for name in files:
|
||||
if ".uint8" in name and ".gz" not in name:
|
||||
with open(os.path.join(root, name), "rb") as f_in:
|
||||
with gzip.open(
|
||||
os.path.join(root, name + ".gz"), "wb"
|
||||
) as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
os.remove(os.path.join(root, name))
|
||||
@@ -0,0 +1,12 @@
|
||||
WEB_DEPENDENCY_MISSING_MESSAGE = """Please install VTK's Web module dependencies.
|
||||
|
||||
These include `wslink` and can be easily installed with vtk by using the
|
||||
`web` extra requirements option. For example:
|
||||
|
||||
pip install vtk[web]
|
||||
|
||||
"""
|
||||
|
||||
class WebDependencyMissingError(ImportError):
|
||||
def __init__(self, message=WEB_DEPENDENCY_MISSING_MESSAGE):
|
||||
super().__init__(message)
|
||||
@@ -0,0 +1,842 @@
|
||||
r"""protocols is a module that contains a set of VTK Web related
|
||||
protocols that can be combined together to provide a flexible way to define
|
||||
very specific web application.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os, sys, logging, types, inspect, traceback, re, base64, time
|
||||
|
||||
from vtkmodules.vtkWebCore import vtkWebInteractionEvent
|
||||
|
||||
from vtkmodules.web.errors import WebDependencyMissingError
|
||||
from vtkmodules.web.render_window_serializer import (
|
||||
serializeInstance,
|
||||
SynchronizationContext,
|
||||
getReferenceId,
|
||||
initializeSerializers,
|
||||
)
|
||||
|
||||
try:
|
||||
from wslink import schedule_callback
|
||||
from wslink import register as exportRpc
|
||||
from wslink.websocket import LinkProtocol
|
||||
except ImportError:
|
||||
raise WebDependencyMissingError()
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Base class for any VTK Web based protocol
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebProtocol(LinkProtocol):
|
||||
def getApplication(self):
|
||||
return self.getSharedObject("app")
|
||||
|
||||
# no need for a setApplication anymore, but keep for compatibility
|
||||
def setApplication(self, app):
|
||||
pass
|
||||
|
||||
def mapIdToObject(self, id):
|
||||
"""
|
||||
Maps global-id for a vtkObject to the vtkObject instance. May return None if the
|
||||
id is not valid.
|
||||
"""
|
||||
id = int(id)
|
||||
if id <= 0:
|
||||
return None
|
||||
return self.getApplication().GetObjectIdMap().GetVTKObject(id)
|
||||
|
||||
def getGlobalId(self, obj):
|
||||
"""
|
||||
Return the id for a given vtkObject
|
||||
"""
|
||||
return self.getApplication().GetObjectIdMap().GetGlobalId(obj)
|
||||
|
||||
def freeObject(self, obj):
|
||||
"""
|
||||
Delete the given vtkObject from the objectIdMap. Returns true if delete succeeded.
|
||||
"""
|
||||
return self.getApplication().GetObjectIdMap().FreeObject(obj)
|
||||
|
||||
def freeObjectById(self, id):
|
||||
"""
|
||||
Delete the vtkObject corresponding to the given objectId from the objectIdMap.
|
||||
Returns true if delete succeeded.
|
||||
"""
|
||||
return self.getApplication().GetObjectIdMap().FreeObjectById(id)
|
||||
|
||||
def getView(self, vid):
|
||||
"""
|
||||
Returns the view for a given view ID, if vid is None then return the
|
||||
current active view.
|
||||
:param vid: The view ID
|
||||
:type vid: str
|
||||
"""
|
||||
v = self.mapIdToObject(vid)
|
||||
|
||||
if not v:
|
||||
# Use active view is none provided.
|
||||
v = self.getApplication().GetObjectIdMap().GetActiveObject("VIEW")
|
||||
if not v:
|
||||
raise Exception("no view provided: %s" % vid)
|
||||
|
||||
return v
|
||||
|
||||
def setActiveView(self, view):
|
||||
"""
|
||||
Set a vtkRenderWindow to be the active one
|
||||
"""
|
||||
self.getApplication().GetObjectIdMap().SetActiveObject("VIEW", view)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Handle Mouse interaction on any type of view
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebMouseHandler(vtkWebProtocol):
|
||||
@exportRpc("viewport.mouse.interaction")
|
||||
def mouseInteraction(self, event):
|
||||
"""
|
||||
RPC Callback for mouse interactions.
|
||||
"""
|
||||
view = self.getView(event["view"])
|
||||
|
||||
buttons = 0
|
||||
if event["buttonLeft"]:
|
||||
buttons |= vtkWebInteractionEvent.LEFT_BUTTON
|
||||
if event["buttonMiddle"]:
|
||||
buttons |= vtkWebInteractionEvent.MIDDLE_BUTTON
|
||||
if event["buttonRight"]:
|
||||
buttons |= vtkWebInteractionEvent.RIGHT_BUTTON
|
||||
|
||||
modifiers = 0
|
||||
if event["shiftKey"]:
|
||||
modifiers |= vtkWebInteractionEvent.SHIFT_KEY
|
||||
if event["ctrlKey"]:
|
||||
modifiers |= vtkWebInteractionEvent.CTRL_KEY
|
||||
if event["altKey"]:
|
||||
modifiers |= vtkWebInteractionEvent.ALT_KEY
|
||||
if event["metaKey"]:
|
||||
modifiers |= vtkWebInteractionEvent.META_KEY
|
||||
|
||||
pvevent = vtkWebInteractionEvent()
|
||||
pvevent.SetButtons(buttons)
|
||||
pvevent.SetModifiers(modifiers)
|
||||
if "x" in event:
|
||||
pvevent.SetX(event["x"])
|
||||
if "y" in event:
|
||||
pvevent.SetY(event["y"])
|
||||
if "scroll" in event:
|
||||
pvevent.SetScroll(event["scroll"])
|
||||
if event["action"] == "dblclick":
|
||||
pvevent.SetRepeatCount(2)
|
||||
# pvevent.SetKeyCode(event["charCode"])
|
||||
retVal = self.getApplication().HandleInteractionEvent(view, pvevent)
|
||||
del pvevent
|
||||
|
||||
if event["action"] == "down":
|
||||
self.getApplication().InvokeEvent("StartInteractionEvent")
|
||||
|
||||
if event["action"] == "up":
|
||||
self.getApplication().InvokeEvent("EndInteractionEvent")
|
||||
|
||||
if retVal:
|
||||
self.getApplication().InvokeEvent("UpdateEvent")
|
||||
|
||||
return retVal
|
||||
|
||||
@exportRpc("viewport.mouse.zoom.wheel")
|
||||
def updateZoomFromWheel(self, event):
|
||||
if "Start" in event["type"]:
|
||||
self.getApplication().InvokeEvent("StartInteractionEvent")
|
||||
|
||||
renderWindow = self.getView(event["view"])
|
||||
if renderWindow and "spinY" in event:
|
||||
zoomFactor = 1.0 - event["spinY"] / 10.0
|
||||
|
||||
camera = renderWindow.GetRenderers().GetFirstRenderer().GetActiveCamera()
|
||||
fp = camera.GetFocalPoint()
|
||||
pos = camera.GetPosition()
|
||||
delta = [fp[i] - pos[i] for i in range(3)]
|
||||
camera.Zoom(zoomFactor)
|
||||
|
||||
pos2 = camera.GetPosition()
|
||||
camera.SetFocalPoint([pos2[i] + delta[i] for i in range(3)])
|
||||
renderWindow.Modified()
|
||||
|
||||
if "End" in event["type"]:
|
||||
self.getApplication().InvokeEvent("EndInteractionEvent")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Basic 3D Viewport API (Camera + Orientation + CenterOfRotation
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebViewPort(vtkWebProtocol):
|
||||
@exportRpc("viewport.camera.reset")
|
||||
def resetCamera(self, viewId):
|
||||
"""
|
||||
RPC callback to reset camera.
|
||||
"""
|
||||
view = self.getView(viewId)
|
||||
renderer = view.GetRenderers().GetFirstRenderer()
|
||||
renderer.ResetCamera()
|
||||
|
||||
self.getApplication().InvalidateCache(view)
|
||||
self.getApplication().InvokeEvent("UpdateEvent")
|
||||
|
||||
return str(self.getGlobalId(view))
|
||||
|
||||
@exportRpc("viewport.axes.orientation.visibility.update")
|
||||
def updateOrientationAxesVisibility(self, viewId, showAxis):
|
||||
"""
|
||||
RPC callback to show/hide OrientationAxis.
|
||||
"""
|
||||
view = self.getView(viewId)
|
||||
# FIXME seb: view.OrientationAxesVisibility = (showAxis if 1 else 0);
|
||||
|
||||
self.getApplication().InvalidateCache(view)
|
||||
self.getApplication().InvokeEvent("UpdateEvent")
|
||||
|
||||
return str(self.getGlobalId(view))
|
||||
|
||||
@exportRpc("viewport.axes.center.visibility.update")
|
||||
def updateCenterAxesVisibility(self, viewId, showAxis):
|
||||
"""
|
||||
RPC callback to show/hide CenterAxesVisibility.
|
||||
"""
|
||||
view = self.getView(viewId)
|
||||
# FIXME seb: view.CenterAxesVisibility = (showAxis if 1 else 0);
|
||||
|
||||
self.getApplication().InvalidateCache(view)
|
||||
self.getApplication().InvokeEvent("UpdateEvent")
|
||||
|
||||
return str(self.getGlobalId(view))
|
||||
|
||||
@exportRpc("viewport.camera.update")
|
||||
def updateCamera(self, view_id, focal_point, view_up, position, forceUpdate=True):
|
||||
view = self.getView(view_id)
|
||||
|
||||
camera = view.GetRenderers().GetFirstRenderer().GetActiveCamera()
|
||||
camera.SetFocalPoint(focal_point)
|
||||
camera.SetViewUp(view_up)
|
||||
camera.SetPosition(position)
|
||||
|
||||
if forceUpdate:
|
||||
self.getApplication().InvalidateCache(view)
|
||||
self.getApplication().InvokeEvent("UpdateEvent")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Provide Image delivery mechanism (deprecated - will be removed in VTK 10+)
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebViewPortImageDelivery(vtkWebProtocol):
|
||||
@exportRpc("viewport.image.render")
|
||||
def stillRender(self, options):
|
||||
"""
|
||||
RPC Callback to render a view and obtain the rendered image.
|
||||
"""
|
||||
beginTime = int(round(time.time() * 1000))
|
||||
view = self.getView(options["view"])
|
||||
size = [view.GetSize()[0], view.GetSize()[1]]
|
||||
# use existing size, overridden only if options["size"] is set.
|
||||
resize = size != options.get("size", size)
|
||||
if resize:
|
||||
size = options["size"]
|
||||
if size[0] > 0 and size[1] > 0:
|
||||
view.SetSize(size)
|
||||
t = 0
|
||||
if options and "mtime" in options:
|
||||
t = options["mtime"]
|
||||
quality = 100
|
||||
if options and "quality" in options:
|
||||
quality = options["quality"]
|
||||
localTime = 0
|
||||
if options and "localTime" in options:
|
||||
localTime = options["localTime"]
|
||||
reply = {}
|
||||
app = self.getApplication()
|
||||
if t == 0:
|
||||
app.InvalidateCache(view)
|
||||
reply["image"] = app.StillRenderToString(view, t, quality)
|
||||
# Check that we are getting image size we have set. If not, wait until we
|
||||
# do. The render call will set the actual window size.
|
||||
tries = 10
|
||||
while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
|
||||
app.InvalidateCache(view)
|
||||
reply["image"] = app.StillRenderToString(view, t, quality)
|
||||
tries -= 1
|
||||
|
||||
reply["stale"] = app.GetHasImagesBeingProcessed(view)
|
||||
reply["mtime"] = app.GetLastStillRenderToMTime()
|
||||
reply["size"] = [view.GetSize()[0], view.GetSize()[1]]
|
||||
reply["format"] = "jpeg;base64"
|
||||
reply["global_id"] = str(self.getGlobalId(view))
|
||||
reply["localTime"] = localTime
|
||||
|
||||
endTime = int(round(time.time() * 1000))
|
||||
reply["workTime"] = endTime - beginTime
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Provide publish-based Image delivery mechanism
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebPublishImageDelivery(vtkWebProtocol):
|
||||
def __init__(self, decode=True):
|
||||
super(vtkWebPublishImageDelivery, self).__init__()
|
||||
self.trackingViews = {}
|
||||
self.lastStaleTime = 0
|
||||
self.staleHandlerCount = 0
|
||||
self.deltaStaleTimeBeforeRender = 0.5 # 0.5s
|
||||
self.decode = decode
|
||||
self.viewsInAnimations = []
|
||||
self.targetFrameRate = 30.0
|
||||
self.minFrameRate = 12.0
|
||||
self.maxFrameRate = 30.0
|
||||
|
||||
def pushRender(self, vId, ignoreAnimation=False):
|
||||
if vId not in self.trackingViews:
|
||||
return
|
||||
|
||||
if not self.trackingViews[vId]["enabled"]:
|
||||
return
|
||||
|
||||
if not ignoreAnimation and len(self.viewsInAnimations) > 0:
|
||||
return
|
||||
|
||||
if "originalSize" not in self.trackingViews[vId]:
|
||||
view = self.getView(vId)
|
||||
self.trackingViews[vId]["originalSize"] = list(view.GetSize())
|
||||
|
||||
if "ratio" not in self.trackingViews[vId]:
|
||||
self.trackingViews[vId]["ratio"] = 1
|
||||
|
||||
ratio = self.trackingViews[vId]["ratio"]
|
||||
mtime = self.trackingViews[vId]["mtime"]
|
||||
quality = self.trackingViews[vId]["quality"]
|
||||
size = [int(s * ratio) for s in self.trackingViews[vId]["originalSize"]]
|
||||
|
||||
reply = self.stillRender(
|
||||
{"view": vId, "mtime": mtime, "quality": quality, "size": size}
|
||||
)
|
||||
stale = reply["stale"]
|
||||
if reply["image"]:
|
||||
# depending on whether the app has encoding enabled:
|
||||
if self.decode:
|
||||
reply["image"] = base64.standard_b64decode(reply["image"])
|
||||
|
||||
reply["image"] = self.addAttachment(reply["image"])
|
||||
reply["format"] = "jpeg"
|
||||
# save mtime for next call.
|
||||
self.trackingViews[vId]["mtime"] = reply["mtime"]
|
||||
# echo back real ID, instead of -1 for 'active'
|
||||
reply["id"] = vId
|
||||
self.publish("viewport.image.push.subscription", reply)
|
||||
if stale:
|
||||
self.lastStaleTime = time.time()
|
||||
if self.staleHandlerCount == 0:
|
||||
self.staleHandlerCount += 1
|
||||
schedule_callback(
|
||||
self.deltaStaleTimeBeforeRender, lambda: self.renderStaleImage(vId)
|
||||
)
|
||||
else:
|
||||
self.lastStaleTime = 0
|
||||
|
||||
def renderStaleImage(self, vId):
|
||||
self.staleHandlerCount -= 1
|
||||
|
||||
if self.lastStaleTime != 0:
|
||||
delta = time.time() - self.lastStaleTime
|
||||
if delta >= self.deltaStaleTimeBeforeRender:
|
||||
self.pushRender(vId)
|
||||
else:
|
||||
self.staleHandlerCount += 1
|
||||
schedule_callback(
|
||||
self.deltaStaleTimeBeforeRender - delta + 0.001,
|
||||
lambda: self.renderStaleImage(vId),
|
||||
)
|
||||
|
||||
def animate(self):
|
||||
if len(self.viewsInAnimations) == 0:
|
||||
return
|
||||
|
||||
nextAnimateTime = time.time() + 1.0 / self.targetFrameRate
|
||||
for vId in self.viewsInAnimations:
|
||||
self.pushRender(vId, True)
|
||||
|
||||
nextAnimateTime -= time.time()
|
||||
|
||||
if self.targetFrameRate > self.maxFrameRate:
|
||||
self.targetFrameRate = self.maxFrameRate
|
||||
|
||||
if nextAnimateTime < 0:
|
||||
if nextAnimateTime < -1.0:
|
||||
self.targetFrameRate = 1
|
||||
if self.targetFrameRate > self.minFrameRate:
|
||||
self.targetFrameRate -= 1.0
|
||||
schedule_callback(0.001, lambda: self.animate())
|
||||
else:
|
||||
if self.targetFrameRate < self.maxFrameRate and nextAnimateTime > 0.005:
|
||||
self.targetFrameRate += 1.0
|
||||
schedule_callback(nextAnimateTime, lambda: self.animate())
|
||||
|
||||
@exportRpc("viewport.image.animation.fps.max")
|
||||
def setMaxFrameRate(self, fps=30):
|
||||
self.maxFrameRate = fps
|
||||
|
||||
@exportRpc("viewport.image.animation.fps.get")
|
||||
def getCurrentFrameRate(self):
|
||||
return self.targetFrameRate
|
||||
|
||||
@exportRpc("viewport.image.animation.start")
|
||||
def startViewAnimation(self, viewId="-1"):
|
||||
sView = self.getView(viewId)
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
|
||||
self.viewsInAnimations.append(realViewId)
|
||||
if len(self.viewsInAnimations) == 1:
|
||||
self.animate()
|
||||
|
||||
@exportRpc("viewport.image.animation.stop")
|
||||
def stopViewAnimation(self, viewId="-1"):
|
||||
sView = self.getView(viewId)
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
|
||||
if realViewId in self.viewsInAnimations:
|
||||
self.viewsInAnimations.remove(realViewId)
|
||||
|
||||
@exportRpc("viewport.image.push")
|
||||
def imagePush(self, options):
|
||||
sView = self.getView(options["view"])
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
# Make sure an image is pushed
|
||||
self.getApplication().InvalidateCache(sView)
|
||||
self.pushRender(realViewId)
|
||||
|
||||
# Internal function since the reply[image] is not
|
||||
# JSON(serializable) it can not be an RPC one
|
||||
def stillRender(self, options):
|
||||
"""
|
||||
RPC Callback to render a view and obtain the rendered image.
|
||||
"""
|
||||
beginTime = int(round(time.time() * 1000))
|
||||
view = self.getView(options["view"])
|
||||
size = view.GetSize()[0:2]
|
||||
resize = size != options.get("size", size)
|
||||
if resize:
|
||||
size = options["size"]
|
||||
if size[0] > 10 and size[1] > 10:
|
||||
view.SetSize(size)
|
||||
t = 0
|
||||
if options and "mtime" in options:
|
||||
t = options["mtime"]
|
||||
quality = 100
|
||||
if options and "quality" in options:
|
||||
quality = options["quality"]
|
||||
localTime = 0
|
||||
if options and "localTime" in options:
|
||||
localTime = options["localTime"]
|
||||
reply = {}
|
||||
app = self.getApplication()
|
||||
if t == 0:
|
||||
app.InvalidateCache(view)
|
||||
if self.decode:
|
||||
stillRender = app.StillRenderToString
|
||||
else:
|
||||
stillRender = app.StillRenderToBuffer
|
||||
reply_image = stillRender(view, t, quality)
|
||||
|
||||
# Check that we are getting image size we have set if not wait until we
|
||||
# do. The render call will set the actual window size.
|
||||
tries = 10
|
||||
while resize and list(view.GetSize()) != size and size != [0, 0] and tries > 0:
|
||||
app.InvalidateCache(view)
|
||||
reply_image = stillRender(view, t, quality)
|
||||
tries -= 1
|
||||
|
||||
if (
|
||||
not resize
|
||||
and options
|
||||
and ("clearCache" in options)
|
||||
and options["clearCache"]
|
||||
):
|
||||
app.InvalidateCache(view)
|
||||
reply_image = stillRender(view, t, quality)
|
||||
|
||||
reply["stale"] = app.GetHasImagesBeingProcessed(view)
|
||||
reply["mtime"] = app.GetLastStillRenderToMTime()
|
||||
reply["size"] = view.GetSize()[0:2]
|
||||
reply["memsize"] = reply_image.GetDataSize() if reply_image else 0
|
||||
reply["format"] = "jpeg;base64" if self.decode else "jpeg"
|
||||
reply["global_id"] = str(self.getGlobalId(view))
|
||||
reply["localTime"] = localTime
|
||||
if self.decode:
|
||||
reply["image"] = reply_image
|
||||
else:
|
||||
# Convert the vtkUnsignedCharArray into a bytes object, required by Autobahn websockets
|
||||
reply["image"] = memoryview(reply_image).tobytes() if reply_image else None
|
||||
|
||||
endTime = int(round(time.time() * 1000))
|
||||
reply["workTime"] = endTime - beginTime
|
||||
|
||||
return reply
|
||||
|
||||
@exportRpc("viewport.image.push.observer.add")
|
||||
def addRenderObserver(self, viewId):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
|
||||
if not realViewId in self.trackingViews:
|
||||
observerCallback = lambda *args, **kwargs: self.pushRender(realViewId)
|
||||
startCallback = lambda *args, **kwargs: self.startViewAnimation(realViewId)
|
||||
stopCallback = lambda *args, **kwargs: self.stopViewAnimation(realViewId)
|
||||
tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
|
||||
tagStart = self.getApplication().AddObserver(
|
||||
"StartInteractionEvent", startCallback
|
||||
)
|
||||
tagStop = self.getApplication().AddObserver(
|
||||
"EndInteractionEvent", stopCallback
|
||||
)
|
||||
# TODO do we need self.getApplication().AddObserver('ResetActiveView', resetActiveView())
|
||||
self.trackingViews[realViewId] = {
|
||||
"tags": [tag, tagStart, tagStop],
|
||||
"observerCount": 1,
|
||||
"mtime": 0,
|
||||
"enabled": True,
|
||||
"quality": 100,
|
||||
}
|
||||
else:
|
||||
# There is an observer on this view already
|
||||
self.trackingViews[realViewId]["observerCount"] += 1
|
||||
|
||||
self.pushRender(realViewId)
|
||||
return {"success": True, "viewId": realViewId}
|
||||
|
||||
@exportRpc("viewport.image.push.observer.remove")
|
||||
def removeRenderObserver(self, viewId):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
|
||||
observerInfo = None
|
||||
if realViewId in self.trackingViews:
|
||||
observerInfo = self.trackingViews[realViewId]
|
||||
|
||||
if not observerInfo:
|
||||
return {"error": "Unable to find subscription for view %s" % realViewId}
|
||||
|
||||
observerInfo["observerCount"] -= 1
|
||||
|
||||
if observerInfo["observerCount"] <= 0:
|
||||
for tag in observerInfo["tags"]:
|
||||
self.getApplication().RemoveObserver(tag)
|
||||
del self.trackingViews[realViewId]
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
@exportRpc("viewport.image.push.quality")
|
||||
def setViewQuality(self, viewId, quality, ratio=1):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
observerInfo = None
|
||||
if realViewId in self.trackingViews:
|
||||
observerInfo = self.trackingViews[realViewId]
|
||||
|
||||
if not observerInfo:
|
||||
return {"error": "Unable to find subscription for view %s" % realViewId}
|
||||
|
||||
observerInfo["quality"] = quality
|
||||
observerInfo["ratio"] = ratio
|
||||
|
||||
# Update image size right now!
|
||||
if "originalSize" in self.trackingViews[realViewId]:
|
||||
size = [
|
||||
int(s * ratio) for s in self.trackingViews[realViewId]["originalSize"]
|
||||
]
|
||||
if hasattr(sView, "SetSize"):
|
||||
sView.SetSize(size)
|
||||
else:
|
||||
sView.ViewSize = size
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
@exportRpc("viewport.image.push.original.size")
|
||||
def setViewSize(self, viewId, width, height):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
observerInfo = None
|
||||
if realViewId in self.trackingViews:
|
||||
observerInfo = self.trackingViews[realViewId]
|
||||
|
||||
if not observerInfo:
|
||||
return {"error": "Unable to find subscription for view %s" % realViewId}
|
||||
|
||||
observerInfo["originalSize"] = [width, height]
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
@exportRpc("viewport.image.push.enabled")
|
||||
def enableView(self, viewId, enabled):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = str(self.getGlobalId(sView))
|
||||
observerInfo = None
|
||||
if realViewId in self.trackingViews:
|
||||
observerInfo = self.trackingViews[realViewId]
|
||||
|
||||
if not observerInfo:
|
||||
return {"error": "Unable to find subscription for view %s" % realViewId}
|
||||
|
||||
observerInfo["enabled"] = enabled
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
@exportRpc("viewport.image.push.invalidate.cache")
|
||||
def invalidateCache(self, viewId):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
self.getApplication().InvalidateCache(sView)
|
||||
self.getApplication().InvokeEvent("UpdateEvent")
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Provide Geometry delivery mechanism (WebGL) (deprecated - will be removed in VTK 10+)
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebViewPortGeometryDelivery(vtkWebProtocol):
|
||||
@exportRpc("viewport.webgl.metadata")
|
||||
def getSceneMetaData(self, view_id):
|
||||
view = self.getView(view_id)
|
||||
data = self.getApplication().GetWebGLSceneMetaData(view)
|
||||
return data
|
||||
|
||||
@exportRpc("viewport.webgl.data")
|
||||
def getWebGLData(self, view_id, object_id, part):
|
||||
view = self.getView(view_id)
|
||||
data = self.getApplication().GetWebGLBinaryData(view, str(object_id), part - 1)
|
||||
return data
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Provide File/Directory listing
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebFileBrowser(vtkWebProtocol):
|
||||
def __init__(
|
||||
self, basePath, name, excludeRegex=r"^\.|~$|^\$", groupRegex=r"[0-9]+\."
|
||||
):
|
||||
"""
|
||||
Configure the way the WebFile browser will expose the server content.
|
||||
- basePath: specify the base directory that we should start with
|
||||
- name: Name of that base directory that will show up on the web
|
||||
- excludeRegex: Regular expression of what should be excluded from the list of files/directories
|
||||
"""
|
||||
self.baseDirectory = basePath
|
||||
self.rootName = name
|
||||
self.pattern = re.compile(excludeRegex)
|
||||
self.gPattern = re.compile(groupRegex)
|
||||
|
||||
@exportRpc("file.server.directory.list")
|
||||
def listServerDirectory(self, relativeDir="."):
|
||||
"""
|
||||
RPC Callback to list a server directory relative to the basePath
|
||||
provided at start-up.
|
||||
"""
|
||||
path = [self.rootName]
|
||||
if len(relativeDir) > len(self.rootName):
|
||||
relativeDir = relativeDir[len(self.rootName) + 1 :]
|
||||
path += relativeDir.replace("\\", "/").split("/")
|
||||
|
||||
currentPath = os.path.join(self.baseDirectory, relativeDir)
|
||||
result = {
|
||||
"label": relativeDir,
|
||||
"files": [],
|
||||
"dirs": [],
|
||||
"groups": [],
|
||||
"path": path,
|
||||
}
|
||||
if relativeDir == ".":
|
||||
result["label"] = self.rootName
|
||||
for file in os.listdir(currentPath):
|
||||
if os.path.isfile(os.path.join(currentPath, file)) and not re.search(
|
||||
self.pattern, file
|
||||
):
|
||||
result["files"].append({"label": file, "size": -1})
|
||||
elif os.path.isdir(os.path.join(currentPath, file)) and not re.search(
|
||||
self.pattern, file
|
||||
):
|
||||
result["dirs"].append(file)
|
||||
|
||||
# Filter files to create groups
|
||||
files = result["files"]
|
||||
files.sort()
|
||||
groups = result["groups"]
|
||||
groupIdx = {}
|
||||
filesToRemove = []
|
||||
for file in files:
|
||||
fileSplit = re.split(self.gPattern, file["label"])
|
||||
if len(fileSplit) == 2:
|
||||
filesToRemove.append(file)
|
||||
gName = "*.".join(fileSplit)
|
||||
if gName in groupIdx:
|
||||
groupIdx[gName]["files"].append(file["label"])
|
||||
else:
|
||||
groupIdx[gName] = {"files": [file["label"]], "label": gName}
|
||||
groups.append(groupIdx[gName])
|
||||
for file in filesToRemove:
|
||||
gName = "*.".join(re.split(self.gPattern, file["label"]))
|
||||
if len(groupIdx[gName]["files"]) > 1:
|
||||
files.remove(file)
|
||||
else:
|
||||
groups.remove(groupIdx[gName])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Provide an updated geometry delivery mechanism which better matches the
|
||||
# client-side rendering capability we have in vtk.js
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class vtkWebLocalRendering(vtkWebProtocol):
|
||||
def __init__(self, **kwargs):
|
||||
super(vtkWebLocalRendering, self).__init__()
|
||||
initializeSerializers()
|
||||
self.context = SynchronizationContext()
|
||||
self.trackingViews = {}
|
||||
self.mtime = 0
|
||||
|
||||
# RpcName: getArray => viewport.geometry.array.get
|
||||
@exportRpc("viewport.geometry.array.get")
|
||||
def getArray(self, dataHash, binary=False):
|
||||
if binary:
|
||||
return self.addAttachment(self.context.getCachedDataArray(dataHash, binary))
|
||||
return self.context.getCachedDataArray(dataHash, binary)
|
||||
|
||||
# RpcName: addViewObserver => viewport.geometry.view.observer.add
|
||||
@exportRpc("viewport.geometry.view.observer.add")
|
||||
def addViewObserver(self, viewId):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
|
||||
|
||||
def pushGeometry(newSubscription=False):
|
||||
stateToReturn = self.getViewState(realViewId, newSubscription)
|
||||
stateToReturn["mtime"] = 0 if newSubscription else self.mtime
|
||||
self.mtime += 1
|
||||
return stateToReturn
|
||||
|
||||
if not realViewId in self.trackingViews:
|
||||
observerCallback = lambda *args, **kwargs: self.publish(
|
||||
"viewport.geometry.view.subscription", pushGeometry()
|
||||
)
|
||||
tag = self.getApplication().AddObserver("UpdateEvent", observerCallback)
|
||||
self.trackingViews[realViewId] = {"tags": [tag], "observerCount": 1}
|
||||
else:
|
||||
# There is an observer on this view already
|
||||
self.trackingViews[realViewId]["observerCount"] += 1
|
||||
|
||||
self.publish("viewport.geometry.view.subscription", pushGeometry(True))
|
||||
return {"success": True, "viewId": realViewId}
|
||||
|
||||
# RpcName: removeViewObserver => viewport.geometry.view.observer.remove
|
||||
@exportRpc("viewport.geometry.view.observer.remove")
|
||||
def removeViewObserver(self, viewId):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
realViewId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
|
||||
|
||||
observerInfo = None
|
||||
if realViewId in self.trackingViews:
|
||||
observerInfo = self.trackingViews[realViewId]
|
||||
|
||||
if not observerInfo:
|
||||
return {"error": "Unable to find subscription for view %s" % realViewId}
|
||||
|
||||
observerInfo["observerCount"] -= 1
|
||||
|
||||
if observerInfo["observerCount"] <= 0:
|
||||
for tag in observerInfo["tags"]:
|
||||
self.getApplication().RemoveObserver(tag)
|
||||
del self.trackingViews[realViewId]
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
# RpcName: getViewState => viewport.geometry.view.get.state
|
||||
@exportRpc("viewport.geometry.view.get.state")
|
||||
def getViewState(self, viewId, newSubscription=False):
|
||||
sView = self.getView(viewId)
|
||||
if not sView:
|
||||
return {"error": "Unable to get view with id %s" % viewId}
|
||||
|
||||
self.context.setIgnoreLastDependencies(newSubscription)
|
||||
|
||||
# Get the active view and render window, use it to iterate over renderers
|
||||
renderWindow = sView
|
||||
renderer = renderWindow.GetRenderers().GetFirstRenderer()
|
||||
camera = renderer.GetActiveCamera()
|
||||
renderWindowId = self.getApplication().GetObjectIdMap().GetGlobalId(sView)
|
||||
viewInstance = serializeInstance(
|
||||
None, renderWindow, renderWindowId, self.context, 1
|
||||
)
|
||||
viewInstance["extra"] = {
|
||||
"vtkRefId": getReferenceId(renderWindow),
|
||||
"centerOfRotation": camera.GetFocalPoint(),
|
||||
"camera": getReferenceId(camera),
|
||||
}
|
||||
|
||||
self.context.setIgnoreLastDependencies(False)
|
||||
self.context.checkForArraysToRelease()
|
||||
|
||||
if viewInstance:
|
||||
return viewInstance
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
Core Module for Web Base Data Generation
|
||||
"""
|
||||
|
||||
import sys, os, json
|
||||
|
||||
from vtkmodules.web import iteritems
|
||||
|
||||
|
||||
class DataHandler(object):
|
||||
def __init__(self, basePath):
|
||||
self.__root = basePath
|
||||
self.types = ["tonic-query-data-model"]
|
||||
self.metadata = {}
|
||||
self.data = {}
|
||||
self.arguments = {}
|
||||
self.current = {}
|
||||
self.sections = {}
|
||||
self.basePattern = None
|
||||
self.priority = []
|
||||
self.argOrder = []
|
||||
self.realValues = {}
|
||||
self.can_write = True
|
||||
|
||||
def getBasePath(self):
|
||||
return self.__root
|
||||
|
||||
def updateBasePattern(self):
|
||||
self.priority.sort(key=lambda item: item[1])
|
||||
self.basePattern = ""
|
||||
patternSeparator = ""
|
||||
currentPriority = -1
|
||||
|
||||
for item in self.priority:
|
||||
if currentPriority != -1:
|
||||
if currentPriority == item[1]:
|
||||
patternSeparator = "_"
|
||||
else:
|
||||
patternSeparator = "/"
|
||||
currentPriority = item[1]
|
||||
self.basePattern = "{%s}%s%s" % (
|
||||
item[0],
|
||||
patternSeparator,
|
||||
self.basePattern,
|
||||
)
|
||||
|
||||
def registerArgument(self, **kwargs):
|
||||
"""
|
||||
We expect the following set of arguments
|
||||
- priority
|
||||
- name
|
||||
- label (optional)
|
||||
- values
|
||||
- uiType
|
||||
- defaultIdx
|
||||
"""
|
||||
newArgument = {}
|
||||
argName = kwargs["name"]
|
||||
self.argOrder.append(argName)
|
||||
for key, value in iteritems(kwargs):
|
||||
if key == "priority":
|
||||
self.priority.append([argName, value])
|
||||
elif key == "values":
|
||||
self.realValues[argName] = value
|
||||
newArgument[key] = ["{value}".format(value=x) for x in value]
|
||||
else:
|
||||
newArgument[key] = value
|
||||
|
||||
self.arguments[argName] = newArgument
|
||||
|
||||
def updatePriority(self, argumentName, newPriority):
|
||||
for item in self.priority:
|
||||
if item[0] == argumentName:
|
||||
item[1] = newPriority
|
||||
|
||||
def setArguments(self, **kwargs):
|
||||
"""
|
||||
Update the arguments index
|
||||
"""
|
||||
for key, value in iteritems(kwargs):
|
||||
self.current[key] = value
|
||||
|
||||
def removeData(self, name):
|
||||
del self.data[name]
|
||||
|
||||
def registerData(self, **kwargs):
|
||||
"""
|
||||
name, type, mimeType, fileName, dependencies
|
||||
"""
|
||||
newData = {"metadata": {}}
|
||||
argName = kwargs["name"]
|
||||
for key, value in iteritems(kwargs):
|
||||
if key == "fileName":
|
||||
if "rootFile" in kwargs and kwargs["rootFile"]:
|
||||
newData["pattern"] = "{pattern}/%s" % value
|
||||
else:
|
||||
newData["pattern"] = "{pattern}%s" % value
|
||||
else:
|
||||
newData[key] = value
|
||||
|
||||
self.data[argName] = newData
|
||||
|
||||
def addDataMetaData(self, name, key, value):
|
||||
self.data[name]["metadata"][key] = value
|
||||
|
||||
def getDataAbsoluteFilePath(self, name, createDirectories=True):
|
||||
dataPattern = self.data[name]["pattern"]
|
||||
if "{pattern}" in dataPattern:
|
||||
if len(self.basePattern) == 0:
|
||||
dataPattern = dataPattern.replace(
|
||||
"{pattern}/", self.basePattern
|
||||
).replace("{pattern}", self.basePattern)
|
||||
self.data[name]["pattern"] = dataPattern
|
||||
else:
|
||||
dataPattern = dataPattern.replace("{pattern}", self.basePattern)
|
||||
self.data[name]["pattern"] = dataPattern
|
||||
|
||||
keyValuePair = {}
|
||||
for key, value in iteritems(self.current):
|
||||
keyValuePair[key] = self.arguments[key]["values"][value]
|
||||
|
||||
fullpath = os.path.join(self.__root, dataPattern.format(**keyValuePair))
|
||||
|
||||
if createDirectories and self.can_write:
|
||||
if not os.path.exists(os.path.dirname(fullpath)):
|
||||
os.makedirs(os.path.dirname(fullpath))
|
||||
|
||||
return fullpath
|
||||
|
||||
def addTypes(self, *args):
|
||||
for arg in args:
|
||||
self.types.append(arg)
|
||||
|
||||
def addMetaData(self, key, value):
|
||||
self.metadata[key] = value
|
||||
|
||||
def addSection(self, key, value):
|
||||
self.sections[key] = value
|
||||
|
||||
def computeDataPatterns(self):
|
||||
if self.basePattern == None:
|
||||
self.updateBasePattern()
|
||||
|
||||
for name in self.data:
|
||||
dataPattern = self.data[name]["pattern"]
|
||||
if "{pattern}" in dataPattern:
|
||||
dataPattern = dataPattern.replace("{pattern}", self.basePattern)
|
||||
self.data[name]["pattern"] = dataPattern
|
||||
|
||||
def __getattr__(self, name):
|
||||
if self.basePattern == None:
|
||||
self.updateBasePattern()
|
||||
|
||||
for i in range(len(self.arguments[name]["values"])):
|
||||
self.current[name] = i
|
||||
yield self.realValues[name][i]
|
||||
|
||||
def writeDataDescriptor(self):
|
||||
if not self.can_write:
|
||||
return
|
||||
|
||||
self.computeDataPatterns()
|
||||
|
||||
jsonData = {
|
||||
"arguments_order": self.argOrder,
|
||||
"type": self.types,
|
||||
"arguments": self.arguments,
|
||||
"metadata": self.metadata,
|
||||
"data": [],
|
||||
}
|
||||
|
||||
# Add sections
|
||||
for key, value in iteritems(self.sections):
|
||||
jsonData[key] = value
|
||||
|
||||
# Add data
|
||||
for key, value in iteritems(self.data):
|
||||
jsonData["data"].append(value)
|
||||
|
||||
filePathToWrite = os.path.join(self.__root, "index.json")
|
||||
with open(filePathToWrite, "w") as fileToWrite:
|
||||
fileToWrite.write(json.dumps(jsonData))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,788 @@
|
||||
r"""
|
||||
This module provides some testing functionality for paraview and
|
||||
vtk web applications. It provides the ability to run an arbitrary
|
||||
test script in a separate thread and communicate the results back
|
||||
to the service so that the CTest framework can be notified of the
|
||||
success or failure of the test.
|
||||
|
||||
This test harness will notice when the test script has finished
|
||||
running and will notify the service to stop. At this point, the
|
||||
test results will be checked in the main thread which ran the
|
||||
service, and in the case of failure an exception will be raised
|
||||
to notify CTest of the failure.
|
||||
|
||||
Test scripts need to follow some simple rules in order to work
|
||||
within the test harness framework:
|
||||
|
||||
1) implement a function called "runTest(args)", where the args
|
||||
parameter contains all the arguments given to the web application
|
||||
upon starting. Among other important items, args will contain the
|
||||
port number where the web application is listening.
|
||||
|
||||
2) import the testing module so that the script has access to
|
||||
the functions which indicate success and failure. Also the
|
||||
testing module contains convenience functions that might be of
|
||||
use to the test scripts.
|
||||
|
||||
from vtk.web import testing
|
||||
|
||||
3) Call the "testPass(testName)" or "testFail(testName)" functions
|
||||
from within the runTest(args) function to indicate to the framework
|
||||
whether the test passed or failed.
|
||||
|
||||
"""
|
||||
|
||||
import_warning_info = ""
|
||||
test_module_comm_queue = None
|
||||
|
||||
from vtkmodules.vtkTestingRendering import vtkTesting
|
||||
|
||||
# Try standard Python imports
|
||||
try:
|
||||
import os, re, time, datetime, threading, imp, inspect, Queue, types, io
|
||||
except:
|
||||
import_warning_info += "\nUnable to load at least one basic Python module"
|
||||
|
||||
# Image comparison imports
|
||||
try:
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
import Image
|
||||
except:
|
||||
raise
|
||||
import base64
|
||||
import itertools
|
||||
except:
|
||||
import_warning_info += (
|
||||
"\nUnable to load at least one modules necessary for image comparison"
|
||||
)
|
||||
|
||||
# Browser testing imports
|
||||
try:
|
||||
import selenium
|
||||
from selenium import webdriver
|
||||
except:
|
||||
import_warning_info += (
|
||||
"\nUnable to load at least one module necessary for browser tests"
|
||||
)
|
||||
|
||||
# HTTP imports
|
||||
try:
|
||||
import requests
|
||||
except:
|
||||
import_warning_info += (
|
||||
"\nUnable to load at least one module necessary for HTTP tests"
|
||||
)
|
||||
|
||||
|
||||
# Define some infrastructure to support different (or no) browsers
|
||||
test_module_browsers = ["firefox", "chrome", "internet_explorer", "safari", "nobrowser"]
|
||||
|
||||
|
||||
class TestModuleBrowsers:
|
||||
firefox, chrome, internet_explorer, safari, nobrowser = range(5)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# We can use this exception type to indicate that the test shouldn't actually
|
||||
# "fail", rather that it was unable to run because some dependencies were not
|
||||
# met.
|
||||
# =============================================================================
|
||||
class DependencyError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# This class allows usage as a dictionary and an object with named property
|
||||
# access.
|
||||
# =============================================================================
|
||||
class Dictionary(dict):
|
||||
def __getattribute__(self, attrName):
|
||||
return self[attrName]
|
||||
|
||||
def __setattr__(self, attrName, attrValue):
|
||||
self[attrName] = attrValue
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Checks whether test script supplied, if so, safely imports needed modules
|
||||
# =============================================================================
|
||||
def initialize(opts, reactor=None, cleanupMethod=None):
|
||||
"""
|
||||
This function should be called to initialize the testing module. The first
|
||||
important thing it does is to store the options for later, since the
|
||||
startTestThread function will need them. Then it checks the arguments that
|
||||
were passed into the server to see if a test was actually requested, making
|
||||
a note of this fact. Then, if a test was required, this function then
|
||||
checks if all the necessary testing modules were safely imported, printing
|
||||
a warning if not. If tests were requested and all modules were present,
|
||||
then this function sets "test_module_do_testing" to True and sets up the
|
||||
startTestThread function to be called after the reactor is running.
|
||||
|
||||
opts: Parsed arguments from the server
|
||||
|
||||
reactor: This argument is optional, but is used by server.py to
|
||||
cause the test thread to be started only after the server itself
|
||||
has started. If it is not provided, the test thread is launched
|
||||
immediately.
|
||||
|
||||
cleanupMethod: A callback method you would like the test thread
|
||||
to execute when the test has finished. This is used by server.py
|
||||
as a way to have the server terminated after the test has finished,
|
||||
but could be used for other cleanup purposes. This argument is
|
||||
also optional.
|
||||
"""
|
||||
|
||||
global import_warning_info
|
||||
|
||||
global testModuleOptions
|
||||
testModuleOptions = Dictionary()
|
||||
|
||||
# Copy the testing options into something we can easily extend
|
||||
for arg in vars(opts):
|
||||
optValue = getattr(opts, arg)
|
||||
testModuleOptions[arg] = optValue
|
||||
|
||||
# If we got one, add the cleanup method to the testing options
|
||||
if cleanupMethod:
|
||||
testModuleOptions["cleanupMethod"] = cleanupMethod
|
||||
|
||||
# Check if a test was actually requested
|
||||
if (
|
||||
testModuleOptions.testScriptPath != ""
|
||||
and testModuleOptions.testScriptPath is not None
|
||||
):
|
||||
# Check if we ran into trouble with any of the testing imports
|
||||
if import_warning_info != "":
|
||||
print("WARNING: Some tests may have unmet dependencies")
|
||||
print(import_warning_info)
|
||||
|
||||
if reactor is not None:
|
||||
# Add startTest callback to the reactor callback queue, so that
|
||||
# the test thread gets started after the reactor is running. Of
|
||||
# course this should only happen if everything is good for tests.
|
||||
reactor.callWhenRunning(_start_test_thread)
|
||||
else:
|
||||
# Otherwise, our aim is to start the thread from another process
|
||||
# so just call the start method.
|
||||
_start_test_thread()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Grab out the command-line arguments needed for by the testing module.
|
||||
# =============================================================================
|
||||
def add_arguments(parser):
|
||||
"""
|
||||
This function retrieves any command-line arguments that the client-side
|
||||
tester needs. In order to run a test, you will typically just need the
|
||||
following:
|
||||
|
||||
--run-test-script => This should be the full path to the test script to
|
||||
be run.
|
||||
|
||||
--baseline-img-dir => This should be the 'Baseline' directory where the
|
||||
baseline images for this test are located.
|
||||
|
||||
--test-use-browser => This should be one of the supported browser types,
|
||||
or else 'nobrowser'. The choices are 'chrome', 'firefox', 'internet_explorer',
|
||||
'safari', or 'nobrowser'.
|
||||
"""
|
||||
|
||||
parser.add_argument(
|
||||
"--run-test-script",
|
||||
default="",
|
||||
help="The path to a test script to run",
|
||||
dest="testScriptPath",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--baseline-img-dir",
|
||||
default="",
|
||||
help="The path to the directory containing the web test baseline images",
|
||||
dest="baselineImgDir",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--test-use-browser",
|
||||
default="nobrowser",
|
||||
help="One of 'chrome', 'firefox', 'internet_explorer', 'safari', or 'nobrowser'.",
|
||||
dest="useBrowser",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--temporary-directory",
|
||||
default=".",
|
||||
help="A temporary directory for storing test images and diffs",
|
||||
dest="tmpDirectory",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--test-image-file-name",
|
||||
default="",
|
||||
help="Name of file in which to store generated test image",
|
||||
dest="testImgFile",
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Initialize the test client
|
||||
# =============================================================================
|
||||
def _start_test_thread():
|
||||
"""
|
||||
This function checks whether testing is required and if so, sets up a Queue
|
||||
for the purpose of communicating with the thread. then it starts the
|
||||
after waiting 5 seconds for the server to have a chance to start up.
|
||||
"""
|
||||
|
||||
global test_module_comm_queue
|
||||
test_module_comm_queue = Queue.Queue()
|
||||
|
||||
t = threading.Thread(
|
||||
target=launch_web_test,
|
||||
args=[],
|
||||
kwargs={
|
||||
"serverOpts": testModuleOptions,
|
||||
"commQueue": test_module_comm_queue,
|
||||
"testScript": testModuleOptions.testScriptPath,
|
||||
},
|
||||
)
|
||||
|
||||
t.start()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test scripts call this function to indicate passage of their test
|
||||
# =============================================================================
|
||||
def test_pass(testName):
|
||||
"""
|
||||
Test scripts should call this function to indicate that the test passed. A
|
||||
note is recorded that the test succeeded, and is checked later on from the
|
||||
main thread so that CTest can be notified of this result.
|
||||
"""
|
||||
|
||||
global test_module_comm_queue
|
||||
resultObj = {testName: "pass"}
|
||||
test_module_comm_queue.put(resultObj)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test scripts call this function to indicate failure of their test
|
||||
# =============================================================================
|
||||
def test_fail(testName):
|
||||
"""
|
||||
Test scripts should call this function to indicate that the test failed. A
|
||||
note is recorded that the test did not succeed, and this note is checked
|
||||
later from the main thread so that CTest can be notified of the result.
|
||||
|
||||
The main thread is the only one that can signal test failure in
|
||||
CTest framework, and the main thread won't have a chance to check for
|
||||
passage or failure of the test until the main loop has terminated. So
|
||||
here we just record the failure result, then we check this result in the
|
||||
processTestResults() function, throwing an exception at that point to
|
||||
indicate to CTest that the test failed.
|
||||
"""
|
||||
|
||||
global test_module_comm_queue
|
||||
resultObj = {testName: "fail"}
|
||||
test_module_comm_queue.put(resultObj)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Concatenate any number of strings into a single path string.
|
||||
# =============================================================================
|
||||
def concat_paths(*pathElts):
|
||||
"""
|
||||
A very simple convenience function so that test scripts can build platform
|
||||
independent paths out of a list of elements, without having to import the
|
||||
os module.
|
||||
|
||||
pathElts: Any number of strings which should be concatenated together
|
||||
in a platform independent manner.
|
||||
"""
|
||||
|
||||
return os.path.join(*pathElts)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# So we can change our time format in a single place, this function is
|
||||
# provided.
|
||||
# =============================================================================
|
||||
def get_current_time_string():
|
||||
"""
|
||||
This function returns the current time as a string, using ISO 8601 format.
|
||||
"""
|
||||
|
||||
return datetime.datetime.now().isoformat(" ")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Uses vtkTesting to compare images. According to comments in the vtkTesting
|
||||
# C++ code (and this seems to work), if there are multiple baseline images in
|
||||
# the same directory as the baseline_img, and they follow the naming pattern:
|
||||
# 'img.png', 'img_1.png', ... , 'img_N.png', then all of these images will be
|
||||
# tried for a match.
|
||||
# =============================================================================
|
||||
def compare_images(test_img, baseline_img, tmp_dir="."):
|
||||
"""
|
||||
This function creates a vtkTesting object, and specifies the name of the
|
||||
baseline image file, using a fully qualified path (baseline_img must be
|
||||
fully qualified). Then it calls the vtkTesting method which compares the
|
||||
image (test_img, specified only with a relative path) against the baseline
|
||||
image as well as any other images in the same directory as the baseline
|
||||
image which follow the naming pattern: 'img.png', 'img_1.png', ... , 'img_N.png'
|
||||
|
||||
test_img: File name of output image to be compared against baseline.
|
||||
|
||||
baseline_img: Fully qualified path to first of the baseline images.
|
||||
|
||||
tmp_dir: Fully qualified path to a temporary directory for storing images.
|
||||
"""
|
||||
|
||||
# Create a vtkTesting object and specify a baseline image
|
||||
t = vtkTesting()
|
||||
t.AddArgument("-T")
|
||||
t.AddArgument(tmp_dir)
|
||||
t.AddArgument("-V")
|
||||
t.AddArgument(baseline_img)
|
||||
|
||||
# Perform the image comparison test and print out the result.
|
||||
return t.RegressionTest(test_img, 0.05)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Provide a wait function
|
||||
# =============================================================================
|
||||
def wait_with_timeout(delay=None, limit=0, criterion=None):
|
||||
"""
|
||||
This function provides the ability to wait for a certain number of seconds,
|
||||
or else to wait for a specific criterion to be met.
|
||||
"""
|
||||
for i in itertools.count():
|
||||
if criterion is not None and criterion():
|
||||
return True
|
||||
elif delay * i > limit:
|
||||
return False
|
||||
else:
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Define a WebTest class with five stages of testing: initialization, setup,
|
||||
# capture, postprocess, and cleanup.
|
||||
# =============================================================================
|
||||
class WebTest(object):
|
||||
"""
|
||||
This is the base class for all automated web-based tests. It defines five
|
||||
stages that any test must run through, and allows any or all of these
|
||||
stages to be overridden by subclasses. This class defines the run_test
|
||||
method to invoke the five stages overridden by subclasses, one at a time:
|
||||
1) initialize, 2) setup, 3) capture, 4) postprocess, and 5) cleanup.
|
||||
"""
|
||||
|
||||
class Abort:
|
||||
pass
|
||||
|
||||
def __init__(self, url=None, testname=None, **kwargs):
|
||||
self.url = url
|
||||
self.testname = testname
|
||||
|
||||
def run_test(self):
|
||||
try:
|
||||
self.checkdependencies()
|
||||
self.initialize()
|
||||
self.setup()
|
||||
self.capture()
|
||||
self.postprocess()
|
||||
except WebTest.Abort:
|
||||
# Placeholder for future option to return failure result
|
||||
pass
|
||||
except:
|
||||
self.cleanup()
|
||||
raise
|
||||
|
||||
self.cleanup()
|
||||
|
||||
def checkdependencies(self):
|
||||
pass
|
||||
|
||||
def initialize(self):
|
||||
pass
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def capture(self):
|
||||
pass
|
||||
|
||||
def postprocess(self):
|
||||
pass
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Define a WebTest subclass designed specifically for browser-based tests.
|
||||
# =============================================================================
|
||||
class BrowserBasedWebTest(WebTest):
|
||||
"""
|
||||
This class can be used as a base for any browser-based web tests. It
|
||||
introduces the notion of a selenium browser and overrides phases (1) and
|
||||
(3), initialization and cleanup, of the test phases introduced in the base
|
||||
class. Initialization involves selecting the browser type, setting the
|
||||
browser window size, and asking the browser to load the url. Cleanup
|
||||
involves closing the browser window.
|
||||
"""
|
||||
|
||||
def __init__(self, size=None, browser=None, **kwargs):
|
||||
self.size = size
|
||||
self.browser = browser
|
||||
self.window = None
|
||||
|
||||
WebTest.__init__(self, **kwargs)
|
||||
|
||||
def initialize(self):
|
||||
try:
|
||||
if self.browser is None or self.browser == TestModuleBrowsers.chrome:
|
||||
self.window = webdriver.Chrome()
|
||||
elif self.browser == TestModuleBrowsers.firefox:
|
||||
self.window = webdriver.Firefox()
|
||||
elif self.browser == TestModuleBrowsers.internet_explorer:
|
||||
self.window = webdriver.Ie()
|
||||
else:
|
||||
raise DependencyError(
|
||||
"self.browser argument has illegal value %r" % (self.browser)
|
||||
)
|
||||
except DependencyError as dErr:
|
||||
raise
|
||||
except Exception as inst:
|
||||
raise DependencyError(inst)
|
||||
|
||||
if self.size is not None:
|
||||
self.window.set_window_size(self.size[0], self.size[1])
|
||||
|
||||
if self.url is not None:
|
||||
self.window.get(self.url)
|
||||
|
||||
def cleanup(self):
|
||||
try:
|
||||
self.window.quit()
|
||||
except:
|
||||
print(
|
||||
"Unable to call window.quit, perhaps this is expected because of unmet browser dependency."
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Extend BrowserBasedWebTest to handle vtk-style image comparison
|
||||
# =============================================================================
|
||||
class ImageComparatorWebTest(BrowserBasedWebTest):
|
||||
"""
|
||||
This class extends browser based web tests to include image comparison. It
|
||||
overrides the capture phase of testing with some functionality to simply
|
||||
grab a screenshot of the entire browser window. It overrides the
|
||||
postprocess phase with a call to vtk image comparison functionality.
|
||||
Derived classes can then simply override the setup function with a series
|
||||
of selenium-based browser interactions to create a complete test. Derived
|
||||
classes may also prefer to override the capture phase to capture only
|
||||
certain portions of the browser window for image comparison.
|
||||
"""
|
||||
|
||||
def __init__(self, filename=None, baseline=None, temporaryDir=None, **kwargs):
|
||||
if filename is None:
|
||||
raise TypeError("missing argument 'filename'")
|
||||
if baseline is None:
|
||||
raise TypeError("missing argument 'baseline'")
|
||||
|
||||
BrowserBasedWebTest.__init__(self, **kwargs)
|
||||
self.filename = filename
|
||||
self.baseline = baseline
|
||||
self.tmpDir = temporaryDir
|
||||
|
||||
def capture(self):
|
||||
self.window.save_screenshot(self.filename)
|
||||
|
||||
def postprocess(self):
|
||||
result = compare_images(self.filename, self.baseline, self.tmpDir)
|
||||
|
||||
if result == 1:
|
||||
test_pass(self.testname)
|
||||
else:
|
||||
test_fail(self.testname)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Given a css selector to use in finding the image element, get the element,
|
||||
# then base64 decode the "src" attribute and return it.
|
||||
# =============================================================================
|
||||
def get_image_data(browser, cssSelector):
|
||||
"""
|
||||
This function takes a selenium browser and a css selector string and uses
|
||||
them to find the target HTML image element. The desired image element
|
||||
should contain it's image data as a Base64 encoded JPEG image string.
|
||||
The 'src' attribute of the image is read, Base64-decoded, and then
|
||||
returned.
|
||||
|
||||
browser: A selenium browser instance, as created by webdriver.Chrome(),
|
||||
for example.
|
||||
|
||||
cssSelector: A string containing a CSS selector which will be used to
|
||||
find the HTML image element of interest.
|
||||
"""
|
||||
|
||||
# Here's maybe a better way to get at that image element
|
||||
imageElt = browser.find_element_by_css_selector(cssSelector)
|
||||
|
||||
# Now get the Base64 image string and decode it into image data
|
||||
base64String = imageElt.get_attribute("src")
|
||||
b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
|
||||
b64Matcher = b64RegEx.match(base64String)
|
||||
imgdata = base64.b64decode(b64Matcher.group(1))
|
||||
|
||||
return imgdata
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Combines a variation on above function with the write_image_to_disk function.
|
||||
# converting jpg to png in the process, if necessary.
|
||||
# =============================================================================
|
||||
def save_image_data_as_png(browser, cssSelector, imgfilename):
|
||||
"""
|
||||
This function takes a selenium browser instance, a css selector string,
|
||||
and a file name. It uses the css selector string to finds the target HTML
|
||||
Image element, which should contain a Base64 encoded JPEG image string,
|
||||
it decodes the string to image data, and then saves the data to the file.
|
||||
The image type of the written file is determined from the extension of the
|
||||
provided filename.
|
||||
|
||||
browser: A selenium browser instance as created by webdriver.Chrome(),
|
||||
for example.
|
||||
|
||||
cssSelector: A string containing a CSS selector which will be used to
|
||||
find the HTML image element of interest.
|
||||
|
||||
imgFilename: The filename to which to save the image. The extension is
|
||||
used to determine the type of image which should be saved.
|
||||
"""
|
||||
imageElt = browser.find_element_by_css_selector(cssSelector)
|
||||
base64String = imageElt.get_attribute("src")
|
||||
b64RegEx = re.compile(r"data:image/jpeg;base64,(.+)")
|
||||
b64Matcher = b64RegEx.match(base64String)
|
||||
img = Image.open(io.BytesIO(base64.b64decode(b64Matcher.group(1))))
|
||||
img.save(imgfilename)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Given a decoded image and the full path to a file, write the image to the
|
||||
# file.
|
||||
# =============================================================================
|
||||
def write_image_to_disk(imgData, filePath):
|
||||
"""
|
||||
This function takes an image data, as returned by this module's
|
||||
get_image_data() function for example, and writes it out to the file given by
|
||||
the filePath parameter.
|
||||
|
||||
imgData: An image data object
|
||||
filePath: The full path, including the file name and extension, where
|
||||
the image should be written.
|
||||
"""
|
||||
|
||||
with open(filePath, "wb") as f:
|
||||
f.write(imgData)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# There could be problems if the script file has more than one class defn which
|
||||
# is a subclass of vtk.web.testing.WebTest, so we should write some
|
||||
# documentation to help people avoid that.
|
||||
# =============================================================================
|
||||
def instantiate_test_subclass(pathToScript, **kwargs):
|
||||
"""
|
||||
This function takes the fully qualified path to a test file, along with
|
||||
any needed keyword arguments, then dynamically loads the file as a module
|
||||
and finds the test class defined inside of it via inspection. It then
|
||||
uses the keyword arguments to instantiate the test class and return the
|
||||
instance.
|
||||
|
||||
pathToScript: Fully qualified path to python file containing defined
|
||||
subclass of one of the test base classes.
|
||||
kwargs: Keyword arguments to be passed to the constructor of the
|
||||
testing subclass.
|
||||
"""
|
||||
|
||||
# Load the file as a module
|
||||
moduleName = imp.load_source("dynamicTestModule", pathToScript)
|
||||
instance = None
|
||||
|
||||
# Inspect dynamically loaded module members
|
||||
for name, obj in inspect.getmembers(moduleName):
|
||||
# Looking for classes only
|
||||
if inspect.isclass(obj):
|
||||
instance = obj.__new__(obj)
|
||||
# And only classes defined in the dynamically loaded module
|
||||
if instance.__module__ == "dynamicTestModule":
|
||||
try:
|
||||
instance.__init__(**kwargs)
|
||||
break
|
||||
except Exception as inst:
|
||||
print("Caught exception: " + str(type(inst)))
|
||||
print(inst)
|
||||
raise
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# For testing purposes, define a function which can interact with a running
|
||||
# paraview or vtk web application service.
|
||||
# =============================================================================
|
||||
def launch_web_test(*args, **kwargs):
|
||||
"""
|
||||
This function loads a python file as a module (with no package), and then
|
||||
instantiates the class it must contain, and finally executes the run_test()
|
||||
method of the class (which the class may override, but which is defined in
|
||||
both of the testing base classes, WebTest and ImageComparatorBaseClass).
|
||||
After the run_test() method finishes, this function will stop the web
|
||||
server if required. This function expects some keyword arguments will be
|
||||
present in order for it to complete it's task:
|
||||
|
||||
kwargs['serverOpts']: An object containing all the parameters used
|
||||
to start the web service. Some of them will be used in the test script
|
||||
in order perform the test. For example, the port on which the server
|
||||
was started will be required in order to connect to the server.
|
||||
|
||||
kwargs['testScript']: The full path to the python file containing the
|
||||
testing subclass.
|
||||
"""
|
||||
|
||||
serverOpts = None
|
||||
testScriptFile = None
|
||||
|
||||
# This is really the thing all test scripts will need: access to all
|
||||
# the options used to start the server process.
|
||||
if "serverOpts" in kwargs:
|
||||
serverOpts = kwargs["serverOpts"]
|
||||
# print 'These are the serverOpts we got: '
|
||||
# print serverOpts
|
||||
|
||||
# Get the full path to the test script
|
||||
if "testScript" in kwargs:
|
||||
testScriptFile = kwargs["testScript"]
|
||||
|
||||
testName = "unknown"
|
||||
|
||||
# Check for a test file (python file)
|
||||
if testScriptFile is None:
|
||||
print("No test script file found, no test script will be run.")
|
||||
test_fail(testName)
|
||||
|
||||
# The test name will be generated from the python script name, so
|
||||
# match and capture a bunch of contiguous characters which are
|
||||
# not '.', '\', or '/', followed immediately by the string '.py'.
|
||||
fnamePattern = re.compile("([^\.\/\\\]+)\.py")
|
||||
fmatch = re.search(fnamePattern, testScriptFile)
|
||||
if fmatch:
|
||||
testName = fmatch.group(1)
|
||||
else:
|
||||
print(
|
||||
"Unable to parse testScriptFile ("
|
||||
+ str(testScriptfile)
|
||||
+ "), no test will be run"
|
||||
)
|
||||
test_fail(testName)
|
||||
|
||||
# If we successfully got a test name, we are ready to try and run the test
|
||||
if testName != "unknown":
|
||||
|
||||
# Output file and baseline file names are generated from the test name
|
||||
imgFileName = testName + ".png"
|
||||
knownGoodFileName = concat_paths(serverOpts.baselineImgDir, imgFileName)
|
||||
tempDir = serverOpts.tmpDirectory
|
||||
testImgFileName = serverOpts.testImgFile
|
||||
|
||||
testBrowser = test_module_browsers.index(serverOpts.useBrowser)
|
||||
|
||||
# Now try to instantiate and run the test
|
||||
try:
|
||||
testInstance = instantiate_test_subclass(
|
||||
testScriptFile,
|
||||
testname=testName,
|
||||
host=serverOpts.host,
|
||||
port=serverOpts.port,
|
||||
browser=testBrowser,
|
||||
filename=testImgFileName,
|
||||
baseline=knownGoodFileName,
|
||||
temporaryDir=tempDir,
|
||||
)
|
||||
|
||||
# If we were able to instantiate the test, run it, otherwise we
|
||||
# consider it a failure.
|
||||
if testInstance is not None:
|
||||
try:
|
||||
testInstance.run_test()
|
||||
except DependencyError as derr:
|
||||
# TODO: trigger return SKIP_RETURN_CODE when CMake 3 is required
|
||||
print(
|
||||
"Some dependency of this test was not met, allowing it to pass"
|
||||
)
|
||||
test_pass(testName)
|
||||
else:
|
||||
print("Unable to instantiate test instance, failing test")
|
||||
test_fail(testName)
|
||||
return
|
||||
|
||||
except Exception as inst:
|
||||
import sys, traceback
|
||||
|
||||
tb = sys.exc_info()[2]
|
||||
print("Caught an exception while running test script:")
|
||||
print(" " + str(type(inst)))
|
||||
print(" " + str(inst))
|
||||
print(" " + "".join(traceback.format_tb(tb)))
|
||||
test_fail(testName)
|
||||
|
||||
# If we were passed a cleanup method to run after testing, invoke it now
|
||||
if "cleanupMethod" in serverOpts:
|
||||
serverOpts["cleanupMethod"]()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# To keep the service module clean, we'll process the test results here, given
|
||||
# the test result object we generated in "launch_web_test". It is
|
||||
# passed back to this function after the service has completed. Failure of
|
||||
# of the test is indicated by raising an exception in here.
|
||||
# =============================================================================
|
||||
def finalize():
|
||||
"""
|
||||
This function checks the module's global test_module_comm_queue variable for a
|
||||
test result. If one is found and the result is 'fail', then this function
|
||||
raises an exception to communicate the failure to the CTest framework.
|
||||
|
||||
In order for a test result to be found in the test_module_comm_queue variable,
|
||||
the test script must have called either the testPass or testFail functions
|
||||
provided by this test module before returning.
|
||||
"""
|
||||
|
||||
global test_module_comm_queue
|
||||
|
||||
if test_module_comm_queue is not None:
|
||||
resultObject = test_module_comm_queue.get()
|
||||
|
||||
failedATest = False
|
||||
|
||||
for testName in resultObject:
|
||||
testResult = resultObject[testName]
|
||||
if testResult == "fail":
|
||||
print(" Test -> " + testName + ": " + testResult)
|
||||
failedATest = True
|
||||
|
||||
if failedATest is True:
|
||||
raise Exception(
|
||||
"At least one of the requested tests failed. "
|
||||
+ "See detailed output, above, for more information"
|
||||
)
|
||||
@@ -0,0 +1,211 @@
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"This module depends on the numpy module. Please make\
|
||||
sure that it is installed properly."
|
||||
)
|
||||
|
||||
import base64
|
||||
|
||||
from vtkmodules.util.numpy_support import vtk_to_numpy
|
||||
from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter
|
||||
|
||||
# Numpy to JS TypedArray
|
||||
to_js_type = {
|
||||
"int8": "Int8Array",
|
||||
"uint8": "Uint8Array",
|
||||
"int16": "Int16Array",
|
||||
"uint16": "Uint16Array",
|
||||
"int32": "Int32Array",
|
||||
"uint32": "Uint32Array",
|
||||
"int64": "Int32Array",
|
||||
"uint64": "Uint32Array",
|
||||
"float32": "Float32Array",
|
||||
"float64": "Float64Array",
|
||||
}
|
||||
|
||||
|
||||
def b64_encode_numpy(obj):
|
||||
# Convert 1D numpy arrays with numeric types to memoryviews with
|
||||
# datatype and shape metadata.
|
||||
if len(obj) == 0:
|
||||
return obj.tolist()
|
||||
|
||||
dtype = obj.dtype
|
||||
if dtype.kind == "f":
|
||||
return np_encode(obj)
|
||||
elif dtype.kind == "b":
|
||||
return np_encode(obj, np.uint8)
|
||||
elif dtype.kind in ["u", "i"]:
|
||||
# Try to see if we can downsize the array
|
||||
max_value = np.amax(obj)
|
||||
min_value = np.amin(obj)
|
||||
signed = min_value < 0
|
||||
test_value = max(max_value, -min_value)
|
||||
if signed:
|
||||
if test_value < np.iinfo(np.int8):
|
||||
return np_encode(obj, np.int8)
|
||||
if test_value < np.iinfo(np.int16).max:
|
||||
return np_encode(obj, np.int16)
|
||||
if test_value < np.iinfo(np.int32).max:
|
||||
return np_encode(obj, np.int32)
|
||||
else:
|
||||
if test_value < np.iinfo(np.uint8).max:
|
||||
return np_encode(obj, np.uint8)
|
||||
if test_value < np.iinfo(np.uint16).max:
|
||||
return np_encode(obj, np.uint16)
|
||||
if test_value < np.iinfo(np.uint32).max:
|
||||
return np_encode(obj, np.uint32)
|
||||
|
||||
# Convert all other numpy arrays to lists
|
||||
return obj.tolist()
|
||||
|
||||
|
||||
def np_encode(array, np_type=None):
|
||||
if np_type:
|
||||
n_array = array.astype(np_type).ravel(order="C")
|
||||
return {
|
||||
"bvals": base64.b64encode(memoryview(n_array)).decode("utf-8"),
|
||||
"dtype": str(n_array.dtype),
|
||||
"shape": list(array.shape),
|
||||
}
|
||||
return {
|
||||
"bvals": base64.b64encode(memoryview(array.ravel(order="C"))).decode("utf-8"),
|
||||
"dtype": str(array.dtype),
|
||||
"shape": list(array.shape),
|
||||
}
|
||||
|
||||
|
||||
def mesh_array(array):
|
||||
if array:
|
||||
return b64_encode_numpy(vtk_to_numpy(array.GetData()))
|
||||
|
||||
|
||||
def data_array(data_array, location="PointData", name=None):
|
||||
if data_array:
|
||||
dataRange = data_array.GetRange(-1)
|
||||
nb_comp = data_array.GetNumberOfComponents()
|
||||
values = vtk_to_numpy(data_array)
|
||||
js_types = to_js_type[str(values.dtype)]
|
||||
return {
|
||||
"name": name if name else data_array.GetName(),
|
||||
"values": b64_encode_numpy(values),
|
||||
"numberOfComponents": nb_comp,
|
||||
"type": js_types,
|
||||
"location": location,
|
||||
"dataRange": dataRange,
|
||||
}
|
||||
|
||||
|
||||
def field_data(field_data, names, location="PointData"):
|
||||
fields = []
|
||||
for name in names:
|
||||
array = field_data.GetArray(name)
|
||||
js_array = data_array(array, location, name)
|
||||
if js_array:
|
||||
fields.append(js_array)
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def mesh(dataset, field_to_keep=None, point_arrays=None, cell_arrays=None):
|
||||
"""Expect any dataset and extract its surface into a dash_vtk.Mesh state property"""
|
||||
if dataset is None:
|
||||
return None
|
||||
|
||||
# Make sure we have a polydata to export
|
||||
polydata = None
|
||||
if dataset.IsA("vtkPolyData"):
|
||||
polydata = dataset
|
||||
else:
|
||||
extractSkinFilter = vtkDataSetSurfaceFilter()
|
||||
extractSkinFilter.SetInputData(dataset)
|
||||
extractSkinFilter.Update()
|
||||
polydata = extractSkinFilter.GetOutput()
|
||||
|
||||
if polydata.GetPoints() is None:
|
||||
return None
|
||||
|
||||
# Extract mesh
|
||||
state = {"mesh": {}}
|
||||
|
||||
points = mesh_array(polydata.GetPoints())
|
||||
if points:
|
||||
state["mesh"]["points"] = points
|
||||
|
||||
verts = mesh_array(polydata.GetVerts())
|
||||
if verts:
|
||||
state["mesh"]["verts"] = verts
|
||||
|
||||
lines = mesh_array(polydata.GetLines())
|
||||
if lines:
|
||||
state["mesh"]["lines"] = lines
|
||||
|
||||
polys = mesh_array(polydata.GetPolys())
|
||||
if polys:
|
||||
state["mesh"]["polys"] = polys
|
||||
|
||||
strips = mesh_array(polydata.GetStrips())
|
||||
if strips:
|
||||
state["mesh"]["strips"] = strips
|
||||
|
||||
# Scalars
|
||||
if field_to_keep is not None:
|
||||
field = None
|
||||
p_array = polydata.GetPointData().GetArray(field_to_keep)
|
||||
c_array = polydata.GetCellData().GetArray(field_to_keep)
|
||||
|
||||
if c_array:
|
||||
field = data_array(c_array, location="CellData", name=field_to_keep)
|
||||
|
||||
if p_array:
|
||||
field = data_array(p_array, location="PointData", name=field_to_keep)
|
||||
|
||||
if field:
|
||||
state.update({"field": field})
|
||||
|
||||
# PointData Fields
|
||||
if point_arrays:
|
||||
point_data = field_data(polydata.GetPointData(), point_arrays, "PointData")
|
||||
if len(point_data):
|
||||
state.update({"pointArrays": point_data})
|
||||
|
||||
# CellData Fields
|
||||
if cell_arrays:
|
||||
cell_data = field_data(polydata.GetCellData(), cell_arrays, "CellData")
|
||||
if len(cell_data):
|
||||
state.update({"cellArrays": cell_data})
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def volume(dataset):
|
||||
"""Expect a vtkImageData and extract its setting for the dash_vtk.Volume state"""
|
||||
if dataset is None or not dataset.IsA("vtkImageData"):
|
||||
return None
|
||||
|
||||
state = {
|
||||
"image": {
|
||||
"dimensions": dataset.GetDimensions(),
|
||||
"spacing": dataset.GetSpacing(),
|
||||
"origin": dataset.GetOrigin(),
|
||||
},
|
||||
}
|
||||
|
||||
# Capture image orientation if any
|
||||
if hasattr(dataset, "GetDirectionMatrix"):
|
||||
matrix = dataset.GetDirectionMatrix()
|
||||
js_mat = []
|
||||
for j in range(3):
|
||||
for i in range(3):
|
||||
js_mat.append(matrix.GetElement(i, j))
|
||||
|
||||
state["image"]["direction"] = js_mat
|
||||
|
||||
scalars = dataset.GetPointData().GetScalars()
|
||||
field = data_array(scalars, location="PointData")
|
||||
if field:
|
||||
state["field"] = field
|
||||
|
||||
return state
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Activate venv for current interpreter:
|
||||
|
||||
Use `from vtk.web import venv` along one of the following
|
||||
- `--venv /path/to/venv/base` argument
|
||||
- environment variable `VTK_VENV=/path/to/venv/base`
|
||||
|
||||
This can be used when you must use an existing Python interpreter, not the venv bin/python.
|
||||
"""
|
||||
import os
|
||||
import site
|
||||
import sys
|
||||
|
||||
VENV_BASE = None
|
||||
VENV_LOADED = False
|
||||
|
||||
if "--venv" in sys.argv:
|
||||
VENV_BASE = os.path.abspath(sys.argv[sys.argv.index("--venv") + 1])
|
||||
|
||||
if os.environ.get("VTK_VENV"):
|
||||
VENV_BASE = os.path.abspath(os.environ.get("VTK_VENV"))
|
||||
|
||||
if not VENV_LOADED and VENV_BASE and os.path.exists(VENV_BASE):
|
||||
VENV_LOADED = True
|
||||
# Code inspired by virutal-env::bin/active_this.py
|
||||
bin_dir = os.path.join(VENV_BASE, "bin")
|
||||
os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
|
||||
os.environ["VIRTUAL_ENV"] = VENV_BASE
|
||||
prev_length = len(sys.path)
|
||||
python_libs = os.path.join(VENV_BASE, f"lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages")
|
||||
site.addsitedir(python_libs)
|
||||
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
||||
sys.real_prefix = sys.prefix
|
||||
sys.prefix = VENV_BASE
|
||||
#
|
||||
print(f"VTK is using venv: {VENV_BASE}")
|
||||
@@ -0,0 +1,283 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
import zlib
|
||||
|
||||
compression = zipfile.ZIP_DEFLATED
|
||||
except:
|
||||
compression = zipfile.ZIP_STORED
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def convertDirectoryToZipFile(directoryPath):
|
||||
if os.path.isfile(directoryPath):
|
||||
return
|
||||
|
||||
zipFilePath = "%s.zip" % directoryPath
|
||||
zf = zipfile.ZipFile(zipFilePath, mode="w")
|
||||
|
||||
try:
|
||||
for dirName, subdirList, fileList in os.walk(directoryPath):
|
||||
for fname in fileList:
|
||||
fullPath = os.path.join(dirName, fname)
|
||||
relPath = "%s" % (os.path.relpath(fullPath, directoryPath))
|
||||
zf.write(fullPath, arcname=relPath, compress_type=compression)
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
shutil.rmtree(directoryPath)
|
||||
shutil.move(zipFilePath, directoryPath)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def addDataToViewer(dataPath, srcHtmlPath, disableGirder=False):
|
||||
if os.path.isfile(dataPath) and os.path.exists(srcHtmlPath):
|
||||
dstDir = os.path.dirname(dataPath)
|
||||
dstHtmlPath = os.path.join(dstDir, "%s.html" % os.path.basename(dataPath)[:-6])
|
||||
|
||||
# Extract data as base64
|
||||
with open(dataPath, "rb") as data:
|
||||
dataContent = data.read()
|
||||
base64Content = base64.b64encode(dataContent)
|
||||
base64Content = base64Content.decode().replace("\n", "")
|
||||
|
||||
# Create new output file
|
||||
with open(srcHtmlPath, mode="r", encoding="utf-8") as srcHtml:
|
||||
with open(dstHtmlPath, mode="w", encoding="utf-8") as dstHtml:
|
||||
for line in srcHtml:
|
||||
if disableGirder and "</title>" in line:
|
||||
dstHtml.write(
|
||||
"""
|
||||
<script>
|
||||
// Force reloading the page if we want to disable girder before anything else.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('noGirder') != 'true') {
|
||||
urlParams.set('noGirder', 'true');
|
||||
window.location.search = urlParams;
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
if "</body>" in line:
|
||||
dstHtml.write("<script>\n")
|
||||
dstHtml.write('var contentToLoad = "%s";\n\n' % base64Content)
|
||||
dstHtml.write(
|
||||
'Glance.importBase64Dataset("%s" , contentToLoad, glanceInstance.proxyManager);\n'
|
||||
% os.path.basename(dataPath)
|
||||
)
|
||||
dstHtml.write("glanceInstance.showApp();\n")
|
||||
dstHtml.write("</script>\n")
|
||||
|
||||
dstHtml.write(line)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def numericSorted(l):
|
||||
"""Numerically sort a list of strings."""
|
||||
|
||||
# pattern to split name into numeric and non-numeric parts
|
||||
splitter_pattern = re.compile('([0-9]+|[^0-9]+)')
|
||||
|
||||
def keyfunc(name):
|
||||
"""Sorting key for numeric sorting."""
|
||||
split_name = re.findall(splitter_pattern, name)
|
||||
# one-liner to convert numeric parts into integers
|
||||
split_name = list(map(lambda x: int(x) if x.isdigit() else x, split_name))
|
||||
# ensure that list begins with a string to avoid string<->int compare
|
||||
if split_name and isinstance(split_name[0], int):
|
||||
split_name.insert(0, '')
|
||||
return split_name
|
||||
|
||||
# return the numerically sorted list
|
||||
return sorted(l, key=keyfunc)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def zipAllTimeSteps(directoryPath):
|
||||
if os.path.isfile(directoryPath):
|
||||
return
|
||||
|
||||
class UrlCounterDict(dict):
|
||||
Counter = 0
|
||||
|
||||
def GetUrlName(self, name):
|
||||
if name not in self.keys():
|
||||
self[name] = str(objNameToUrls.Counter)
|
||||
self.Counter = self.Counter + 1
|
||||
return self[name]
|
||||
|
||||
def InitIndex(sourcePath, destObj):
|
||||
with open(sourcePath, "r") as sourceFile:
|
||||
sourceData = sourceFile.read()
|
||||
sourceObj = json.loads(sourceData)
|
||||
for key in sourceObj:
|
||||
destObj[key] = sourceObj[key]
|
||||
# remove vtkHttpDataSetReader information
|
||||
for obj in destObj["scene"]:
|
||||
obj.pop(obj["type"])
|
||||
obj.pop("type")
|
||||
|
||||
def getUrlToNameDictionary(indexObj):
|
||||
urls = {}
|
||||
for obj in indexObj["scene"]:
|
||||
urls[obj[obj["type"]]["url"]] = obj["name"]
|
||||
return urls
|
||||
|
||||
def addDirectoryToZip(
|
||||
dirname, zipobj, storedData, rootIdx, timeStep, objNameToUrls
|
||||
):
|
||||
# Update root index.json file from index.json of this timestep
|
||||
with open(os.path.join(dirname, "index.json"), "r") as currentIdxFile:
|
||||
currentIdx = json.loads(currentIdxFile.read())
|
||||
urlToName = getUrlToNameDictionary(currentIdx)
|
||||
rootTimeStepSection = rootIdx["animation"]["timeSteps"][timeStep]
|
||||
for key in currentIdx:
|
||||
if key == "scene" or key == "version":
|
||||
continue
|
||||
rootTimeStepSection[key] = currentIdx[key]
|
||||
for obj in currentIdx["scene"]:
|
||||
objName = obj["name"]
|
||||
rootTimeStepSection[objName] = {}
|
||||
rootTimeStepSection[objName]["actor"] = obj["actor"]
|
||||
rootTimeStepSection[objName]["actorRotation"] = obj["actorRotation"]
|
||||
rootTimeStepSection[objName]["mapper"] = obj["mapper"]
|
||||
rootTimeStepSection[objName]["property"] = obj["property"]
|
||||
|
||||
# For every object in the current timestep
|
||||
for folder in sorted(os.listdir(dirname)):
|
||||
currentItem = os.path.join(dirname, folder)
|
||||
if os.path.isdir(currentItem) is False:
|
||||
continue
|
||||
# Write all data array of the current timestep in the archive
|
||||
for filename in os.listdir(os.path.join(currentItem, "data")):
|
||||
fullpath = os.path.join(currentItem, "data", filename)
|
||||
if os.path.isfile(fullpath) and filename not in storedData:
|
||||
storedData.add(filename)
|
||||
relPath = os.path.join("data", filename)
|
||||
zipobj.write(fullpath, arcname=relPath, compress_type=compression)
|
||||
# Write the index.json containing pointers to these data arrays
|
||||
# while replacing every basepath as '../../data'
|
||||
objIndexFilePath = os.path.join(dirname, folder, "index.json")
|
||||
with open(objIndexFilePath, "r") as objIndexFile:
|
||||
objIndexObjData = json.loads(objIndexFile.read())
|
||||
for elm in objIndexObjData.keys():
|
||||
try:
|
||||
if "ref" in objIndexObjData[elm].keys():
|
||||
objIndexObjData[elm]["ref"]["basepath"] = "../../data"
|
||||
if "arrays" in objIndexObjData[elm].keys():
|
||||
for array in objIndexObjData[elm]["arrays"]:
|
||||
array["data"]["ref"]["basepath"] = "../../data"
|
||||
except AttributeError:
|
||||
continue
|
||||
currentObjName = urlToName[folder]
|
||||
objIndexRelPath = os.path.join(
|
||||
objNameToUrls.GetUrlName(currentObjName), str(timeStep), "index.json"
|
||||
)
|
||||
zipobj.writestr(
|
||||
objIndexRelPath,
|
||||
json.dumps(objIndexObjData, indent=2),
|
||||
compress_type=compression,
|
||||
)
|
||||
|
||||
# ---
|
||||
|
||||
zipFilePath = "%s.zip" % directoryPath
|
||||
currentDirectory = os.path.abspath(os.path.join(directoryPath, os.pardir))
|
||||
rootIndexPath = os.path.join(currentDirectory, "index.json")
|
||||
rootIndexFile = open(rootIndexPath, "r")
|
||||
rootIndexObj = json.loads(rootIndexFile.read())
|
||||
|
||||
zf = zipfile.ZipFile(zipFilePath, mode="w")
|
||||
try:
|
||||
# We copy the scene from an index of a specific timestep to the root index
|
||||
# Scenes should all have the same objects so only do it for the first one
|
||||
isSceneInitialized = False
|
||||
# currentlyAddedData set stores hashes of every data we already added to the
|
||||
# vtkjs archive to prevent data duplication
|
||||
currentlyAddedData = set()
|
||||
# Regex that folders storing timestep data from paraview should follow
|
||||
reg = re.compile(r"^" + os.path.basename(directoryPath) + r"\.[0-9]+$")
|
||||
# We assume an object will not be deleted from a timestep to another so we create a generic index.json for each object
|
||||
genericIndexObj = {}
|
||||
genericIndexObj["series"] = []
|
||||
timeStep = 0
|
||||
for item in rootIndexObj["animation"]["timeSteps"]:
|
||||
genericIndexObj["series"].append({})
|
||||
genericIndexObj["series"][timeStep]["url"] = str(timeStep)
|
||||
genericIndexObj["series"][timeStep]["timeStep"] = float(item["time"])
|
||||
timeStep = timeStep + 1
|
||||
# Keep track of the url for every object
|
||||
objNameToUrls = UrlCounterDict()
|
||||
|
||||
timeStep = 0
|
||||
# zip all timestep directories
|
||||
for folder in numericSorted(os.listdir(currentDirectory)):
|
||||
fullPath = os.path.join(currentDirectory, folder)
|
||||
if os.path.isdir(fullPath) and reg.match(folder):
|
||||
if not isSceneInitialized:
|
||||
InitIndex(os.path.join(fullPath, "index.json"), rootIndexObj)
|
||||
isSceneInitialized = True
|
||||
addDirectoryToZip(
|
||||
fullPath,
|
||||
zf,
|
||||
currentlyAddedData,
|
||||
rootIndexObj,
|
||||
timeStep,
|
||||
objNameToUrls,
|
||||
)
|
||||
shutil.rmtree(fullPath)
|
||||
timeStep = timeStep + 1
|
||||
|
||||
# Write every index.json holding time information for each object
|
||||
for name in objNameToUrls:
|
||||
zf.writestr(
|
||||
os.path.join(objNameToUrls[name], "index.json"),
|
||||
json.dumps(genericIndexObj, indent=2),
|
||||
compress_type=compression,
|
||||
)
|
||||
|
||||
# Update root index.json urls and write it in the archive
|
||||
for obj in rootIndexObj["scene"]:
|
||||
obj["id"] = obj["name"]
|
||||
obj["type"] = "vtkHttpDataSetSeriesReader"
|
||||
obj["vtkHttpDataSetSeriesReader"] = {}
|
||||
obj["vtkHttpDataSetSeriesReader"]["url"] = objNameToUrls[obj["name"]]
|
||||
zf.writestr(
|
||||
"index.json", json.dumps(rootIndexObj, indent=2), compress_type=compression
|
||||
)
|
||||
os.remove(rootIndexPath)
|
||||
|
||||
finally:
|
||||
zf.close()
|
||||
|
||||
shutil.move(zipFilePath, directoryPath)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(
|
||||
"Usage: directoryToFile /path/to/directory.vtkjs [/path/to/ParaViewGlance.html]"
|
||||
)
|
||||
else:
|
||||
fileName = sys.argv[1]
|
||||
convertDirectoryToZipFile(fileName)
|
||||
|
||||
if len(sys.argv) == 3:
|
||||
addDataToViewer(fileName, sys.argv[2])
|
||||
@@ -0,0 +1,67 @@
|
||||
r"""wslink is a module that extends any
|
||||
wslink related classes for the purposes of vtkWeb.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
# import inspect, types, string, random, logging, six, json, re, base64
|
||||
import json, base64, logging, time
|
||||
|
||||
from vtkmodules.web.errors import WebDependencyMissingError
|
||||
|
||||
try:
|
||||
from wslink import websocket
|
||||
from wslink import register as exportRpc
|
||||
except ImportError:
|
||||
raise WebDependencyMissingError()
|
||||
|
||||
from vtkmodules.web import protocols
|
||||
from vtkmodules.vtkWebCore import vtkWebApplication
|
||||
|
||||
# =============================================================================
|
||||
application = None
|
||||
|
||||
# =============================================================================
|
||||
#
|
||||
# Base class for vtkWeb ServerProtocol
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ServerProtocol(websocket.ServerProtocol):
|
||||
"""
|
||||
Defines the core server protocol for vtkWeb. Adds support to
|
||||
marshall/unmarshall RPC callbacks that involve ServerManager proxies as
|
||||
arguments or return values.
|
||||
|
||||
Applications typically don't use this class directly, but instead
|
||||
sub-class it and call self.registerVtkWebProtocol() with useful vtkWebProtocols.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
logging.info("Creating SP")
|
||||
self.setSharedObject("app", self.initApplication())
|
||||
websocket.ServerProtocol.__init__(self)
|
||||
|
||||
def initApplication(self):
|
||||
"""
|
||||
Let subclass optionally initialize a custom application in lieu
|
||||
of the default vtkWebApplication.
|
||||
"""
|
||||
global application
|
||||
if not application:
|
||||
application = vtkWebApplication()
|
||||
return application
|
||||
|
||||
def setApplication(self, application):
|
||||
self.setSharedObject("app", application)
|
||||
|
||||
def getApplication(self):
|
||||
return self.getSharedObject("app")
|
||||
|
||||
def registerVtkWebProtocol(self, protocol):
|
||||
self.registerLinkProtocol(protocol)
|
||||
|
||||
def getVtkWebProtocols(self):
|
||||
return self.getLinkProtocols()
|
||||
Reference in New Issue
Block a user