Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix copyImageToCanvas bug and add ReadImageToCanvasOptions #7539

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

andremig-bentley
Copy link
Contributor

@andremig-bentley andremig-bentley commented Jan 10, 2025

This PR is to address this issue: https://github.com/iTwin/itwinjs-backlog/issues/1310

This PR has two main goals. First, to address the bug outlined in the above issue. Currently, when there is only a single viewport open and that canvas is copied, such as with copyImageToCanvas, canvas decorations do not show up on the resulting image. This is because the single view is rendered onto an on-screen webgl canvas for higher performance, while the canvas decorations were rendered onto a separate 2d canvas then laid on top. When saving, only the webgl canvas was being copied. This bug is fixed by combining the two canvases before saving them in this single viewport case.

Secondly, the issue above lays out a desire for a way to control whether or not decorations are in the image saved through Viewport.ReadImageToCanvas. This is accomplished through the addition of ReadImageToCanvasOptions. If undefined, or if ReadImageToCanvasOptions.includeCanvasDecorations is false, then the original behavior will persist. If includeCanvasDecorations is set to true, then the saved image will include the canvas decorations.

Also included in this PR is the SaveImageWithCanvasDecorations Tool for Display Test App. This tool gives an example of how to use the new ReadImageToCanvasOptions by allowing you to save an image of the canvas with the decorations either on or off.

@andremig-bentley andremig-bentley marked this pull request as ready for review January 10, 2025 16:58
@pmconne
Copy link
Member

pmconne commented Jan 10, 2025

Secondly, the issue above lays out a desire for a way to control whether or not decorations are shown, so that saved images of the canvas can still show no decorations when desired. This is solved with the addition of the canvasDecorations view flag.

Canvas decorations are not the only kinds of decorations. The application should be able to configure what decorations are or not displayed (just as they can control what other content is displayed) in the viewport, separately from capturing an image from the viewport's contents.

ViewFlags are for display styles and they are used to persist data in an iModel, the saved views service, etc. Decorations are mostly a transient viewport thing, not a display style thing, so adding a new view flag is not the correct solution.

@pmconne
Copy link
Member

pmconne commented Jan 10, 2025

This bug is fixed by combining the two canvases before saving them in this single viewport case.

Arguably, you have introduced a different bug by changing the existing behavior. Do we know if anyone was relying on the existing behavior?

@markschlosseratbentley
Copy link
Contributor

markschlosseratbentley commented Jan 10, 2025

This bug is fixed by combining the two canvases before saving them in this single viewport case.

Arguably, you have introduced a different bug by changing the existing behavior. Do we know if anyone was relying on the existing behavior?

Based on the GitHub issue and discussions: basically the app that raised this issue relies on this bug to be able to disable canvas-decorations-only when reading a viewport. However, this bug-reliance does not work if more than one viewport exists, so it's not a good behavior to rely on anyway. The bug only happens in the single viewport case.

The idea here was to fix the bug so they no longer have to work around the bug by calling internal API and messing around with our direct-to-screen logic from their app. Then, we provide them with a way to truly disable canvas decorations as they desired.

@pmconne
Copy link
Member

pmconne commented Jan 10, 2025

Then, we provide them with a way to truly disable canvas decorations as they desired.

