From 5e86c52df05d908e9f728272dedf39a882bd82ba Mon Sep 17 00:00:00 2001 From: Georgina Halpern Date: Thu, 18 Sep 2025 18:41:43 -0400 Subject: [PATCH 1/6] texture previewer --- .../textures/baseTextureProperties.tsx | 9 +- .../properties/textures/textureHelper.ts | 123 ++++++++++++++++++ .../properties/textures/texturePreview.tsx | 102 +++++++++++++++ 3 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 packages/dev/inspector-v2/src/components/properties/textures/textureHelper.ts create mode 100644 packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx diff --git a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx index 2aa898ff061..2efef012848 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx @@ -14,7 +14,6 @@ import { FileUploadLine } from "shared-ui-components/fluent/hoc/fileUploadLine"; import { BooleanBadgePropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/booleanBadgePropertyLine"; import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine"; import { TextInputPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/inputPropertyLine"; -import { PlaceholderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/propertyLine"; import { StringifiedPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/stringifiedPropertyLine"; import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; import { SyncedSliderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/syncedSliderPropertyLine"; @@ -22,12 +21,16 @@ import { TextPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/ import { useProperty } from "../../../hooks/compoundPropertyHooks"; import { BoundProperty } from "../boundProperty"; import { FindTextureFormat, FindTextureType } from "./textureFormatUtils"; +import { TexturePreview } from "./texturePreview"; +import { useInterceptObservable } from "../../../hooks/instrumentationHooks"; +import { useObservableState } from "../../../hooks/observableHooks"; export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseTexture }> = (props) => { const { texture } = props; const isUpdatable = texture instanceof Texture || texture instanceof CubeTexture; + const textureToSend = isUpdatable ? useObservableState(() => texture, useInterceptObservable("function", texture, "updateURL")) : texture; const updateTexture = useCallback( (file: File) => { ReadFile( @@ -63,7 +66,7 @@ export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseText return ( <> - {}} /> + {/* TODO: This should probably be dynamically fetching a list of supported texture extensions. */} {isUpdatable && ( )} - {}} /> + {}} /> ); }; diff --git a/packages/dev/inspector-v2/src/components/properties/textures/textureHelper.ts b/packages/dev/inspector-v2/src/components/properties/textures/textureHelper.ts new file mode 100644 index 00000000000..ac1ffac3258 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/properties/textures/textureHelper.ts @@ -0,0 +1,123 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { BaseTexture } from "core/Materials/Textures/baseTexture"; +import type { Texture } from "core/Materials/Textures/texture"; +import { TextureTools } from "core/Misc/textureTools"; + +/** + * Defines which channels of the texture to retrieve with {@link TextureHelper.GetTextureDataAsync}. + */ +export interface TextureChannelsToDisplay { + /** + * True if the red channel should be included. + */ + R: boolean; + /** + * True if the green channel should be included. + */ + G: boolean; + /** + * True if the blue channel should be included. + */ + B: boolean; + /** + * True if the alpha channel should be included. + */ + A: boolean; +} + +/** + * Helper class for retrieving texture data. + */ +export class TextureHelper { + /** + * Gets the data of the specified texture by rendering it to an intermediate RGBA texture and retrieving the bytes from it. + * This is convienent to get 8-bit RGBA values for a texture in a GPU compressed format. + * @param texture the source texture + * @param width the width of the result, which does not have to match the source texture width + * @param height the height of the result, which does not have to match the source texture height + * @param face if the texture has multiple faces, the face index to use for the source + * @param channels a filter for which of the RGBA channels to return in the result + * @param lod if the texture has multiple LODs, the lod index to use for the source + * @returns the 8-bit texture data + */ + public static async GetTextureDataAsync( + texture: BaseTexture, + width: number, + height: number, + face: number, + channels: TextureChannelsToDisplay, + lod: number = 0 + ): Promise { + const data = await TextureTools.GetTextureDataAsync(texture, width, height, face, lod); + if (!channels.R || !channels.G || !channels.B || !channels.A) { + for (let i = 0; i < width * height * 4; i += 4) { + // If alpha is the only channel, just display alpha across all channels + if (channels.A && !channels.R && !channels.G && !channels.B) { + data[i] = data[i + 3]; + data[i + 1] = data[i + 3]; + data[i + 2] = data[i + 3]; + data[i + 3] = 255; + continue; + } + let r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + // If alpha is not visible, make everything 100% alpha + if (!channels.A) { + a = 255; + } + // If only one color channel is selected, map both colors to it. If two are selected, the unused one gets set to 0 + if (!channels.R) { + if (channels.G && !channels.B) { + r = g; + } else if (channels.B && !channels.G) { + r = b; + } else { + r = 0; + } + } + if (!channels.G) { + if (channels.R && !channels.B) { + g = r; + } else if (channels.B && !channels.R) { + g = b; + } else { + g = 0; + } + } + if (!channels.B) { + if (channels.R && !channels.G) { + b = r; + } else if (channels.G && !channels.R) { + b = g; + } else { + b = 0; + } + } + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = a; + } + } + + //To flip image on Y axis. + if ((texture as Texture).invertY || texture.isCube) { + const numberOfChannelsByLine = width * 4; + const halfHeight = height / 2; + for (let i = 0; i < halfHeight; i++) { + for (let j = 0; j < numberOfChannelsByLine; j++) { + const currentCell = j + i * numberOfChannelsByLine; + const targetLine = height - i - 1; + const targetCell = j + targetLine * numberOfChannelsByLine; + + const temp = data[currentCell]; + data[currentCell] = data[targetCell]; + data[targetCell] = temp; + } + } + } + return data; + } +} diff --git a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx new file mode 100644 index 00000000000..c6a8072aa70 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx @@ -0,0 +1,102 @@ +import type { BaseTexture } from "core/Materials/Textures/baseTexture"; +import { Button, Toolbar, ToolbarButton, makeStyles, shorthands } from "@fluentui/react-components"; +import { useRef, useState, useEffect } from "react"; +import type { FunctionComponent } from "react"; +import { TextureHelper } from "./textureHelper"; + +const useStyles = makeStyles({ + root: { display: "flex", flexDirection: "column", gap: "8px" }, + controls: { display: "flex", gap: "4px", ...shorthands.padding("4px") }, + preview: { border: "1px solid #ccc", marginTop: "8px", maxWidth: "100%" }, +}); + +const TextureChannelStates = { + R: { R: true, G: false, B: false, A: false }, + G: { R: false, G: true, B: false, A: false }, + B: { R: false, G: false, B: true, A: false }, + A: { R: false, G: false, B: false, A: true }, + ALL: { R: true, G: true, B: true, A: true }, +}; + +type TexturePreviewProps = { + texture: BaseTexture; + width: number; + height: number; + hideChannelSelect?: boolean; +}; + +export const TexturePreview: FunctionComponent = (props) => { + const { texture, width, height, hideChannelSelect } = props; + const classes = useStyles(); + const canvasRef = useRef(null); + const [channels, setChannels] = useState(TextureChannelStates.ALL); + const [face, setFace] = useState(0); + + const updatePreviewAsync = async () => { + const previewCanvas = canvasRef.current!; + const size = texture.getSize(); + const ratio = size.width / size.height; + let w = width; + let h = (w / ratio) | 1; + const engine = texture.getScene()?.getEngine(); + + if (engine && h > engine.getCaps().maxTextureSize) { + w = size.width; + h = size.height; + } + + try { + const data = await TextureHelper.GetTextureDataAsync(texture, w, h, face, channels); + previewCanvas.width = w; + previewCanvas.height = h; + const context = previewCanvas.getContext("2d"); + if (context) { + const imageData = context.createImageData(w, h); + imageData.data.set(data); + context.putImageData(imageData, 0, 0); + } + previewCanvas.style.height = h + "px"; + } catch { + previewCanvas.width = w; + previewCanvas.height = h; + previewCanvas.style.height = h + "px"; + } + }; + useEffect(() => { + void updatePreviewAsync(); + }, [texture, width, height, face, channels]); + + return ( +
+ {!hideChannelSelect && texture.isCube && ( + + {["+X", "-X", "+Y", "-Y", "+Z", "-Z"].map((label, idx) => ( + setFace(idx)}> + {label} + + ))} + + )} + {!hideChannelSelect && !texture.isCube && ( + + {(["R", "G", "B", "A", "ALL"] as const).map((ch) => ( + setChannels(TextureChannelStates[ch])}> + {ch} + + ))} + + )} + + {texture.isRenderTarget && ( + + )} +
+ ); +}; From ac173b85cd7ec8d2b190beb82a77b5fed2ce0729 Mon Sep 17 00:00:00 2001 From: Georgina Halpern Date: Fri, 26 Sep 2025 16:16:48 -0400 Subject: [PATCH 2/6] size not being updated because promise never resolving --- .../textures/baseTextureProperties.tsx | 6 +- .../properties/textures/textureHelper.ts | 154 ++++++++---------- .../properties/textures/texturePreview.tsx | 97 ++++++++--- 3 files changed, 144 insertions(+), 113 deletions(-) diff --git a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx index 2efef012848..8d78780eedd 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx @@ -22,15 +22,13 @@ import { useProperty } from "../../../hooks/compoundPropertyHooks"; import { BoundProperty } from "../boundProperty"; import { FindTextureFormat, FindTextureType } from "./textureFormatUtils"; import { TexturePreview } from "./texturePreview"; -import { useInterceptObservable } from "../../../hooks/instrumentationHooks"; -import { useObservableState } from "../../../hooks/observableHooks"; export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseTexture }> = (props) => { const { texture } = props; const isUpdatable = texture instanceof Texture || texture instanceof CubeTexture; - const textureToSend = isUpdatable ? useObservableState(() => texture, useInterceptObservable("function", texture, "updateURL")) : texture; + const url = isUpdatable ? useProperty(texture, "url") : null; const updateTexture = useCallback( (file: File) => { ReadFile( @@ -66,7 +64,7 @@ export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseText return ( <> - + {/* TODO: This should probably be dynamically fetching a list of supported texture extensions. */} {isUpdatable && ( { - const data = await TextureTools.GetTextureDataAsync(texture, width, height, face, lod); - if (!channels.R || !channels.G || !channels.B || !channels.A) { - for (let i = 0; i < width * height * 4; i += 4) { - // If alpha is the only channel, just display alpha across all channels - if (channels.A && !channels.R && !channels.G && !channels.B) { - data[i] = data[i + 3]; - data[i + 1] = data[i + 3]; - data[i + 2] = data[i + 3]; - data[i + 3] = 255; - continue; - } - let r = data[i], - g = data[i + 1], - b = data[i + 2], - a = data[i + 3]; - // If alpha is not visible, make everything 100% alpha - if (!channels.A) { - a = 255; - } - // If only one color channel is selected, map both colors to it. If two are selected, the unused one gets set to 0 - if (!channels.R) { - if (channels.G && !channels.B) { - r = g; - } else if (channels.B && !channels.G) { - r = b; - } else { - r = 0; - } +export async function ApplyChannelsToTextureDataAsync(texture: BaseTexture, face: number, channels: TextureChannelsToDisplay, lod: number = 0): Promise { + const data = await TextureTools.GetTextureDataAsync(texture, undefined, undefined, face, lod); + const { width, height } = texture.getSize(); + + if (!channels.R || !channels.G || !channels.B || !channels.A) { + for (let i = 0; i < width * height * 4; i += 4) { + // If alpha is the only channel, just display alpha across all channels + if (channels.A && !channels.R && !channels.G && !channels.B) { + data[i] = data[i + 3]; + data[i + 1] = data[i + 3]; + data[i + 2] = data[i + 3]; + data[i + 3] = 255; + continue; + } + let r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + // If alpha is not visible, make everything 100% alpha + if (!channels.A) { + a = 255; + } + // If only one color channel is selected, map both colors to it. If two are selected, the unused one gets set to 0 + if (!channels.R) { + if (channels.G && !channels.B) { + r = g; + } else if (channels.B && !channels.G) { + r = b; + } else { + r = 0; } - if (!channels.G) { - if (channels.R && !channels.B) { - g = r; - } else if (channels.B && !channels.R) { - g = b; - } else { - g = 0; - } + } + if (!channels.G) { + if (channels.R && !channels.B) { + g = r; + } else if (channels.B && !channels.R) { + g = b; + } else { + g = 0; } - if (!channels.B) { - if (channels.R && !channels.G) { - b = r; - } else if (channels.G && !channels.R) { - b = g; - } else { - b = 0; - } + } + if (!channels.B) { + if (channels.R && !channels.G) { + b = r; + } else if (channels.G && !channels.R) { + b = g; + } else { + b = 0; } - data[i] = r; - data[i + 1] = g; - data[i + 2] = b; - data[i + 3] = a; } + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = a; } + } - //To flip image on Y axis. - if ((texture as Texture).invertY || texture.isCube) { - const numberOfChannelsByLine = width * 4; - const halfHeight = height / 2; - for (let i = 0; i < halfHeight; i++) { - for (let j = 0; j < numberOfChannelsByLine; j++) { - const currentCell = j + i * numberOfChannelsByLine; - const targetLine = height - i - 1; - const targetCell = j + targetLine * numberOfChannelsByLine; + //To flip image on Y axis. + if ((texture as Texture).invertY || texture.isCube) { + const numberOfChannelsByLine = width * 4; + const halfHeight = height / 2; + for (let i = 0; i < halfHeight; i++) { + for (let j = 0; j < numberOfChannelsByLine; j++) { + const currentCell = j + i * numberOfChannelsByLine; + const targetLine = height - i - 1; + const targetCell = j + targetLine * numberOfChannelsByLine; - const temp = data[currentCell]; - data[currentCell] = data[targetCell]; - data[targetCell] = temp; - } + const temp = data[currentCell]; + data[currentCell] = data[targetCell]; + data[targetCell] = temp; } } - return data; } + return data; } diff --git a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx index c6a8072aa70..be0ccd4710b 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx @@ -1,13 +1,35 @@ import type { BaseTexture } from "core/Materials/Textures/baseTexture"; import { Button, Toolbar, ToolbarButton, makeStyles, shorthands } from "@fluentui/react-components"; -import { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect, useCallback } from "react"; import type { FunctionComponent } from "react"; -import { TextureHelper } from "./textureHelper"; +import { ApplyChannelsToTextureDataAsync } from "./textureHelper"; +import type { Nullable } from "core/types"; const useStyles = makeStyles({ root: { display: "flex", flexDirection: "column", gap: "8px" }, - controls: { display: "flex", gap: "4px", ...shorthands.padding("4px") }, - preview: { border: "1px solid #ccc", marginTop: "8px", maxWidth: "100%" }, + controls: { + display: "flex", + gap: "2px", + ...shorthands.padding("2px"), + width: "100%", + justifyContent: "center", // Center the buttons + }, + controlButton: { + minWidth: "auto", + flex: "1 1 0", // Equal flex grow/shrink with 0 basis + ...shorthands.padding("4px", "8px"), // Reasonable padding + fontSize: "inherit", // Use default font size + overflow: "hidden", // Prevent text overflow + textOverflow: "ellipsis", // Add ellipsis if needed + }, + preview: { + border: "1px solid #ccc", + marginTop: "8px", + maxWidth: "100%", + marginLeft: "auto", // Center horizontally + marginRight: "auto", // Center horizontally + display: "block", // Ensure it's a block element + }, }); const TextureChannelStates = { @@ -20,58 +42,76 @@ const TextureChannelStates = { type TexturePreviewProps = { texture: BaseTexture; + url: Nullable; width: number; height: number; hideChannelSelect?: boolean; }; export const TexturePreview: FunctionComponent = (props) => { - const { texture, width, height, hideChannelSelect } = props; + const { texture, width, height, hideChannelSelect, url } = props; const classes = useStyles(); const canvasRef = useRef(null); const [channels, setChannels] = useState(TextureChannelStates.ALL); const [face, setFace] = useState(0); - const updatePreviewAsync = async () => { - const previewCanvas = canvasRef.current!; - const size = texture.getSize(); - const ratio = size.width / size.height; - let w = width; - let h = (w / ratio) | 1; - const engine = texture.getScene()?.getEngine(); + const updatePreviewCanvasSize = useCallback( + (previewCanvas: HTMLCanvasElement) => { + const size = texture.getSize(); + const ratio = size.width / size.height; + let w = width; + let h = (w / ratio) | 1; + const engine = texture.getScene()?.getEngine(); - if (engine && h > engine.getCaps().maxTextureSize) { - w = size.width; - h = size.height; - } + if (engine && h > engine.getCaps().maxTextureSize) { + w = size.width; + h = size.height; + } - try { - const data = await TextureHelper.GetTextureDataAsync(texture, w, h, face, channels); previewCanvas.width = w; previewCanvas.height = h; + previewCanvas.style.width = w + "px"; + previewCanvas.style.height = h + "px"; + + return { + w: w, + h: h, + }; + }, + [canvasRef.current, texture, width, height, url] + ); + + const updatePreviewAsync = useCallback(async () => { + if (!canvasRef.current) { + return; + } + const previewCanvas = canvasRef.current!; + try { + const data = await ApplyChannelsToTextureDataAsync(texture, face, channels); + // @alex we are never reaching this line, the internal promise never resolves + const { w, h } = updatePreviewCanvasSize(previewCanvas); + const context = previewCanvas.getContext("2d"); if (context) { const imageData = context.createImageData(w, h); imageData.data.set(data); context.putImageData(imageData, 0, 0); } - previewCanvas.style.height = h + "px"; } catch { - previewCanvas.width = w; - previewCanvas.height = h; - previewCanvas.style.height = h + "px"; + updatePreviewCanvasSize(previewCanvas); } - }; + }, [[texture, width, height, face, channels, url]]); + useEffect(() => { void updatePreviewAsync(); - }, [texture, width, height, face, channels]); + }, [texture, width, height, face, channels, url]); return (
{!hideChannelSelect && texture.isCube && ( {["+X", "-X", "+Y", "-Y", "+Z", "-Z"].map((label, idx) => ( - setFace(idx)}> + setFace(idx)}> {label} ))} @@ -80,7 +120,12 @@ export const TexturePreview: FunctionComponent = (props) => {!hideChannelSelect && !texture.isCube && ( {(["R", "G", "B", "A", "ALL"] as const).map((ch) => ( - setChannels(TextureChannelStates[ch])}> + setChannels(TextureChannelStates[ch])} + > {ch} ))} From 613e2930c6f7a8a12ef14b0a1c42a23b3c498773 Mon Sep 17 00:00:00 2001 From: Georgina Halpern Date: Fri, 3 Oct 2025 17:55:51 -0400 Subject: [PATCH 3/6] working texture preview --- .../textures/baseTextureProperties.tsx | 3 +- .../properties/textures/textureHelper.ts | 111 ------------ .../properties/textures/texturePreview.tsx | 165 +++++++++++++++--- 3 files changed, 141 insertions(+), 138 deletions(-) delete mode 100644 packages/dev/inspector-v2/src/components/properties/textures/textureHelper.ts diff --git a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx index 8d78780eedd..e011483e8eb 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx @@ -28,7 +28,6 @@ export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseText const isUpdatable = texture instanceof Texture || texture instanceof CubeTexture; - const url = isUpdatable ? useProperty(texture, "url") : null; const updateTexture = useCallback( (file: File) => { ReadFile( @@ -64,7 +63,7 @@ export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseText return ( <> - + {/* TODO: This should probably be dynamically fetching a list of supported texture extensions. */} {isUpdatable && ( { - const data = await TextureTools.GetTextureDataAsync(texture, undefined, undefined, face, lod); - const { width, height } = texture.getSize(); - - if (!channels.R || !channels.G || !channels.B || !channels.A) { - for (let i = 0; i < width * height * 4; i += 4) { - // If alpha is the only channel, just display alpha across all channels - if (channels.A && !channels.R && !channels.G && !channels.B) { - data[i] = data[i + 3]; - data[i + 1] = data[i + 3]; - data[i + 2] = data[i + 3]; - data[i + 3] = 255; - continue; - } - let r = data[i], - g = data[i + 1], - b = data[i + 2], - a = data[i + 3]; - // If alpha is not visible, make everything 100% alpha - if (!channels.A) { - a = 255; - } - // If only one color channel is selected, map both colors to it. If two are selected, the unused one gets set to 0 - if (!channels.R) { - if (channels.G && !channels.B) { - r = g; - } else if (channels.B && !channels.G) { - r = b; - } else { - r = 0; - } - } - if (!channels.G) { - if (channels.R && !channels.B) { - g = r; - } else if (channels.B && !channels.R) { - g = b; - } else { - g = 0; - } - } - if (!channels.B) { - if (channels.R && !channels.G) { - b = r; - } else if (channels.G && !channels.R) { - b = g; - } else { - b = 0; - } - } - data[i] = r; - data[i + 1] = g; - data[i + 2] = b; - data[i + 3] = a; - } - } - - //To flip image on Y axis. - if ((texture as Texture).invertY || texture.isCube) { - const numberOfChannelsByLine = width * 4; - const halfHeight = height / 2; - for (let i = 0; i < halfHeight; i++) { - for (let j = 0; j < numberOfChannelsByLine; j++) { - const currentCell = j + i * numberOfChannelsByLine; - const targetLine = height - i - 1; - const targetCell = j + targetLine * numberOfChannelsByLine; - - const temp = data[currentCell]; - data[currentCell] = data[targetCell]; - data[targetCell] = temp; - } - } - } - return data; -} diff --git a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx index be0ccd4710b..47707e5a305 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx @@ -1,37 +1,39 @@ import type { BaseTexture } from "core/Materials/Textures/baseTexture"; -import { Button, Toolbar, ToolbarButton, makeStyles, shorthands } from "@fluentui/react-components"; +import { Button, Toolbar, ToolbarButton, makeStyles } from "@fluentui/react-components"; import { useRef, useState, useEffect, useCallback } from "react"; import type { FunctionComponent } from "react"; -import { ApplyChannelsToTextureDataAsync } from "./textureHelper"; -import type { Nullable } from "core/types"; +import { GetTextureDataAsync, WhenTextureReadyAsync } from "core/Misc/textureTools"; +import { useProperty } from "../../../hooks/compoundPropertyHooks"; +import type { Texture } from "core/Materials"; const useStyles = makeStyles({ root: { display: "flex", flexDirection: "column", gap: "8px" }, controls: { display: "flex", gap: "2px", - ...shorthands.padding("2px"), + padding: "2px", width: "100%", - justifyContent: "center", // Center the buttons + justifyContent: "center", }, controlButton: { minWidth: "auto", flex: "1 1 0", // Equal flex grow/shrink with 0 basis - ...shorthands.padding("4px", "8px"), // Reasonable padding - fontSize: "inherit", // Use default font size - overflow: "hidden", // Prevent text overflow - textOverflow: "ellipsis", // Add ellipsis if needed + paddingVertical: "4px", + paddingHorizontal: "8px", + overflow: "hidden", + textOverflow: "ellipsis", }, preview: { border: "1px solid #ccc", marginTop: "8px", maxWidth: "100%", - marginLeft: "auto", // Center horizontally - marginRight: "auto", // Center horizontally - display: "block", // Ensure it's a block element + marginLeft: "auto", + marginRight: "auto", + display: "block", }, }); +// This method of holding TextureChannels was brought over from inspectorv1 and can likely be refactored/simplified const TextureChannelStates = { R: { R: true, G: false, B: false, A: false }, G: { R: false, G: true, B: false, A: false }, @@ -42,21 +44,21 @@ const TextureChannelStates = { type TexturePreviewProps = { texture: BaseTexture; - url: Nullable; width: number; height: number; - hideChannelSelect?: boolean; }; export const TexturePreview: FunctionComponent = (props) => { - const { texture, width, height, hideChannelSelect, url } = props; + const { texture, width, height } = props; const classes = useStyles(); const canvasRef = useRef(null); const [channels, setChannels] = useState(TextureChannelStates.ALL); const [face, setFace] = useState(0); + const internalTexture = useProperty(texture, "_texture"); const updatePreviewCanvasSize = useCallback( (previewCanvas: HTMLCanvasElement) => { + // This logic was brought over from inspectorv1 and can likely be refactored/simplified const size = texture.getSize(); const ratio = size.width / size.height; let w = width; @@ -78,7 +80,7 @@ export const TexturePreview: FunctionComponent = (props) => h: h, }; }, - [canvasRef.current, texture, width, height, url] + [canvasRef.current, texture, width, height, internalTexture] ); const updatePreviewAsync = useCallback(async () => { @@ -87,10 +89,9 @@ export const TexturePreview: FunctionComponent = (props) => } const previewCanvas = canvasRef.current!; try { - const data = await ApplyChannelsToTextureDataAsync(texture, face, channels); - // @alex we are never reaching this line, the internal promise never resolves - const { w, h } = updatePreviewCanvasSize(previewCanvas); - + await WhenTextureReadyAsync(texture); // Ensure texture is loaded before grabbing size + const { w, h } = updatePreviewCanvasSize(previewCanvas); // Grab desired size + const data = await ApplyChannelsToTextureDataAsync(texture, w, h, face, channels); // get channel data to load onto canvas context const context = previewCanvas.getContext("2d"); if (context) { const imageData = context.createImageData(w, h); @@ -100,15 +101,15 @@ export const TexturePreview: FunctionComponent = (props) => } catch { updatePreviewCanvasSize(previewCanvas); } - }, [[texture, width, height, face, channels, url]]); + }, [[texture, width, height, face, channels, internalTexture]]); useEffect(() => { void updatePreviewAsync(); - }, [texture, width, height, face, channels, url]); + }, [texture, width, height, face, channels, internalTexture]); return (
- {!hideChannelSelect && texture.isCube && ( + {texture.isCube ? ( {["+X", "-X", "+Y", "-Y", "+Z", "-Z"].map((label, idx) => ( setFace(idx)}> @@ -116,8 +117,7 @@ export const TexturePreview: FunctionComponent = (props) => ))} - )} - {!hideChannelSelect && !texture.isCube && ( + ) : ( {(["R", "G", "B", "A", "ALL"] as const).map((ch) => ( = (props) =>
); }; + +/** + * Defines which channels of the texture to retrieve with {@link TextureHelper.GetTextureDataAsync}. + */ +type TextureChannelsToDisplay = { + /** + * True if the red channel should be included. + */ + R: boolean; + /** + * True if the green channel should be included. + */ + G: boolean; + /** + * True if the blue channel should be included. + */ + B: boolean; + /** + * True if the alpha channel should be included. + */ + A: boolean; +}; + +/** + * Gets the data of the specified texture by rendering it to an intermediate RGBA texture and retrieving the bytes from it. + * This is convienent to get 8-bit RGBA values for a texture in a GPU compressed format. + * @param texture the source texture + * @param width the width of the result, which does not have to match the source texture width + * @param height the height of the result, which does not have to match the source texture height + * @param face if the texture has multiple faces, the face index to use for the source + * @param channels a filter for which of the RGBA channels to return in the result + * @param lod if the texture has multiple LODs, the lod index to use for the source + * @returns the 8-bit texture data + */ +async function ApplyChannelsToTextureDataAsync( + texture: BaseTexture, + width: number, + height: number, + face: number, + channels: TextureChannelsToDisplay, + lod: number = 0 +): Promise { + const data = await GetTextureDataAsync(texture, width, height, face, lod); + + if (!channels.R || !channels.G || !channels.B || !channels.A) { + for (let i = 0; i < width * height * 4; i += 4) { + // If alpha is the only channel, just display alpha across all channels + if (channels.A && !channels.R && !channels.G && !channels.B) { + data[i] = data[i + 3]; + data[i + 1] = data[i + 3]; + data[i + 2] = data[i + 3]; + data[i + 3] = 255; + continue; + } + let r = data[i], + g = data[i + 1], + b = data[i + 2], + a = data[i + 3]; + // If alpha is not visible, make everything 100% alpha + if (!channels.A) { + a = 255; + } + // If only one color channel is selected, map both colors to it. If two are selected, the unused one gets set to 0 + if (!channels.R) { + if (channels.G && !channels.B) { + r = g; + } else if (channels.B && !channels.G) { + r = b; + } else { + r = 0; + } + } + if (!channels.G) { + if (channels.R && !channels.B) { + g = r; + } else if (channels.B && !channels.R) { + g = b; + } else { + g = 0; + } + } + if (!channels.B) { + if (channels.R && !channels.G) { + b = r; + } else if (channels.G && !channels.R) { + b = g; + } else { + b = 0; + } + } + data[i] = r; + data[i + 1] = g; + data[i + 2] = b; + data[i + 3] = a; + } + } + + //To flip image on Y axis. + if ((texture as Texture).invertY || texture.isCube) { + const numberOfChannelsByLine = width * 4; + const halfHeight = height / 2; + for (let i = 0; i < halfHeight; i++) { + for (let j = 0; j < numberOfChannelsByLine; j++) { + const currentCell = j + i * numberOfChannelsByLine; + const targetLine = height - i - 1; + const targetCell = j + targetLine * numberOfChannelsByLine; + + const temp = data[currentCell]; + data[currentCell] = data[targetCell]; + data[targetCell] = temp; + } + } + } + return data; +} From 4b0d9e719ace9761bef897dd8cdbc3ccfba427fa Mon Sep 17 00:00:00 2001 From: Georgie <72631815+georginahalpern@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:41:31 -0400 Subject: [PATCH 4/6] Update packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx Co-authored-by: Ryan Tremblay --- .../src/components/properties/textures/texturePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx index 47707e5a305..cd28823a3f0 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx @@ -40,7 +40,7 @@ const TextureChannelStates = { B: { R: false, G: false, B: true, A: false }, A: { R: false, G: false, B: false, A: true }, ALL: { R: true, G: true, B: true, A: true }, -}; +} as const; type TexturePreviewProps = { texture: BaseTexture; From e411ec8af2fba0e5d268b15492c44205c41d84c8 Mon Sep 17 00:00:00 2001 From: Georgie <72631815+georginahalpern@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:41:46 -0400 Subject: [PATCH 5/6] Update packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx Co-authored-by: Ryan Tremblay --- .../src/components/properties/textures/texturePreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx index cd28823a3f0..b8ed0e709fa 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx @@ -84,10 +84,10 @@ export const TexturePreview: FunctionComponent = (props) => ); const updatePreviewAsync = useCallback(async () => { - if (!canvasRef.current) { + const previewCanvas = canvasRef.current; + if (!previewCanvas) { return; } - const previewCanvas = canvasRef.current!; try { await WhenTextureReadyAsync(texture); // Ensure texture is loaded before grabbing size const { w, h } = updatePreviewCanvasSize(previewCanvas); // Grab desired size From 7ce6e6a719a72eeefe5634cc7656ba90ee1cf9f3 Mon Sep 17 00:00:00 2001 From: Georgina Halpern Date: Fri, 3 Oct 2025 20:50:32 -0400 Subject: [PATCH 6/6] pr cmments --- .../src/components/properties/textures/texturePreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx index b8ed0e709fa..f9ef3b6c99b 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/texturePreview.tsx @@ -40,7 +40,7 @@ const TextureChannelStates = { B: { R: false, G: false, B: true, A: false }, A: { R: false, G: false, B: false, A: true }, ALL: { R: true, G: true, B: true, A: true }, -} as const; +}; type TexturePreviewProps = { texture: BaseTexture; @@ -99,7 +99,7 @@ export const TexturePreview: FunctionComponent = (props) => context.putImageData(imageData, 0, 0); } } catch { - updatePreviewCanvasSize(previewCanvas); + updatePreviewCanvasSize(previewCanvas); // If we fail above, best effort sizing preview canvas } }, [[texture, width, height, face, channels, internalTexture]]);