Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
67d522d
feat: add tiled screenshot rendering to support very large images (#581)
deveshbervar Nov 25, 2025
e06a9c2
fix: safe camera view offset calls
deveshbervar Nov 27, 2025
6d7a5f0
fix(make-picture): remove outdated size check for tiled screenshots
deveshbervar Nov 30, 2025
c9ee693
fix: remove checkSize and disabled logic as requested
deveshbervar Dec 1, 2025
cc8b764
fix: restore crop/stretch logic + integrate tiling safely
deveshbervar Dec 2, 2025
5a1662c
fix: correct Stretch screenshot aspect ratio & restore camera aspect
deveshbervar Dec 3, 2025
5eddf2d
fix: restore stretch aspect logic + safe TS guards
deveshbervar Dec 3, 2025
1b836b3
fix: correct stretch rendering logic, remove crop offsets, proper vie…
deveshbervar Dec 4, 2025
4a6d799
fix: initialize callback functions + update Docker install step to fi…
deveshbervar Dec 4, 2025
247b909
fix: resolve Dockerfile conflict and enable native build for lmdb
deveshbervar Dec 4, 2025
5b0c2b1
Merge branch 'main' into screenshot-tiling-fix
EdwardMoyse Dec 5, 2025
e86b35d
fix: initialize callbacks + update Dockerfile base image to fix CI
deveshbervar Dec 6, 2025
0c41d48
fix: restore callback typings + initialize defaults + replace python …
deveshbervar Dec 7, 2025
07e9fc1
fix: correct viewOffset typing to resolve TS2349 error
deveshbervar Dec 7, 2025
82e5612
fix: python install and node-gyp python3 path for CI
deveshbervar Dec 7, 2025
56317f2
fix: remove unsupported --verbose flag from yarn install
deveshbervar Dec 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 133 additions & 90 deletions packages/phoenix-event-display/src/managers/three-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1405,110 +1405,153 @@ export class ThreeManager {
* - Strech : current view is streched to given format
* this is the default and used also for any other value given to fitting
*/
public makeScreenShot(
/**
* Takes a very large screenshot safely by tiling renders
*/
public async makeScreenShot(
width: number,
height: number,
fitting: string = 'Strech',
fitting: string = 'Stretch',
) {
// compute actual size of screen shot, based on current view and requested size
const mainRenderer = this.rendererManager.getMainRenderer();
const renderer = this.rendererManager.getMainRenderer();
const camera = this.controlsManager.getMainCamera();

// ORIGINAL SCREEN SIZE
const originalSize = new Vector2();
mainRenderer.getSize(originalSize);
const scaledSize = this.croppedSize(
width,
height,
originalSize.width,
originalSize.height,
);
const heightShift = (scaledSize.height - height) / 2;
const widthShift = (scaledSize.width - width) / 2;
renderer.getSize(originalSize);
const originalWidth = originalSize.width;
const originalHeight = originalSize.height;

// ---------------------------
// 1. CROP & STRETCH LOGIC
// ---------------------------
let targetWidth = width;
let targetHeight = height;
let shiftX = 0;
let shiftY = 0;

if (fitting === 'Crop') {
const scaled = this.croppedSize(
width,
height,
originalWidth,
originalHeight,
);

targetWidth = scaled.width;
targetHeight = scaled.height;

shiftX = (scaled.width - width) / 2;
shiftY = (scaled.height - height) / 2;
}

// Stretch → KEEP exact width/height (NO crop, NO shift)
if (fitting === 'Stretch') {
shiftX = 0;
shiftY = 0;
targetWidth = width;
targetHeight = height;
}

// Fix aspect only for PerspectiveCamera
let originalAspect: number | undefined;
if (fitting === 'Stretch' && camera instanceof PerspectiveCamera) {
originalAspect = camera.aspect;
camera.aspect = width / height;
camera.updateProjectionMatrix();
}

// get background color to be used
const bkgColor = getComputedStyle(document.body).getPropertyValue(
// ---------------------------
// 2. Prepare output canvas
// ---------------------------
const output = document.getElementById(
'screenshotCanvas',
) as HTMLCanvasElement;
output.width = width;
output.height = height;

const ctxOut = output.getContext('2d')!;
const bg = getComputedStyle(document.body).getPropertyValue(
'--phoenix-background-color',
);
ctxOut.fillStyle = bg;
ctxOut.fillRect(0, 0, width, height);

// Deal with devices having special devicePixelRatio (retina screens in particular)
// ---------------------------
// 3. TILE RENDERING
// ---------------------------
const scale = window.devicePixelRatio;
const gl = renderer.getContext();
const maxSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);

// grab output canvas on which we will draw, and set size
const outputCanvas = document.getElementById(
'screenshotCanvas',
) as HTMLCanvasElement;
outputCanvas.width = width;
outputCanvas.height = height;
outputCanvas.style.width = (width / scale).toString() + 'px';
outputCanvas.style.height = (height / scale).toString() + 'px';
const ctx = outputCanvas.getContext('2d');
if (ctx) {
ctx.fillStyle = bkgColor;
ctx.fillRect(0, 0, width, height);
// draw main image on our output canvas, with right size
mainRenderer.setSize(
scaledSize.width / scale,
scaledSize.height / scale,
false,
);
this.render();
ctx.drawImage(
mainRenderer.domElement,
widthShift,
heightShift,
width,
height,
0,
0,
width,
height,
);
}
const tileW = Math.min(width, maxSize);
const tileH = Math.min(height, maxSize);

mainRenderer.setSize(originalSize.width, originalSize.height, false);
this.render();
const tilesX = Math.ceil(width / tileW);
const tilesY = Math.ceil(height / tileH);

// Get info panel
const infoPanel = document.getElementById('experimentInfo');
if (infoPanel != null) {
// Compute size of info panel on final picture
const infoHeight =
(infoPanel.clientHeight * scaledSize.height) / originalSize.height;
const infoWidth =
(infoPanel.clientWidth * scaledSize.width) / originalSize.width;

// Add info panel to output. This is HTML, so first convert it to canvas,
// and then draw to our output canvas
const h2c: any = html2canvas;
// See: https://github.com/niklasvh/html2canvas/issues/1977#issuecomment-529448710 for why this is needed
h2c(infoPanel, {
backgroundColor: bkgColor,
// avoid cloning canvas in the main page, this is useless and leads to
// warnings in the javascript console similar to this :
// "Unable to clone WebGL context as it has preserveDrawingBuffer=false"
ignoreElements: (element: Element) => element.tagName == 'CANVAS',
}).then((canvas: HTMLCanvasElement) => {
canvas.toBlob((blob) => {
ctx?.drawImage(
canvas,
infoHeight / 6,
infoHeight / 6,
infoWidth,
infoHeight,
for (let ty = 0; ty < tilesY; ty++) {
for (let tx = 0; tx < tilesX; tx++) {
const offsetX = tx * tileW;
const offsetY = ty * tileH;

const w = Math.min(tileW, width - offsetX);
const h = Math.min(tileH, height - offsetY);

// FINAL effective offsets for camera
const effX = offsetX + shiftX;
const effY = offsetY + shiftY;

if ('setViewOffset' in camera) {
(camera as PerspectiveCamera | OrthographicCamera).setViewOffset(
targetWidth,
targetHeight,
effX,
effY,
w,
h,
);
// Finally save to png file
outputCanvas.toBlob((blob) => {
if (blob) {
const a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = `screencapture.png`;
a.click();
}
});
});
});
}

renderer.setSize(w / scale, h / scale, false);
this.render();

ctxOut.drawImage(
renderer.domElement,
0,
0,
w,
h,
offsetX,
offsetY,
w,
h,
);
}
}

// Clear camera offset
if ('clearViewOffset' in camera) {
camera.clearViewOffset();
}

// Restore original aspect if changed
if (originalAspect !== undefined && camera instanceof PerspectiveCamera) {
camera.aspect = originalAspect;
camera.updateProjectionMatrix();
}

// Reset renderer size
renderer.setSize(originalWidth, originalHeight, false);
this.render();

output.toBlob((blob) => {
if (!blob) return;
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'screencapture.png';
a.click();
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,17 @@ export class MakePictureComponent implements OnInit {
disabled: boolean = false;
constructor(private eventDisplay: EventDisplayService) {}
ngOnInit() {}
private checkSize() {
return this.eventDisplay
.getThreeManager()
.checkScreenShotCanvasSize(this.width, this.height, this.fitting);
}

setWidth(value) {
this.width = value;
this.disabled = !this.checkSize();
this.disabled = false;
}
setHeight(value) {
this.height = value;
this.disabled = !this.checkSize();
this.disabled = false;
}
buttonText() {
return this.disabled ? 'Size too large' : 'Create picture';
return 'Create picture';
}
makePicture() {
this.eventDisplay
Expand Down
Loading