Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
62 changes: 46 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,62 @@
FROM nvidia/opengl:1.0-glvnd-devel-ubuntu20.04 AS build
FROM nvidia/opengl:1.0-glvnd-devel-ubuntu22.04 AS build

# 1. System dependencies needed by native Node modules (canvas, headless-gl)
RUN apt-get update -y
RUN apt-get install -y curl gnupg ca-certificates && \
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs
RUN apt-get install -y build-essential python libxi-dev libglu-dev libglew-dev pkg-config git
# 0. Set frontend to noninteractive to suppress warnings during install
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev

# 2. Project workspace
# 1. System dependencies & Python Fix
# We combine everything into ONE Run command to keep the image small and avoid errors.
RUN apt-get update -y && \
# Install prerequisites for Node setup
apt-get install -y --no-install-recommends curl gnupg ca-certificates && \
# Setup Node 18 repository
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
# Install Node, Python 3, Build Tools, and Canvas dependencies
apt-get install -y --no-install-recommends \
nodejs \
build-essential \
python3 \
python3-dev \
python-is-python3 \
libxi-dev \
libglu-dev \
libglew-dev \
pkg-config \
git \
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev && \
# Clean up apt lists to save space (Must be done at the very end!)
rm -rf /var/lib/apt/lists/*

# 2. Build Phoenix
WORKDIR /phoenix
COPY . .

# 3. Environment: mark CI + skip Cypress binary (we only build static docs here)
# Enable Corepack
RUN corepack enable

# Force native modules (lmdb) to build from source
ENV npm_config_build_from_source=true

# *** IMPORTANT FIX ***
# Tell node-gyp explicitly to use the python3 executable we just installed
ENV npm_config_python=/usr/bin/python3

# CI environment variables
ENV CI=1
ENV CYPRESS_INSTALL_BINARY=0

# 4. Use Yarn via Corepack (honors yarnPath -> Yarn 3 committed in repo)
RUN corepack enable

# 5. Install dependencies & build the web/docs
# Install dependencies (verbose so you can see if python errors occur)
RUN yarn install --silent

# Build the web app
RUN yarn deploy:web

# 6. Remove node_modules to keep final image lean
# Remove node_modules folders to save space
RUN find . -name "node_modules" -type d -exec rm -rf "{}" +

# 7. Runtime stage: just serve the built docs
# 3. Serve the build through NGINX
FROM nginx:alpine
COPY --from=build /phoenix/packages/phoenix-ng/docs /usr/share/nginx/html
231 changes: 137 additions & 94 deletions packages/phoenix-event-display/src/managers/three-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ export class ThreeManager {
elem: Intersection<Object3D<Object3DEventMap>>,
) => boolean;
/** 'click' event listener callback to show 3D coordinates of the clicked point */
private show3DPointsCallback: (event: MouseEvent) => void;
private show3DPointsCallback: (event: MouseEvent) => void = () => {};
/** 'click' event listener callback to shift the cartesian grid at the clicked point */
private shiftCartesianGridCallback: (event: MouseEvent) => void;
private shiftCartesianGridCallback: (event: MouseEvent) => void = () => {};
/** 'click' event listener callback to show 3D distance between two clicked points */
private show3DDistanceCallback: (event: MouseEvent) => void;
private show3DDistanceCallback: (event: MouseEvent) => void = () => {};

/** Origin of the cartesian grid w.r.t. world origin */
public origin: Vector3 = new Vector3(0, 0, 0);
/** Scene export ignore list. */
Expand All @@ -122,7 +123,7 @@ export class ThreeManager {
/** Color of the text to be displayed as per dark theme */
private displayColor: string = 'black';
/** Mousemove callback to draw dynamic distance line */
private mousemoveCallback: (event: MouseEvent) => void;
private mousemoveCallback: (event: MouseEvent) => void = () => {};
/** Emitting that a new 3D coordinate has been clicked upon */
originChanged = new EventEmitter<Vector3>();
/** Whether the shifting of the grid is enabled */
Expand Down Expand Up @@ -1405,110 +1406,152 @@ 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();
}

// ---------------------------
// 2. Prepare output canvas
// ---------------------------
const output = document.getElementById(
'screenshotCanvas',
) as HTMLCanvasElement;
output.width = width;
output.height = height;

// get background color to be used
const bkgColor = getComputedStyle(document.body).getPropertyValue(
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);

const tilesX = Math.ceil(width / tileW);
const tilesY = Math.ceil(height / tileH);

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 (
camera instanceof PerspectiveCamera ||
camera instanceof OrthographicCamera
) {
camera.setViewOffset(targetWidth, targetHeight, effX, effY, w, h);
}

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

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

mainRenderer.setSize(originalSize.width, originalSize.height, false);
this.render();
// Clear camera offset
if (
camera instanceof PerspectiveCamera ||
camera instanceof OrthographicCamera
) {
camera.clearViewOffset();
}

// 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,
);
// 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();
}
});
});
});
// 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