diff --git a/Dockerfile b/Dockerfile index ee8baa660..25b4314c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/packages/phoenix-event-display/src/managers/three-manager/index.ts b/packages/phoenix-event-display/src/managers/three-manager/index.ts index e4681990d..841810f43 100644 --- a/packages/phoenix-event-display/src/managers/three-manager/index.ts +++ b/packages/phoenix-event-display/src/managers/three-manager/index.ts @@ -94,11 +94,12 @@ export class ThreeManager { elem: Intersection>, ) => 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. */ @@ -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(); /** Whether the shifting of the grid is enabled */ @@ -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(); + }); } /** diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/make-picture/make-picture.component.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/make-picture/make-picture.component.ts index 941be5159..191f87cd8 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/make-picture/make-picture.component.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/make-picture/make-picture.component.ts @@ -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