import * as THREE from 'three';
import { WebGLScene, getShader, formatTimeSpan } from './utils';
function generateColorTexture(names) {
const n = names.length;
const data = new Float32Array(3 * n + 3);
for (let i = 0; i < n; i++) {
const c = getColorFromString(names[i]);
data[3 * i + 0] = c.r;
data[3 * i + 1] = c.g;
data[3 * i + 2] = c.b;
data[4 * i + 3] = 1.0;
}
const tex = new THREE.DataTexture(
data,
n,
1,
THREE.RGBAFormat,
THREE.FloatType
);
tex.magFilter = THREE.NearestFilter;
tex.needsUpdate = true;
return tex;
}
function getColorFromString(s) {
let hash = 0;
let i = 0;
if (s.length === 0) return [0, 0, 0];
for (i = 0; i < s.length; i++) {
// chr = s.charCodeAt(i);
// hash = ((hash << 5) - hash) + chr;
// hash |= 0; // Convert to 32bit integer
const c = s.charCodeAt(i);
hash += (i + 1) * (i + 1) * 53 * c;
}
hash = Math.abs(hash);
hash = hash * 47;
const hue = (hash % 256) / 256;
hash = Math.floor(hash / 256);
const lightness = 0.3 + (0.4 * (hash % 256)) / 256;
hash = Math.floor(hash / 256);
const saturation = 0.3 + (0.4 * (hash % 256)) / 256;
const c = new THREE.Color();
c.setHSL(hue, saturation, lightness);
return c;
}
function CreateStreamVisualizer(rectangles, uniforms) {
const n_rects = rectangles.name_id.length;
const rect_ids = new Float32Array(n_rects);
for (let i = 0; i < n_rects; i++) rect_ids[i] = i + 1;
const geo = new THREE.InstancedBufferGeometry();
geo.setAttribute(
'position',
new THREE.BufferAttribute(
new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0]),
3
)
);
geo.setAttribute(
'times',
new THREE.InstancedBufferAttribute(new Float32Array(rectangles.times), 2)
);
geo.setAttribute(
'level',
new THREE.InstancedBufferAttribute(new Uint16Array(rectangles.level), 1)
);
geo.setAttribute(
'name_id',
new THREE.InstancedBufferAttribute(new Uint16Array(rectangles.name_id), 1)
);
geo.setAttribute('rect_id', new THREE.InstancedBufferAttribute(rect_ids, 1));
geo.setDrawRange(0, 6);
geo.instanceCount = n_rects;
geo._maxInstanceCount = n_rects;
const material = new THREE.RawShaderMaterial({
vertexShader: getShader('trace_explorer.vert'),
fragmentShader: getShader('trace_explorer.frag'),
side: THREE.DoubleSide,
uniforms: uniforms,
});
const mesh = new THREE.Mesh(geo, material);
mesh.frustumCulled = false;
return mesh;
}
export class TraceViewer extends WebGLScene {
camera;
names;
streams;
scene;
uniforms;
buffer_texture;
pixels;
w;
h;
last_id;
data;
tooltip;
initCanvas(element, data) {
WebGLScene.prototype.initCanvas.call(this, element, {});
window.addEventListener('resize', () => this.onResize(), false);
this.uniforms = {};
this.camera = new THREE.OrthographicCamera(
0,
data.max_time,
3.0,
-5.0,
-100,
100
);
this.data = data;
// this.streams.push(
this.scene = new THREE.Scene();
this.scene.add(CreateStreamVisualizer(data.streams[0], this.uniforms));
this.uniforms.tex_colormap = { value: generateColorTexture(data.names) };
this.uniforms.n_colors = { value: data.max_id + 1 };
this.uniforms.mode = { value: 0 };
this.uniforms.selected_rect = { value: -1 };
// const geometry = new THREE.BoxGeometry(1, 1, 1);
// const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
// const cube = new THREE.Mesh(geometry, material);
// this.scene.add(cube);
this.onResize();
this.names = data.names;
this.tooltip = document.querySelectorAll('.tooltip span')[0];
console.log('tooltip', this.tooltip);
}
constructor(parent, data) {
super();
this.streams = [];
// this.names = data.names;
// this.streams = data.streams;
this.initCanvas(parent, data);
this.canvas.addEventListener(
'mousemove',
(e) => this.onMouseMove(e),
false
);
this.canvas.addEventListener(
'mousewheel',
(e) => this.onMouseWheel(e),
false
);
this.last_id = 0;
}
onResize() {
const w = this.element.parentNode.clientWidth - 20;
const h = this.element.parentNode.clientHeight - 20;
this.w = w;
this.h = h;
const aspect = w / h;
this.camera.aspect = aspect;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
{
this.pixels = new Float32Array(w * h * 4);
this.buffer_texture = new THREE.WebGLRenderTarget(w, h, {
minFilter: THREE.LinearFilter,
magFilter: THREE.NearestFilter,
type: THREE.FloatType,
format: THREE.RGBAFormat,
});
this.uniforms.tex_values = new THREE.Uniform(this.buffer_texture.texture);
}
this.animate();
}
render() {
const gl = this.context;
// gl.depthFunc(gl.GREATER);
// gl.depthFunc(gl.GREATER);
// gl.depthFunc(gl.GEQUAL);
// gl.clearDepth(0);
this.uniforms.mode.value = 0;
this.renderer.clear(true, true, true);
this.renderer.render(this.scene, this.camera);
{
this.uniforms.mode.value = 1;
this.renderer.setRenderTarget(this.buffer_texture);
this.renderer.setClearColor(new THREE.Color(0.0, 0.0, 0.0));
this.renderer.clear(true, true, true);
this.renderer.render(this.scene, this.camera);
gl.readPixels(
0,
0,
gl.drawingBufferWidth,
gl.drawingBufferHeight,
gl.RGBA,
gl.FLOAT,
this.pixels
);
this.renderer.setRenderTarget(null);
this.renderer.setClearColor(new THREE.Color(1.0, 1.0, 1.0));
}
this.requestId = 0;
}
getMousePos(/*x, y*/) {
// do nothing
}
onMouseWheel(event) {
event.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) / (rect.right - rect.left);
const dt = this.camera.right - this.camera.left;
const t = this.camera.left + x * dt;
const scaling = 1.0 + 1e-3 * event.deltaY;
let t0 = t + scaling * (this.camera.left - t);
let t1 = t + scaling * (this.camera.right - t);
if (t0 < 0) t0 = 0;
if (t1 > this.data.max_time) t1 = this.data.max_time;
this.camera.left = t0;
this.camera.right = t1;
this.camera.updateProjectionMatrix();
this.animate();
}
onMouseMove(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = rect.bottom - event.clientY;
let id = this.pixels[4 * (x + this.w * y)];
if (id != this.last_id) {
this.last_id = id;
this.uniforms.selected_rect.value = id;
if (id > 0) {
id -= 1;
const name_id = this.data.streams[0].name_id[id];
const t0 = this.data.streams[0].times[2 * id];
const t1 = this.data.streams[0].times[2 * id + 1];
const name = this.data.names[name_id];
let text = name + '
';
text += `start: ${formatTimeSpan(t0)}
`;
text += `stop: ${formatTimeSpan(t1)}
`;
text += `duration: ${formatTimeSpan(t1 - t0)}
`;
this.tooltip.innerHTML = text;
this.tooltip.style.visibility = 'visible';
} else {
this.tooltip.style.visibility = 'hidden';
}
this.animate();
}
{
if (rect.width - x < 300) {
const sx = event.clientX - 20 + 'px';
this.tooltip.style.right = sx;
} else {
const sx = event.clientX + 20 + 'px';
this.tooltip.style.left = sx;
}
const sy = event.clientY + 20 + 'px';
this.tooltip.style.top = sy;
}
}
}