If they truly only care about canvas decorations (I think there's some confusion about what is a canvas decoration and what is not), a dead-simple solution =

interface ReadImageToCanvasOptions {
  includeCanvasDecorations?: boolean;
}

class Viewport {
  public readImageToCanvas(options?: ReadImageToCanvasOptions): HTMLCanvasElement { ... }
}

If includeCanvasDecorations is undefined then existing behavior is preserved.

I would be reluctant to provide a Viewport.disableCanvasDecorations because canvas decorations include logos (such as Google Maps) that we are required contractually to display. We don't want to make it easy for applications to turn those off indefinitely.

@markschlosseratbentley
Copy link
Contributor

If they truly only care about canvas decorations (I think there's some confusion about what is a canvas decoration and what is not)

@ChrisBAtBentley @jason-crow Can you verify our understanding?

@andremig-bentley andremig-bentley changed the title Fix copyImageToCanvas bug and add canvasDecorations view flag Fix copyImageToCanvas bug and add control to view canvas decorations in copied canvases Jan 21, 2025
@andremig-bentley
Copy link
Contributor Author

The changes necessary for the canvasDecoration view flag have been removed.

@jason-crow can you confirm whether the simple solution outlined in this comment above will satisfy what you need? Or, do you think you need something with more control over the decorations akin to the view flags solution (although not the view flags solution as discussed above)? Let me know if you have any other questions.

@jason-crow
Copy link
Contributor

@markschlosseratbentley @pmconne apologies for the delay....

The proposed solution for allowing a prop to specify whether to include decorations:

interface ReadImageToCanvasOptions {
  includeCanvasDecorations?: boolean;
}

class Viewport {
  public readImageToCanvas(options?: ReadImageToCanvasOptions): HTMLCanvasElement { ... }
}

Is a perfect solution for us. We do not actually want to turn off decorations in the viewport, we just want a way to capture a screenshot of the viewport without them using readImageToCanvas. This is preferred for us since a saved view has no mechanism to restore the forms' markers that were possibly active when the screenshot was taken, so ideally the saved view thumbnail would keep them hidden so it doesn't give the user a false expectation about how it will appear when applied.

Since there was a bug that prevented the decorations from showing when a single viewport was active, versus 2, I don't really care if the default is for includeCanvasDecorations to be off or on, since I suppose either way there is a change in behavior; however, the suggested solution of making includeCanvasDecorations off by default probably preserves the most since most consumers likely work with a single viewport.

Thanks for the work on this!

@andremig-bentley andremig-bentley changed the title Fix copyImageToCanvas bug and add control to view canvas decorations in copied canvases Fix copyImageToCanvas bug and add ReadImageToCanvasOptions Jan 22, 2025
@andremig-bentley
Copy link
Contributor Author

As of the latest commit, this PR now includes the proposed ReadImageToCanvasOptions solution, as well the SaveImageWithCanvasDecorations Tool in display test app which provides an example of how to use the new options.

Copy link
Member

@eringram eringram left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core-frontend changes look good to me. Another thing I'd suggest is updating DTA docs for any tool changes

test-apps/display-test-app/public/locales/en/SVTTools.json Outdated Show resolved Hide resolved
@andremig-bentley
Copy link
Contributor Author

core-frontend changes look good to me. Another thing I'd suggest is updating DTA docs for any tool changes

DTA docs have been updated to include the new include decorations functionality on the existing save image tool

Copy link
Contributor

@markschlosseratbentley markschlosseratbentley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this probably warrants a NextVersion.md entry describing the changes to API behavior / flags.

@andremig-bentley andremig-bentley requested a review from a team as a code owner January 23, 2025 14:40
@andremig-bentley
Copy link
Contributor Author

I think this probably warrants a NextVersion.md entry describing the changes to API behavior / flags.

A NextVersion.md entry has been added.

Copy link
Contributor

@markschlosseratbentley markschlosseratbentley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a simple test or alter an existing test to use this new flag?

*/
export interface ReadImageToCanvasOptions {
/** If defined, canvas decorations will be included in the saved image.
* @note This only affects single viewport applications. For multi-viewport applications, the canvas decorations are always included.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Make the currently inconsistent behavior consistent.

Copy link
Contributor Author

@andremig-bentley andremig-bentley Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean when includeCanvasDecorations is undefined, remove decorations for both single and multi viewport applications in the saved image?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I say { includeCanvasDecorations: false }, the canvas decorations should be omitted, regardless of how many viewports I have open. Your @note says they will be included even if I tell you to omit them, if more than one viewport is open.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your note wrong? The comment above it is wrong (it claims any value other than undefined will cause the decorations to be included, but the property is a boolean).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above it is wrong, should say if true they will be included. The note was correct with the current behavior - basically, if includeCanvasDecorations !== true then current behavior will persist, this meaning if one viewport, no decorations, if multiple viewports, decorations included. You're right that this is inconsistent, working on changing it to:
if includeCanvasDecorations === true, then canvas decorations will be included, regardless of number of viewports. Otherwise, no canvas decorations will be included, regardless of number of viewports.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My recommendation is to make includeCanvasDecorations a non-optional property. The new options argument to readImageToCanvas is necessarily optional. So if you don't supply any options, you get the current dumb behavior for backwards compatibility. If you supply the options, you must choose whether or not to include canvas decorations. Then you only need to document the dumb "how many viewports" logic in one place (on readImageToCanvas).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better:

  /** [document dumb behavior here].
   * @deprecated in 5.0 Use the overload accepting a ReadImageToCanvasOptions.
   */
  public readImageToCanvas(): HTMLCanvasElement;
  /** [document sane behavior here] */
  public readImageToCanvas(options: ReadImageToCanvasOptions): HTMLCanvasElement;
  /** @internal */
  public readImageToCanvas(options: ReadImageToCanvasOptions | undefined): HTMLCanvasElement {
    // implementation goes here.
  }

Then you can make includeCanvasDecorations optional and give it a sane default behavior (include the decorations by default, in which case you should probably invert its name to omitCanvasDecorations since undefined is falsy), and we'll be able to remove the dumb behavior eventually.

});

it("should not include canvas decorations if omitCanvasDecorations is true", () => {
const vp = createViewport();
Copy link
Contributor Author

@andremig-bentley andremig-bentley Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of these tests is to: Open a viewport, place a canvas decoration, use readImageToCanvas to get a copied canvas, check if the decoration is still there. This works and passes when the decoration is supposed to be included in the saved image (when omitCanvasDecorations is false). I am looking for feedback for the case in which the decorations should not be included in the saved image.

That result is relying on the fact that we render directly to the webgl canvas for a single viewport, and canvas decorations are placed on an overlaying second canvas. For decorations to be included in the saved image, the overlaying canvas must be combined with the webgl canvas. When testing this, despite no other viewports present and no combination taking place (confirmed through debug), the decorations remain present in this case, meaning the test is not using the standard single viewport behavior.

Any suggestions on forcing the test to use the direct render to screen methodology? Other things I've tried are written out in a comment in the code, but I'll add them here as well:

 * Use openBlankViewport to get a blank viewport instead of creating a screen viewport directly
 * Set vp.rendersToScreen to true before calling vp.renderFrame (for both vp types)
 * Use vp.target.SetRenderToScreen to do the same thing
 * Making sure all Viewports are removed before adding a new one at the beginning of the test
 * Fully shutting down IModel App and then restarting it at the beginning of the test

@andremig-bentley
Copy link
Contributor Author

Still To Do:

  • Unit Tests
  • Clean up readImageToCanvas declarations/implementations
  • Clean up docs/readme/nextversion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants