@@ -94,11 +94,12 @@ export class ThreeManager {
9494 elem : Intersection < Object3D < Object3DEventMap > > ,
9595 ) => boolean ;
9696 /** 'click' event listener callback to show 3D coordinates of the clicked point */
97- private show3DPointsCallback : ( event : MouseEvent ) => void ;
97+ private show3DPointsCallback : ( event : MouseEvent ) => void = ( ) => { } ;
9898 /** 'click' event listener callback to shift the cartesian grid at the clicked point */
99- private shiftCartesianGridCallback : ( event : MouseEvent ) => void ;
99+ private shiftCartesianGridCallback : ( event : MouseEvent ) => void = ( ) => { } ;
100100 /** 'click' event listener callback to show 3D distance between two clicked points */
101- private show3DDistanceCallback : ( event : MouseEvent ) => void ;
101+ private show3DDistanceCallback : ( event : MouseEvent ) => void = ( ) => { } ;
102+
102103 /** Origin of the cartesian grid w.r.t. world origin */
103104 public origin : Vector3 = new Vector3 ( 0 , 0 , 0 ) ;
104105 /** Scene export ignore list. */
@@ -122,7 +123,7 @@ export class ThreeManager {
122123 /** Color of the text to be displayed as per dark theme */
123124 private displayColor : string = 'black' ;
124125 /** Mousemove callback to draw dynamic distance line */
125- private mousemoveCallback : ( event : MouseEvent ) => void ;
126+ private mousemoveCallback : ( event : MouseEvent ) => void = ( ) => { } ;
126127 /** Emitting that a new 3D coordinate has been clicked upon */
127128 originChanged = new EventEmitter < Vector3 > ( ) ;
128129 /** Whether the shifting of the grid is enabled */
@@ -1405,110 +1406,152 @@ export class ThreeManager {
14051406 * - Strech : current view is streched to given format
14061407 * this is the default and used also for any other value given to fitting
14071408 */
1408- public makeScreenShot (
1409+ /**
1410+ * Takes a very large screenshot safely by tiling renders
1411+ */
1412+ public async makeScreenShot (
14091413 width : number ,
14101414 height : number ,
1411- fitting : string = 'Strech ' ,
1415+ fitting : string = 'Stretch ' ,
14121416 ) {
1413- // compute actual size of screen shot, based on current view and requested size
1414- const mainRenderer = this . rendererManager . getMainRenderer ( ) ;
1417+ const renderer = this . rendererManager . getMainRenderer ( ) ;
1418+ const camera = this . controlsManager . getMainCamera ( ) ;
1419+
1420+ // ORIGINAL SCREEN SIZE
14151421 const originalSize = new Vector2 ( ) ;
1416- mainRenderer . getSize ( originalSize ) ;
1417- const scaledSize = this . croppedSize (
1418- width ,
1419- height ,
1420- originalSize . width ,
1421- originalSize . height ,
1422- ) ;
1423- const heightShift = ( scaledSize . height - height ) / 2 ;
1424- const widthShift = ( scaledSize . width - width ) / 2 ;
1422+ renderer . getSize ( originalSize ) ;
1423+ const originalWidth = originalSize . width ;
1424+ const originalHeight = originalSize . height ;
1425+
1426+ // ---------------------------
1427+ // 1. CROP & STRETCH LOGIC
1428+ // ---------------------------
1429+ let targetWidth = width ;
1430+ let targetHeight = height ;
1431+ let shiftX = 0 ;
1432+ let shiftY = 0 ;
1433+
1434+ if ( fitting === 'Crop' ) {
1435+ const scaled = this . croppedSize (
1436+ width ,
1437+ height ,
1438+ originalWidth ,
1439+ originalHeight ,
1440+ ) ;
1441+
1442+ targetWidth = scaled . width ;
1443+ targetHeight = scaled . height ;
1444+
1445+ shiftX = ( scaled . width - width ) / 2 ;
1446+ shiftY = ( scaled . height - height ) / 2 ;
1447+ }
1448+
1449+ // Stretch → KEEP exact width/height (NO crop, NO shift)
1450+ if ( fitting === 'Stretch' ) {
1451+ shiftX = 0 ;
1452+ shiftY = 0 ;
1453+ targetWidth = width ;
1454+ targetHeight = height ;
1455+ }
1456+
1457+ // Fix aspect only for PerspectiveCamera
1458+ let originalAspect : number | undefined ;
1459+ if ( fitting === 'Stretch' && camera instanceof PerspectiveCamera ) {
1460+ originalAspect = camera . aspect ;
1461+ camera . aspect = width / height ;
1462+ camera . updateProjectionMatrix ( ) ;
1463+ }
1464+
1465+ // ---------------------------
1466+ // 2. Prepare output canvas
1467+ // ---------------------------
1468+ const output = document . getElementById (
1469+ 'screenshotCanvas' ,
1470+ ) as HTMLCanvasElement ;
1471+ output . width = width ;
1472+ output . height = height ;
14251473
1426- // get background color to be used
1427- const bkgColor = getComputedStyle ( document . body ) . getPropertyValue (
1474+ const ctxOut = output . getContext ( '2d' ) ! ;
1475+ const bg = getComputedStyle ( document . body ) . getPropertyValue (
14281476 '--phoenix-background-color' ,
14291477 ) ;
1478+ ctxOut . fillStyle = bg ;
1479+ ctxOut . fillRect ( 0 , 0 , width , height ) ;
14301480
1431- // Deal with devices having special devicePixelRatio (retina screens in particular)
1481+ // ---------------------------
1482+ // 3. TILE RENDERING
1483+ // ---------------------------
14321484 const scale = window . devicePixelRatio ;
1485+ const gl = renderer . getContext ( ) ;
1486+ const maxSize = gl . getParameter ( gl . MAX_RENDERBUFFER_SIZE ) ;
14331487
1434- // grab output canvas on which we will draw, and set size
1435- const outputCanvas = document . getElementById (
1436- 'screenshotCanvas' ,
1437- ) as HTMLCanvasElement ;
1438- outputCanvas . width = width ;
1439- outputCanvas . height = height ;
1440- outputCanvas . style . width = ( width / scale ) . toString ( ) + 'px' ;
1441- outputCanvas . style . height = ( height / scale ) . toString ( ) + 'px' ;
1442- const ctx = outputCanvas . getContext ( '2d' ) ;
1443- if ( ctx ) {
1444- ctx . fillStyle = bkgColor ;
1445- ctx . fillRect ( 0 , 0 , width , height ) ;
1446- // draw main image on our output canvas, with right size
1447- mainRenderer . setSize (
1448- scaledSize . width / scale ,
1449- scaledSize . height / scale ,
1450- false ,
1451- ) ;
1452- this . render ( ) ;
1453- ctx . drawImage (
1454- mainRenderer . domElement ,
1455- widthShift ,
1456- heightShift ,
1457- width ,
1458- height ,
1459- 0 ,
1460- 0 ,
1461- width ,
1462- height ,
1463- ) ;
1488+ const tileW = Math . min ( width , maxSize ) ;
1489+ const tileH = Math . min ( height , maxSize ) ;
1490+
1491+ const tilesX = Math . ceil ( width / tileW ) ;
1492+ const tilesY = Math . ceil ( height / tileH ) ;
1493+
1494+ for ( let ty = 0 ; ty < tilesY ; ty ++ ) {
1495+ for ( let tx = 0 ; tx < tilesX ; tx ++ ) {
1496+ const offsetX = tx * tileW ;
1497+ const offsetY = ty * tileH ;
1498+
1499+ const w = Math . min ( tileW , width - offsetX ) ;
1500+ const h = Math . min ( tileH , height - offsetY ) ;
1501+
1502+ // FINAL effective offsets for camera
1503+ const effX = offsetX + shiftX ;
1504+ const effY = offsetY + shiftY ;
1505+
1506+ if (
1507+ camera instanceof PerspectiveCamera ||
1508+ camera instanceof OrthographicCamera
1509+ ) {
1510+ camera . setViewOffset ( targetWidth , targetHeight , effX , effY , w , h ) ;
1511+ }
1512+
1513+ renderer . setSize ( w / scale , h / scale , false ) ;
1514+ this . render ( ) ;
1515+
1516+ ctxOut . drawImage (
1517+ renderer . domElement ,
1518+ 0 ,
1519+ 0 ,
1520+ w ,
1521+ h ,
1522+ offsetX ,
1523+ offsetY ,
1524+ w ,
1525+ h ,
1526+ ) ;
1527+ }
14641528 }
14651529
1466- mainRenderer . setSize ( originalSize . width , originalSize . height , false ) ;
1467- this . render ( ) ;
1530+ // Clear camera offset
1531+ if (
1532+ camera instanceof PerspectiveCamera ||
1533+ camera instanceof OrthographicCamera
1534+ ) {
1535+ camera . clearViewOffset ( ) ;
1536+ }
14681537
1469- // Get info panel
1470- const infoPanel = document . getElementById ( 'experimentInfo' ) ;
1471- if ( infoPanel != null ) {
1472- // Compute size of info panel on final picture
1473- const infoHeight =
1474- ( infoPanel . clientHeight * scaledSize . height ) / originalSize . height ;
1475- const infoWidth =
1476- ( infoPanel . clientWidth * scaledSize . width ) / originalSize . width ;
1477-
1478- // Add info panel to output. This is HTML, so first convert it to canvas,
1479- // and then draw to our output canvas
1480- const h2c : any = html2canvas ;
1481- // See: https://github.com/niklasvh/html2canvas/issues/1977#issuecomment-529448710 for why this is needed
1482- h2c ( infoPanel , {
1483- backgroundColor : bkgColor ,
1484- // avoid cloning canvas in the main page, this is useless and leads to
1485- // warnings in the javascript console similar to this :
1486- // "Unable to clone WebGL context as it has preserveDrawingBuffer=false"
1487- ignoreElements : ( element : Element ) => element . tagName == 'CANVAS' ,
1488- } ) . then ( ( canvas : HTMLCanvasElement ) => {
1489- canvas . toBlob ( ( blob ) => {
1490- ctx ?. drawImage (
1491- canvas ,
1492- infoHeight / 6 ,
1493- infoHeight / 6 ,
1494- infoWidth ,
1495- infoHeight ,
1496- ) ;
1497- // Finally save to png file
1498- outputCanvas . toBlob ( ( blob ) => {
1499- if ( blob ) {
1500- const a = document . createElement ( 'a' ) ;
1501- document . body . appendChild ( a ) ;
1502- a . style . display = 'none' ;
1503- const url = window . URL . createObjectURL ( blob ) ;
1504- a . href = url ;
1505- a . download = `screencapture.png` ;
1506- a . click ( ) ;
1507- }
1508- } ) ;
1509- } ) ;
1510- } ) ;
1538+ // Restore original aspect if changed
1539+ if ( originalAspect !== undefined && camera instanceof PerspectiveCamera ) {
1540+ camera . aspect = originalAspect ;
1541+ camera . updateProjectionMatrix ( ) ;
15111542 }
1543+
1544+ // Reset renderer size
1545+ renderer . setSize ( originalWidth , originalHeight , false ) ;
1546+ this . render ( ) ;
1547+
1548+ output . toBlob ( ( blob ) => {
1549+ if ( ! blob ) return ;
1550+ const a = document . createElement ( 'a' ) ;
1551+ a . href = URL . createObjectURL ( blob ) ;
1552+ a . download = 'screencapture.png' ;
1553+ a . click ( ) ;
1554+ } ) ;
15121555 }
15131556
15141557 /**
0 commit comments