Parcourir la source

Texture inspector (#8529)

* texture inspector functionality

* rename edit to view

* clean up unused code

* move up popup component & update whatsnew

* use snippet server for material

* fix keyboard events

* focus canvas on load

* focus canvas on load

* remove floodfill

* painting

* rudimentary tool loading

* migrate tool state to react component

* remove hardcoded paint tool

* add metadata support

* switch to parameters object

* load multiple tools

* remove tools

* change wording to focus on viewing functionality
DarraghBurkeMS il y a 5 ans
Parent
commit
948abe8530

+ 1 - 1
dist/preview release/what's new.md

@@ -68,7 +68,7 @@
 - Added support for recording GIF ([Deltakosh](https://github.com/deltakosh))
 - Popup Window available (To be used in Curve Editor) ([pixelspace](https://github.com/devpixelspace))
 - Add support to update inspector when switching to a new scene ([belfortk](https://github.com/belfortk))
-- View textures in pop out window with zoom & pan ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
+- View textures in pop out window with zoom & pan and individual channels. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
 
 ### Cameras
 

+ 274 - 274
inspector/src/components/actionTabs/tabs/propertyGrids/materials/texturePropertyGridComponent.tsx

@@ -1,275 +1,275 @@
-import * as React from "react";
-
-import { Nullable } from "babylonjs/types";
-import { Tools } from "babylonjs/Misc/tools";
-import { Observable } from "babylonjs/Misc/observable";
-import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
-import { Texture } from "babylonjs/Materials/Textures/texture";
-import { CubeTexture } from "babylonjs/Materials/Textures/cubeTexture";
-
-import { PropertyChangedEvent } from "../../../../propertyChangedEvent";
-import { LineContainerComponent } from "../../../lineContainerComponent";
-import { SliderLineComponent } from "../../../lines/sliderLineComponent";
-import { TextLineComponent } from "../../../lines/textLineComponent";
-import { CheckBoxLineComponent } from "../../../lines/checkBoxLineComponent";
-import { TextureLineComponent } from "../../../lines/textureLineComponent";
-import { FloatLineComponent } from "../../../lines/floatLineComponent";
-import { OptionsLineComponent } from "../../../lines/optionsLineComponent";
-import { FileButtonLineComponent } from "../../../lines/fileButtonLineComponent";
-import { LockObject } from "../lockObject";
-import { ValueLineComponent } from "../../../lines/valueLineComponent";
-import { GlobalState } from "../../../../../components/globalState";
-
-import { AdvancedDynamicTextureInstrumentation } from "babylonjs-gui/2D/adtInstrumentation";
-import { AdvancedDynamicTexture } from "babylonjs-gui/2D/advancedDynamicTexture";
-import { CustomPropertyGridComponent } from '../customPropertyGridComponent';
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
-import { AnimationGridComponent } from '../animations/animationPropertyGridComponent';
-
-import { Engine } from 'babylonjs/Engines/engine';
-import { PopupComponent } from '../../../../popupComponent';
-import { TextureEditorComponent } from './textures/textureEditorComponent';
-
-interface ITexturePropertyGridComponentProps {
-    texture: BaseTexture,
-    lockObject: LockObject,
-    globalState: GlobalState,
-    onPropertyChangedObservable?: Observable<PropertyChangedEvent>
-}
-
-export class TexturePropertyGridComponent extends React.Component<ITexturePropertyGridComponentProps> {
-
-    private _adtInstrumentation: Nullable<AdvancedDynamicTextureInstrumentation>;
-    private textureLineRef: React.RefObject<TextureLineComponent>;
-
-    private _isTextureEditorOpen = false;
-    
-
-    constructor(props: ITexturePropertyGridComponentProps) {
-        super(props);
-
-        const texture = this.props.texture;
-
-        this.textureLineRef = React.createRef();
-
-        if (!texture || !(texture as any).rootContainer) {
-            return;
-        }
-
-        const adt = texture as AdvancedDynamicTexture;
-
-        this._adtInstrumentation = new AdvancedDynamicTextureInstrumentation(adt);
-        this._adtInstrumentation!.captureRenderTime = true;
-        this._adtInstrumentation!.captureLayoutTime = true;
-    }
-
-    componentWillUnmount() {
-        if (this._adtInstrumentation) {
-            this._adtInstrumentation.dispose();
-            this._adtInstrumentation = null;
-        }
-    }
-
-    updateTexture(file: File) {
-        const texture = this.props.texture;
-        Tools.ReadFile(file, (data) => {
-            var blob = new Blob([data], { type: "octet/stream" });
-
-            var reader = new FileReader();
-            reader.readAsDataURL(blob); 
-            reader.onloadend = () => {
-                let base64data = reader.result as string;     
-
-                if (texture.isCube) {
-                    let extension: string | undefined = undefined;
-                    if (file.name.toLowerCase().indexOf(".dds") > 0) {
-                        extension = ".dds";
-                    } else if (file.name.toLowerCase().indexOf(".env") > 0) {
-                        extension = ".env";
-                    }
-
-                    (texture as CubeTexture).updateURL(base64data, extension, () => this.forceRefresh());
-                } else {
-                    (texture as Texture).updateURL(base64data, null, () => this.forceRefresh());
-                }
-            };
-
-        }, undefined, true);
-    }
-
-    onOpenTextureEditor() {
-        this._isTextureEditorOpen = true;
-    }
-    
-    onCloseTextureEditor(window: Window | null) {
-        this._isTextureEditorOpen = false;
-        if (window !== null) {
-            window.close();
-        }
-    }
-
-    forceRefresh() {
-        this.forceUpdate();
-        (this.textureLineRef.current as TextureLineComponent).updatePreview();
-    }
-
-    render() {
-        const texture = this.props.texture;
-
-        var samplingMode = [
-            { label: "Nearest", value: Texture.NEAREST_NEAREST },
-            { label: "Nearest & linear mip", value: Texture.NEAREST_LINEAR },
-            { label: "Linear", value: Texture.LINEAR_LINEAR_MIPLINEAR },
-        ];
-
-        var coordinatesMode = [
-            { label: "Explicit", value: Texture.EXPLICIT_MODE },
-            { label: "Cubic", value: Texture.CUBIC_MODE },
-            { label: "Inverse cubic", value: Texture.INVCUBIC_MODE },
-            { label: "Equirectangular", value: Texture.EQUIRECTANGULAR_MODE },
-            { label: "Fixed equirectangular", value: Texture.FIXED_EQUIRECTANGULAR_MODE },
-            { label: "Fixed equirectangular mirrored", value: Texture.FIXED_EQUIRECTANGULAR_MIRRORED_MODE },
-            { label: "Planar", value: Texture.PLANAR_MODE },
-            { label: "Projection", value: Texture.PROJECTION_MODE },
-            { label: "Skybox", value: Texture.SKYBOX_MODE },
-            { label: "Spherical", value: Texture.SPHERICAL_MODE },
-        ];
-
-        let extension = "";
-        let url = (texture as Texture).url;
-        let textureUrl = (!url || url.substring(0, 4) === "data" || url.substring(0, 4) === "blob") ? "" : url;
-
-        if (textureUrl) {
-            for (var index = textureUrl.length - 1; index >= 0; index--) {
-                if (textureUrl[index] === ".") {
-                    break;
-                }
-                extension = textureUrl[index] + extension;
-            }
-        }
-
-        const editable = texture.textureType != Engine.TEXTURETYPE_FLOAT && texture.textureType != Engine.TEXTURETYPE_FLOAT_32_UNSIGNED_INT_24_8_REV && texture.textureType !== Engine.TEXTURETYPE_HALF_FLOAT;
-
-        return (
-            <div className="pane">
-                <LineContainerComponent globalState={this.props.globalState} title="PREVIEW">
-                    <TextureLineComponent ref={this.textureLineRef} texture={texture} width={256} height={256} globalState={this.props.globalState} />
-                    <FileButtonLineComponent label="Load texture from file" onClick={(file) => this.updateTexture(file)} accept=".jpg, .png, .tga, .dds, .env" />
-                    {editable &&
-                        <ButtonLineComponent label="View" onClick={() => this.onOpenTextureEditor()} />
-                    }
-                    <TextInputLineComponent label="URL" value={textureUrl} lockObject={this.props.lockObject} onChange={url => {
-                        (texture as Texture).updateURL(url);
-                        this.forceRefresh();
-                    }} />
-                </LineContainerComponent>
-                {this._isTextureEditorOpen && (
-                <PopupComponent
-                  id='texture-editor'
-                  title='Texture Editor'
-                  size={{ width: 1024, height: 490 }}
-                  onOpen={(window: Window) => {}}
-                  onClose={(window: Window) =>
-                    this.onCloseTextureEditor(window)
-                  }
-                >
-                    <TextureEditorComponent
-                        globalState={this.props.globalState}
-                        texture={this.props.texture}
-                    />
-                </PopupComponent>)}
-                <CustomPropertyGridComponent globalState={this.props.globalState} target={texture}
-                    lockObject={this.props.lockObject}
-                    onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                <LineContainerComponent globalState={this.props.globalState} title="GENERAL">
-                    <TextLineComponent label="Width" value={texture.getSize().width.toString()} />
-                    <TextLineComponent label="Height" value={texture.getSize().height.toString()} />
-                    {
-                        texture.isRenderTarget &&
-                        <ButtonLineComponent label="Scale up" onClick={() => {
-                            let scene = texture.getScene()!;
-                            texture.scale(2);
-                            setTimeout(() => {
-                                this.props.globalState.onSelectionChangedObservable.notifyObservers(scene.getTextureByUniqueID(texture.uniqueId));
-                            });
-                        }} />
-                    }
-                    {
-                        texture.isRenderTarget &&
-                        <ButtonLineComponent label="Scale down" onClick={() => {                            
-                            let scene = texture.getScene()!;
-                            texture.scale(0.5);
-                            setTimeout(() => {
-                                this.props.globalState.onSelectionChangedObservable.notifyObservers(scene.getTextureByUniqueID(texture.uniqueId));
-                            });
-                        }} />
-                    }
-                    {
-                        extension &&
-                        <TextLineComponent label="File format" value={extension} />
-                    }
-                    <TextLineComponent label="Unique ID" value={texture.uniqueId.toString()} />
-                    <TextLineComponent label="Class" value={texture.getClassName()} />
-                    <TextLineComponent label="Has alpha" value={texture.hasAlpha ? "Yes" : "No"} />
-                    <TextLineComponent label="Is 3D" value={texture.is3D ? "Yes" : "No"} />
-                    <TextLineComponent label="Is 2D array" value={texture.is2DArray ? "Yes" : "No"} />
-                    <TextLineComponent label="Is cube" value={texture.isCube ? "Yes" : "No"} />
-                    <TextLineComponent label="Is render target" value={texture.isRenderTarget ? "Yes" : "No"} />
-                    {
-                        (texture instanceof Texture) && 
-                        <TextLineComponent label="Stored as inverted on Y" value={texture.invertY ? "Yes" : "No"} />
-                    }
-                    <TextLineComponent label="Has mipmaps" value={!texture.noMipmap ? "Yes" : "No"} />
-                    <SliderLineComponent label="UV set" target={texture} propertyName="coordinatesIndex" minimum={0} maximum={3} step={1} onPropertyChangedObservable={this.props.onPropertyChangedObservable} decimalCount={0} />
-                    <OptionsLineComponent label="Mode" options={coordinatesMode} target={texture} propertyName="coordinatesMode" onPropertyChangedObservable={this.props.onPropertyChangedObservable} onSelect={(value) => texture.updateSamplingMode(value)} />
-                    <SliderLineComponent label="Level" target={texture} propertyName="level" minimum={0} maximum={2} step={0.01} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                    {
-                        texture.updateSamplingMode &&
-                        <OptionsLineComponent label="Sampling" options={samplingMode} target={texture} noDirectUpdate={true} propertyName="samplingMode" onPropertyChangedObservable={this.props.onPropertyChangedObservable} onSelect={(value) => texture.updateSamplingMode(value)} />
-                    }
-                </LineContainerComponent>
-                {
-                    texture.getScene() &&
-                    <AnimationGridComponent globalState={this.props.globalState} animatable={texture} scene={texture.getScene()!} lockObject={this.props.lockObject} />
-                }
-                {
-                    (texture as any).rootContainer &&
-                    <LineContainerComponent globalState={this.props.globalState} title="ADVANCED TEXTURE PROPERTIES">
-                        <ValueLineComponent label="Last layout time" value={this._adtInstrumentation!.renderTimeCounter.current} units="ms" />
-                        <ValueLineComponent label="Last render time" value={this._adtInstrumentation!.layoutTimeCounter.current} units="ms" />
-                        <SliderLineComponent label="Render scale" minimum={0.1} maximum={5} step={0.1} target={texture} propertyName="renderScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                        <CheckBoxLineComponent label="Premultiply alpha" target={texture} propertyName="premulAlpha" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                        <FloatLineComponent lockObject={this.props.lockObject} label="Ideal width" target={texture} propertyName="idealWidth" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                        <FloatLineComponent lockObject={this.props.lockObject} label="Ideal height" target={texture} propertyName="idealHeight" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                        <CheckBoxLineComponent label="Use smallest ideal" target={texture} propertyName="useSmallestIdeal" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                        <CheckBoxLineComponent label="Render at ideal size" target={texture} propertyName="renderAtIdealSize" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                        <CheckBoxLineComponent label="Invalidate Rect optimization" target={texture} propertyName="useInvalidateRectOptimization" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                    </LineContainerComponent>
-                }
-                <LineContainerComponent globalState={this.props.globalState} title="TRANSFORM">
-                    {
-                        !texture.isCube &&
-                        <div>
-                            <FloatLineComponent lockObject={this.props.lockObject} label="U offset" target={texture} propertyName="uOffset" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <FloatLineComponent lockObject={this.props.lockObject} label="V offset" target={texture} propertyName="vOffset" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <FloatLineComponent lockObject={this.props.lockObject} label="U scale" target={texture} propertyName="uScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <FloatLineComponent lockObject={this.props.lockObject} label="V scale" target={texture} propertyName="vScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <FloatLineComponent lockObject={this.props.lockObject} label="U angle" useEuler={this.props.globalState.onlyUseEulers} target={texture} propertyName="uAng" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <FloatLineComponent lockObject={this.props.lockObject} label="V angle" useEuler={this.props.globalState.onlyUseEulers} target={texture} propertyName="vAng" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <FloatLineComponent lockObject={this.props.lockObject} label="W angle" useEuler={this.props.globalState.onlyUseEulers} target={texture} propertyName="wAng" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                            <CheckBoxLineComponent label="Clamp U" isSelected={() => texture.wrapU === Texture.CLAMP_ADDRESSMODE} onSelect={(value) => texture.wrapU = value ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE} />
-                            <CheckBoxLineComponent label="Clamp V" isSelected={() => texture.wrapV === Texture.CLAMP_ADDRESSMODE} onSelect={(value) => texture.wrapV = value ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE} />
-                        </div>
-                    }
-                    {
-                        texture.isCube &&
-                        <div>
-                            <SliderLineComponent label="Rotation Y" useEuler={this.props.globalState.onlyUseEulers} minimum={0} maximum={2 * Math.PI} step={0.1} target={texture} propertyName="rotationY" />
-                        </div>
-                    }
-                </LineContainerComponent>
-            </div>
-        );
-    }
+import * as React from "react";
+
+import { Nullable } from "babylonjs/types";
+import { Tools } from "babylonjs/Misc/tools";
+import { Observable } from "babylonjs/Misc/observable";
+import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
+import { Texture } from "babylonjs/Materials/Textures/texture";
+import { CubeTexture } from "babylonjs/Materials/Textures/cubeTexture";
+
+import { PropertyChangedEvent } from "../../../../propertyChangedEvent";
+import { LineContainerComponent } from "../../../lineContainerComponent";
+import { SliderLineComponent } from "../../../lines/sliderLineComponent";
+import { TextLineComponent } from "../../../lines/textLineComponent";
+import { CheckBoxLineComponent } from "../../../lines/checkBoxLineComponent";
+import { TextureLineComponent } from "../../../lines/textureLineComponent";
+import { FloatLineComponent } from "../../../lines/floatLineComponent";
+import { OptionsLineComponent } from "../../../lines/optionsLineComponent";
+import { FileButtonLineComponent } from "../../../lines/fileButtonLineComponent";
+import { LockObject } from "../lockObject";
+import { ValueLineComponent } from "../../../lines/valueLineComponent";
+import { GlobalState } from "../../../../../components/globalState";
+
+import { AdvancedDynamicTextureInstrumentation } from "babylonjs-gui/2D/adtInstrumentation";
+import { AdvancedDynamicTexture } from "babylonjs-gui/2D/advancedDynamicTexture";
+import { CustomPropertyGridComponent } from '../customPropertyGridComponent';
+import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
+import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
+import { AnimationGridComponent } from '../animations/animationPropertyGridComponent';
+
+import { Engine } from 'babylonjs/Engines/engine';
+import { PopupComponent } from '../../../../popupComponent';
+import { TextureEditorComponent } from './textures/textureEditorComponent';
+
+interface ITexturePropertyGridComponentProps {
+    texture: BaseTexture,
+    lockObject: LockObject,
+    globalState: GlobalState,
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>
+}
+
+export class TexturePropertyGridComponent extends React.Component<ITexturePropertyGridComponentProps> {
+
+    private _adtInstrumentation: Nullable<AdvancedDynamicTextureInstrumentation>;
+    private textureLineRef: React.RefObject<TextureLineComponent>;
+
+    private _isTextureEditorOpen = false;
+    
+
+    constructor(props: ITexturePropertyGridComponentProps) {
+        super(props);
+
+        const texture = this.props.texture;
+
+        this.textureLineRef = React.createRef();
+
+        if (!texture || !(texture as any).rootContainer) {
+            return;
+        }
+
+        const adt = texture as AdvancedDynamicTexture;
+
+        this._adtInstrumentation = new AdvancedDynamicTextureInstrumentation(adt);
+        this._adtInstrumentation!.captureRenderTime = true;
+        this._adtInstrumentation!.captureLayoutTime = true;
+    }
+
+    componentWillUnmount() {
+        if (this._adtInstrumentation) {
+            this._adtInstrumentation.dispose();
+            this._adtInstrumentation = null;
+        }
+    }
+
+    updateTexture(file: File) {
+        const texture = this.props.texture;
+        Tools.ReadFile(file, (data) => {
+            var blob = new Blob([data], { type: "octet/stream" });
+
+            var reader = new FileReader();
+            reader.readAsDataURL(blob); 
+            reader.onloadend = () => {
+                let base64data = reader.result as string;     
+
+                if (texture.isCube) {
+                    let extension: string | undefined = undefined;
+                    if (file.name.toLowerCase().indexOf(".dds") > 0) {
+                        extension = ".dds";
+                    } else if (file.name.toLowerCase().indexOf(".env") > 0) {
+                        extension = ".env";
+                    }
+
+                    (texture as CubeTexture).updateURL(base64data, extension, () => this.forceRefresh());
+                } else {
+                    (texture as Texture).updateURL(base64data, null, () => this.forceRefresh());
+                }
+            };
+
+        }, undefined, true);
+    }
+
+    onOpenTextureEditor() {
+        this._isTextureEditorOpen = true;
+    }
+    
+    onCloseTextureEditor(window: Window | null) {
+        this._isTextureEditorOpen = false;
+        if (window !== null) {
+            window.close();
+        }
+    }
+
+    forceRefresh() {
+        this.forceUpdate();
+        (this.textureLineRef.current as TextureLineComponent).updatePreview();
+    }
+
+    render() {
+        const texture = this.props.texture;
+
+        var samplingMode = [
+            { label: "Nearest", value: Texture.NEAREST_NEAREST },
+            { label: "Nearest & linear mip", value: Texture.NEAREST_LINEAR },
+            { label: "Linear", value: Texture.LINEAR_LINEAR_MIPLINEAR },
+        ];
+
+        var coordinatesMode = [
+            { label: "Explicit", value: Texture.EXPLICIT_MODE },
+            { label: "Cubic", value: Texture.CUBIC_MODE },
+            { label: "Inverse cubic", value: Texture.INVCUBIC_MODE },
+            { label: "Equirectangular", value: Texture.EQUIRECTANGULAR_MODE },
+            { label: "Fixed equirectangular", value: Texture.FIXED_EQUIRECTANGULAR_MODE },
+            { label: "Fixed equirectangular mirrored", value: Texture.FIXED_EQUIRECTANGULAR_MIRRORED_MODE },
+            { label: "Planar", value: Texture.PLANAR_MODE },
+            { label: "Projection", value: Texture.PROJECTION_MODE },
+            { label: "Skybox", value: Texture.SKYBOX_MODE },
+            { label: "Spherical", value: Texture.SPHERICAL_MODE },
+        ];
+
+        let extension = "";
+        let url = (texture as Texture).url;
+        let textureUrl = (!url || url.substring(0, 4) === "data" || url.substring(0, 4) === "blob") ? "" : url;
+
+        if (textureUrl) {
+            for (var index = textureUrl.length - 1; index >= 0; index--) {
+                if (textureUrl[index] === ".") {
+                    break;
+                }
+                extension = textureUrl[index] + extension;
+            }
+        }
+
+        const editable = texture.textureType != Engine.TEXTURETYPE_FLOAT && texture.textureType != Engine.TEXTURETYPE_FLOAT_32_UNSIGNED_INT_24_8_REV && texture.textureType !== Engine.TEXTURETYPE_HALF_FLOAT;
+
+        return (
+            <div className="pane">
+                <LineContainerComponent globalState={this.props.globalState} title="PREVIEW">
+                    <TextureLineComponent ref={this.textureLineRef} texture={texture} width={256} height={256} globalState={this.props.globalState} />
+                    <FileButtonLineComponent label="Load texture from file" onClick={(file) => this.updateTexture(file)} accept=".jpg, .png, .tga, .dds, .env" />
+                    {editable &&
+                        <ButtonLineComponent label="View" onClick={() => this.onOpenTextureEditor()} />
+                    }
+                    <TextInputLineComponent label="URL" value={textureUrl} lockObject={this.props.lockObject} onChange={url => {
+                        (texture as Texture).updateURL(url);
+                        this.forceRefresh();
+                    }} />
+                </LineContainerComponent>
+                {this._isTextureEditorOpen && (
+                <PopupComponent
+                  id='texture-editor'
+                  title='Texture Editor'
+                  size={{ width: 1024, height: 490 }}
+                  onOpen={(window: Window) => {}}
+                  onClose={(window: Window) =>
+                    this.onCloseTextureEditor(window)
+                  }
+                >
+                    <TextureEditorComponent
+                        globalState={this.props.globalState}
+                        texture={this.props.texture}
+                    />
+                </PopupComponent>)}
+                <CustomPropertyGridComponent globalState={this.props.globalState} target={texture}
+                    lockObject={this.props.lockObject}
+                    onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                <LineContainerComponent globalState={this.props.globalState} title="GENERAL">
+                    <TextLineComponent label="Width" value={texture.getSize().width.toString()} />
+                    <TextLineComponent label="Height" value={texture.getSize().height.toString()} />
+                    {
+                        texture.isRenderTarget &&
+                        <ButtonLineComponent label="Scale up" onClick={() => {
+                            let scene = texture.getScene()!;
+                            texture.scale(2);
+                            setTimeout(() => {
+                                this.props.globalState.onSelectionChangedObservable.notifyObservers(scene.getTextureByUniqueID(texture.uniqueId));
+                            });
+                        }} />
+                    }
+                    {
+                        texture.isRenderTarget &&
+                        <ButtonLineComponent label="Scale down" onClick={() => {                            
+                            let scene = texture.getScene()!;
+                            texture.scale(0.5);
+                            setTimeout(() => {
+                                this.props.globalState.onSelectionChangedObservable.notifyObservers(scene.getTextureByUniqueID(texture.uniqueId));
+                            });
+                        }} />
+                    }
+                    {
+                        extension &&
+                        <TextLineComponent label="File format" value={extension} />
+                    }
+                    <TextLineComponent label="Unique ID" value={texture.uniqueId.toString()} />
+                    <TextLineComponent label="Class" value={texture.getClassName()} />
+                    <TextLineComponent label="Has alpha" value={texture.hasAlpha ? "Yes" : "No"} />
+                    <TextLineComponent label="Is 3D" value={texture.is3D ? "Yes" : "No"} />
+                    <TextLineComponent label="Is 2D array" value={texture.is2DArray ? "Yes" : "No"} />
+                    <TextLineComponent label="Is cube" value={texture.isCube ? "Yes" : "No"} />
+                    <TextLineComponent label="Is render target" value={texture.isRenderTarget ? "Yes" : "No"} />
+                    {
+                        (texture instanceof Texture) && 
+                        <TextLineComponent label="Stored as inverted on Y" value={texture.invertY ? "Yes" : "No"} />
+                    }
+                    <TextLineComponent label="Has mipmaps" value={!texture.noMipmap ? "Yes" : "No"} />
+                    <SliderLineComponent label="UV set" target={texture} propertyName="coordinatesIndex" minimum={0} maximum={3} step={1} onPropertyChangedObservable={this.props.onPropertyChangedObservable} decimalCount={0} />
+                    <OptionsLineComponent label="Mode" options={coordinatesMode} target={texture} propertyName="coordinatesMode" onPropertyChangedObservable={this.props.onPropertyChangedObservable} onSelect={(value) => texture.updateSamplingMode(value)} />
+                    <SliderLineComponent label="Level" target={texture} propertyName="level" minimum={0} maximum={2} step={0.01} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    {
+                        texture.updateSamplingMode &&
+                        <OptionsLineComponent label="Sampling" options={samplingMode} target={texture} noDirectUpdate={true} propertyName="samplingMode" onPropertyChangedObservable={this.props.onPropertyChangedObservable} onSelect={(value) => texture.updateSamplingMode(value)} />
+                    }
+                </LineContainerComponent>
+                {
+                    texture.getScene() &&
+                    <AnimationGridComponent globalState={this.props.globalState} animatable={texture} scene={texture.getScene()!} lockObject={this.props.lockObject} />
+                }
+                {
+                    (texture as any).rootContainer &&
+                    <LineContainerComponent globalState={this.props.globalState} title="ADVANCED TEXTURE PROPERTIES">
+                        <ValueLineComponent label="Last layout time" value={this._adtInstrumentation!.renderTimeCounter.current} units="ms" />
+                        <ValueLineComponent label="Last render time" value={this._adtInstrumentation!.layoutTimeCounter.current} units="ms" />
+                        <SliderLineComponent label="Render scale" minimum={0.1} maximum={5} step={0.1} target={texture} propertyName="renderScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <CheckBoxLineComponent label="Premultiply alpha" target={texture} propertyName="premulAlpha" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <FloatLineComponent lockObject={this.props.lockObject} label="Ideal width" target={texture} propertyName="idealWidth" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <FloatLineComponent lockObject={this.props.lockObject} label="Ideal height" target={texture} propertyName="idealHeight" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <CheckBoxLineComponent label="Use smallest ideal" target={texture} propertyName="useSmallestIdeal" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <CheckBoxLineComponent label="Render at ideal size" target={texture} propertyName="renderAtIdealSize" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <CheckBoxLineComponent label="Invalidate Rect optimization" target={texture} propertyName="useInvalidateRectOptimization" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    </LineContainerComponent>
+                }
+                <LineContainerComponent globalState={this.props.globalState} title="TRANSFORM">
+                    {
+                        !texture.isCube &&
+                        <div>
+                            <FloatLineComponent lockObject={this.props.lockObject} label="U offset" target={texture} propertyName="uOffset" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <FloatLineComponent lockObject={this.props.lockObject} label="V offset" target={texture} propertyName="vOffset" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <FloatLineComponent lockObject={this.props.lockObject} label="U scale" target={texture} propertyName="uScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <FloatLineComponent lockObject={this.props.lockObject} label="V scale" target={texture} propertyName="vScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <FloatLineComponent lockObject={this.props.lockObject} label="U angle" useEuler={this.props.globalState.onlyUseEulers} target={texture} propertyName="uAng" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <FloatLineComponent lockObject={this.props.lockObject} label="V angle" useEuler={this.props.globalState.onlyUseEulers} target={texture} propertyName="vAng" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <FloatLineComponent lockObject={this.props.lockObject} label="W angle" useEuler={this.props.globalState.onlyUseEulers} target={texture} propertyName="wAng" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <CheckBoxLineComponent label="Clamp U" isSelected={() => texture.wrapU === Texture.CLAMP_ADDRESSMODE} onSelect={(value) => texture.wrapU = value ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE} />
+                            <CheckBoxLineComponent label="Clamp V" isSelected={() => texture.wrapV === Texture.CLAMP_ADDRESSMODE} onSelect={(value) => texture.wrapV = value ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE} />
+                        </div>
+                    }
+                    {
+                        texture.isCube &&
+                        <div>
+                            <SliderLineComponent label="Rotation Y" useEuler={this.props.globalState.onlyUseEulers} minimum={0} maximum={2 * Math.PI} step={0.1} target={texture} propertyName="rotationY" />
+                        </div>
+                    }
+                </LineContainerComponent>
+            </div>
+        );
+    }
 }

+ 265 - 156
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -1,156 +1,265 @@
-import { Engine } from 'babylonjs/Engines/engine';
-import { Scene } from 'babylonjs/scene';
-import { Vector3 } from 'babylonjs/Maths/math.vector';
-import { Color4 } from 'babylonjs/Maths/math.color';
-import { FreeCamera } from 'babylonjs/Cameras/freeCamera';
-
-import { PlaneBuilder } from 'babylonjs/Meshes/Builders/planeBuilder';
-import { Mesh } from 'babylonjs/Meshes/mesh';
-import { Camera } from 'babylonjs/Cameras/camera';
-import { DynamicTexture } from 'babylonjs/Materials/Textures/dynamicTexture';
-import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
-import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
-
-import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
-import { KeyboardEventTypes } from 'babylonjs/Events/keyboardEvents';
-
-export class TextureCanvasManager {
-    private _engine: Engine;
-    private _scene: Scene;
-    private _texture: DynamicTexture;
-    private _camera: FreeCamera;
-    private _canvas : HTMLCanvasElement;
-
-    private _scale : number;
-    private _isPanning : boolean;
-    private _mouseX : number;
-    private _mouseY : number;
-
-    private _plane : Mesh;
-    private _planeMaterial : NodeMaterial;
-
-    private static ZOOM_MOUSE_SPEED : number = 0.0005;
-    private static ZOOM_KEYBOARD_SPEED : number = 0.2;
-    private static PAN_SPEED : number = 0.002;
-    private static PAN_BUTTON : number = 0; // left mouse button
-    private static MIN_SCALE : number = 0.01;
-    private static MAX_SCALE : number = 10;
-
-    public constructor(targetCanvas: HTMLCanvasElement, texture: BaseTexture) {
-        this._canvas = targetCanvas;
-
-        this._engine = new Engine(targetCanvas, true);
-        this._scene = new Scene(this._engine);
-        this._scene.clearColor = new Color4(0.2, 0.2, 0.2, 1.0);
-
-        this._camera = new FreeCamera("Camera", new Vector3(0, 0, -1), this._scene);
-        this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
-
-        if (texture) {
-            /* Grab image data from original texture and paint it onto the context of a DynamicTexture */
-            const pixelData = texture.readPixels()!;
-            const arr = new Uint8ClampedArray(pixelData.buffer);
-            let imgData = new ImageData(arr, texture.getSize().width, texture.getSize().height);
-            this._texture = new DynamicTexture("texture", texture.getSize(), this._scene, false);
-            const ctx = this._texture.getContext();
-            ctx.putImageData(imgData, 0, 0);
-            this._texture.update();
-            this._texture.hasAlpha = texture.hasAlpha;
-        } else {
-            /* If we don't have a texture to start with, just generate a white rectangle */
-            this._texture = new DynamicTexture("texture",  256, this._scene, false);
-            const ctx = this._texture.getContext();
-            ctx.fillStyle = 'white';
-            ctx.fillRect(0, 0, 256, 256);
-            this._texture.update();
-        }
-
-        this._texture.updateSamplingMode(Engine.TEXTURE_NEAREST_LINEAR);
-        const textureRatio = this._texture.getSize().width / this._texture.getSize().height;
-
-        this._plane = PlaneBuilder.CreatePlane("plane", {width: textureRatio, height: 1}, this._scene);
-        NodeMaterial.ParseFromSnippetAsync("#TPSEV2#3", this._scene)
-            .then((material) => {
-                this._planeMaterial = material;
-                this._planeMaterial.getTextureBlocks()[0].texture = this._texture;
-                this._plane.material = this._planeMaterial;
-                this._canvas.focus();
-            });
-        this._plane.enableEdgesRendering();
-        this._plane.edgesWidth = 4.0;
-        this._plane.edgesColor = new Color4(1,1,1,1);
-
-        this._engine.runRenderLoop(() => {
-            this._engine.resize();
-            this._scene.render();
-
-        });
-
-        this._scale = 1;
-        this._isPanning = false;
-
-        this._scene.onBeforeRenderObservable.add(() => {
-            let ratio = this._canvas?.width / this._canvas?.height;
-            this._camera.orthoBottom = -this._scale;
-            this._camera.orthoTop = this._scale;
-            this._camera.orthoLeft = -this._scale * ratio;
-            this._camera.orthoRight = this._scale * ratio;
-        })
-
-        this._scene.onPointerObservable.add((pointerInfo) => {
-            switch (pointerInfo.type) {
-                case PointerEventTypes.POINTERWHEEL:
-                    const event = pointerInfo.event as MouseWheelEvent;
-                    this._scale += (event.deltaY * TextureCanvasManager.ZOOM_MOUSE_SPEED * this._scale);
-                    this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE), TextureCanvasManager.MAX_SCALE);
-                    break;
-                case PointerEventTypes.POINTERDOWN:
-                    if (pointerInfo.event.button === TextureCanvasManager.PAN_BUTTON) {
-                        this._isPanning = true;
-                        this._mouseX = pointerInfo.event.x;
-                        this._mouseY = pointerInfo.event.y;
-                    }
-                    break;
-                case PointerEventTypes.POINTERUP:
-                    if (pointerInfo.event.button === TextureCanvasManager.PAN_BUTTON) {
-                        this._isPanning = false;
-                    }
-                    break;
-                case PointerEventTypes.POINTERMOVE:
-                    if (this._isPanning) {
-                        this._camera.position.x -= (pointerInfo.event.x - this._mouseX) * this._scale * TextureCanvasManager.PAN_SPEED;
-                        this._camera.position.y += (pointerInfo.event.y - this._mouseY) * this._scale * TextureCanvasManager.PAN_SPEED;
-                        this._mouseX = pointerInfo.event.x;
-                        this._mouseY = pointerInfo.event.y;
-                    }
-                    break;
-            }
-        })
-
-        this._scene.onKeyboardObservable.add((kbInfo) => {
-            switch(kbInfo.type) {
-                case KeyboardEventTypes.KEYDOWN:
-                    if (kbInfo.event.key == "+") {
-                        this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
-                    }
-                    if (kbInfo.event.key == "-") {
-                        this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
-                    }
-                    this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE), TextureCanvasManager.MAX_SCALE);
-                    break;
-            }
-        })
-
-    }
-
-    public dispose() {
-        if (this._planeMaterial) {
-            this._planeMaterial.dispose();
-        }
-        this._texture.dispose();
-        this._plane.dispose();
-        this._camera.dispose();
-        this._scene.dispose();
-        this._engine.dispose();
-    }
-} 
+import { Engine } from 'babylonjs/Engines/engine';
+import { Scene } from 'babylonjs/scene';
+import { Vector3 } from 'babylonjs/Maths/math.vector';
+import { Color4 } from 'babylonjs/Maths/math.color';
+import { FreeCamera } from 'babylonjs/Cameras/freeCamera';
+import { Nullable } from 'babylonjs/types'
+
+import { PlaneBuilder } from 'babylonjs/Meshes/Builders/planeBuilder';
+import { Mesh } from 'babylonjs/Meshes/mesh';
+import { Camera } from 'babylonjs/Cameras/camera';
+import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
+import { HtmlElementTexture } from 'babylonjs/Materials/Textures/htmlElementTexture';
+import { InternalTexture } from 'babylonjs/Materials/Textures/internalTexture';
+import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
+import { TextureHelper, TextureChannelToDisplay } from '../../../../../../textureHelper';
+import { ISize } from 'babylonjs/Maths/math.size';
+
+
+import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
+import { KeyboardEventTypes } from 'babylonjs/Events/keyboardEvents';
+
+export class TextureCanvasManager {
+    private _engine: Engine;
+    private _scene: Scene;
+    private _camera: FreeCamera;
+
+    private _scale : number;
+    private _isPanning : boolean = false;
+    private _mouseX : number;
+    private _mouseY : number;
+
+    private _UICanvas : HTMLCanvasElement;
+
+    private _size : ISize;
+
+    /* This is the canvas we paint onto using the canvas API */
+    private _2DCanvas : HTMLCanvasElement;
+    /* The texture we are currently editing, which is based on _2DCanvas */
+    private _texture: HtmlElementTexture;
+
+    private _displayCanvas : HTMLCanvasElement;
+    private _displayChannel : TextureChannelToDisplay = TextureChannelToDisplay.All;
+    /* This is the actual texture that is being displayed. Sometimes it's just a single channel from _textures */
+    private _displayTexture : HtmlElementTexture;
+
+    /* The texture from the original engine that we invoked the editor on */
+    private _originalTexture: BaseTexture;
+    /* This is a hidden texture which is only responsible for holding the actual texture memory in the original engine */
+    private _targetTexture : Nullable<HtmlElementTexture> = null;
+    /* The internal texture representation of the original texture */
+    private _originalInternalTexture : Nullable<InternalTexture> = null;
+
+    private _plane : Mesh;
+    private _planeMaterial : NodeMaterial;
+
+    /* Tracks which keys are currently pressed */
+    private keyMap : any = {};
+
+    private static ZOOM_MOUSE_SPEED : number = 0.0005;
+    private static ZOOM_KEYBOARD_SPEED : number = 0.2;
+    private static ZOOM_IN_KEY : string = '+';
+    private static ZOOM_OUT_KEY : string = '-';
+
+    private static PAN_SPEED : number = 0.002;
+    private static PAN_MOUSE_BUTTON : number = 0; // RMB
+    private static PAN_KEY : string = ' ';
+
+    private static MIN_SCALE : number = 0.01;
+    private static MAX_SCALE : number = 10;
+
+    public metadata : any = {
+        color: '#ffffff',
+        opacity: 1.0
+    };
+
+    public constructor(texture: BaseTexture, canvasUI: HTMLCanvasElement, canvas2D: HTMLCanvasElement, canvasDisplay: HTMLCanvasElement) {
+        this._UICanvas = canvasUI;
+        this._2DCanvas = canvas2D;
+        this._displayCanvas = canvasDisplay;
+
+        this._originalTexture = texture;
+        this._size = this._originalTexture.getSize();
+
+        this._engine = new Engine(this._UICanvas, true);
+        this._scene = new Scene(this._engine);
+        this._scene.clearColor = new Color4(0.2, 0.2, 0.2, 1.0);
+
+        this._camera = new FreeCamera("Camera", new Vector3(0, 0, -1), this._scene);
+        this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
+        this._texture = new HtmlElementTexture("texture", this._2DCanvas, {engine: this._engine, scene: this._scene});
+        if (texture) {
+            /* Grab image data from original texture and paint it onto the context of a DynamicTexture */
+            const pixelData = this._originalTexture.readPixels()!;
+            TextureCanvasManager.paintPixelsOnCanvas(new Uint8Array(pixelData.buffer), this._2DCanvas);
+            this._texture.update();
+        }
+
+        this._displayTexture = new HtmlElementTexture("display", this._displayCanvas, {engine: this._engine, scene: this._scene});
+        this.copyTextureToDisplayTexture();
+        this._displayTexture.updateSamplingMode(Engine.TEXTURE_NEAREST_LINEAR);
+
+        const textureRatio = this._size.width / this._size.height;
+        
+        this._plane = PlaneBuilder.CreatePlane("plane", {width: textureRatio, height: 1}, this._scene);
+        NodeMaterial.ParseFromSnippetAsync("#TPSEV2#4", this._scene)
+            .then((material) => {
+                this._planeMaterial = material;
+                this._planeMaterial.getTextureBlocks()[0].texture = this._displayTexture;
+                this._plane.material = this._planeMaterial;
+                this._UICanvas.focus();
+            });
+        this._plane.enableEdgesRendering();
+        this._plane.edgesWidth = 4.0;
+        this._plane.edgesColor = new Color4(1,1,1,1);
+        this._plane.enablePointerMoveEvents = true;
+
+        this._engine.runRenderLoop(() => {
+            this._engine.resize();
+            this._scene.render();
+            let cursor = 'initial';
+            if (this.keyMap[TextureCanvasManager.PAN_KEY]) {
+                cursor = 'pointer';
+            }
+            this._UICanvas.parentElement!.style.cursor = cursor;
+        });
+
+        this._scale = 1;
+        this._isPanning = false;
+
+        this._scene.onBeforeRenderObservable.add(() => {
+            this._scale = Math.min(Math.max(this._scale, TextureCanvasManager.MIN_SCALE), TextureCanvasManager.MAX_SCALE);
+            const ratio = this._UICanvas?.width / this._UICanvas?.height;
+            this._camera.orthoBottom = -this._scale;
+            this._camera.orthoTop = this._scale;
+            this._camera.orthoLeft = -this._scale * ratio;
+            this._camera.orthoRight = this._scale * ratio;
+        })
+
+        this._scene.onPointerObservable.add((pointerInfo) => {
+            switch (pointerInfo.type) {
+                case PointerEventTypes.POINTERWHEEL:
+                    const event = pointerInfo.event as MouseWheelEvent;
+                    this._scale += (event.deltaY * TextureCanvasManager.ZOOM_MOUSE_SPEED * this._scale);
+                    break;
+                case PointerEventTypes.POINTERDOWN:
+                    if (pointerInfo.event.button === TextureCanvasManager.PAN_MOUSE_BUTTON && this.keyMap[TextureCanvasManager.PAN_KEY]) {
+                        this._isPanning = true;
+                        this._mouseX = pointerInfo.event.x;
+                        this._mouseY = pointerInfo.event.y;
+                        pointerInfo.event.preventDefault();
+                    }
+                    break;
+                case PointerEventTypes.POINTERUP:
+                    if (pointerInfo.event.button === TextureCanvasManager.PAN_MOUSE_BUTTON) {
+                        this._isPanning = false;
+                    }
+                    break;
+                case PointerEventTypes.POINTERMOVE:
+                    if (this._isPanning) {
+                        this._camera.position.x -= (pointerInfo.event.x - this._mouseX) * this._scale * TextureCanvasManager.PAN_SPEED;
+                        this._camera.position.y += (pointerInfo.event.y - this._mouseY) * this._scale * TextureCanvasManager.PAN_SPEED;
+                        this._mouseX = pointerInfo.event.x;
+                        this._mouseY = pointerInfo.event.y;
+                    }
+                    break;
+            }
+        })
+
+        this._scene.onKeyboardObservable.add((kbInfo) => {
+            switch(kbInfo.type) {
+                case KeyboardEventTypes.KEYDOWN:
+                    this.keyMap[kbInfo.event.key] = true;
+                    if (kbInfo.event.key === TextureCanvasManager.ZOOM_IN_KEY) {
+                        this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                    }
+                    if (kbInfo.event.key === TextureCanvasManager.ZOOM_OUT_KEY) {
+                        this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                    }
+                    break;
+                case KeyboardEventTypes.KEYUP:
+                    this.keyMap[kbInfo.event.key] = false;
+                    if (kbInfo.event.key == TextureCanvasManager.PAN_KEY) {
+                        this._isPanning = false;
+                    }
+                break;
+            }
+        })
+
+    }
+
+    public updateTexture() {
+        this._texture.update();
+        if (!this._targetTexture) {
+            this._originalInternalTexture = this._originalTexture._texture;
+            this._targetTexture = new HtmlElementTexture("editor", this._2DCanvas, {engine: this._originalTexture.getScene()?.getEngine()!, scene: null});
+        }
+        this._targetTexture.update();
+        this._originalTexture._texture = this._targetTexture._texture;
+        this.copyTextureToDisplayTexture();
+    }
+
+    private copyTextureToDisplayTexture() {
+        TextureHelper.GetTextureDataAsync(this._texture, this._size.width, this._size.height, 0, this._displayChannel)
+            .then(data => {
+                TextureCanvasManager.paintPixelsOnCanvas(data, this._displayCanvas);
+                this._displayTexture.update();
+            })
+    }
+
+    public set displayChannel(channel: TextureChannelToDisplay) {
+        this._displayChannel = channel;
+        this.copyTextureToDisplayTexture();
+    }
+
+    public get displayChannel() : TextureChannelToDisplay {
+        return this._displayChannel;
+    }
+
+    public static paintPixelsOnCanvas(pixelData : Uint8Array, canvas: HTMLCanvasElement) {
+        const ctx = canvas.getContext('2d')!;
+        const imgData = ctx.createImageData(canvas.width, canvas.height);
+        imgData.data.set(pixelData);
+        ctx.putImageData(imgData, 0, 0);
+        TextureCanvasManager.flipCanvas(canvas);
+    }
+
+    /* When copying from a WebGL texture to a Canvas, the y axis is inverted. This function flips it back */
+    public static flipCanvas(canvas: HTMLCanvasElement) {
+        const ctx = canvas.getContext('2d')!;
+        const transform = ctx.getTransform();
+        ctx.globalCompositeOperation = 'copy';
+        ctx.globalAlpha = 1.0;
+        ctx.translate(0,canvas.height);
+        ctx.scale(1,-1);
+        ctx.drawImage(canvas, 0, 0);
+        ctx.setTransform(transform);
+    }
+
+    public get scene() : Scene {
+        return this._scene;
+    }
+
+    public get canvas2D() : HTMLCanvasElement {
+        return this._2DCanvas;
+    }
+
+    public get size() : ISize {
+        return this._size;
+    }
+
+    public dispose() {
+        if (this._planeMaterial) {
+            this._planeMaterial.dispose();
+        }
+        if (this._originalInternalTexture) {
+            this._originalInternalTexture.dispose();
+        }
+        this._displayTexture.dispose();
+        this._texture.dispose();
+        this._plane.dispose();
+        this._camera.dispose();
+        this._scene.dispose();
+        this._engine.dispose();
+    }
+} 

+ 109 - 3
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss

@@ -1,4 +1,110 @@
-#texture-canvas {
-    width: 100%;
-    height: 100%;
+#texture-editor {
+    display: grid;
+    height: 100%;
+    width: 100%;
+    grid-template-columns: auto auto auto auto auto;
+    grid-template-rows: 60px calc(100% - 60px);
+}
+
+#controls {
+    width: 100%;
+    grid-row: 1;
+    grid-column: 1 / 6;
+    background: #464646;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-around;
+    
+    #toolbar {
+        display: flex;
+        flex-direction: column;
+        justify-content: left;
+        
+        #tools {
+            display: flex;
+            flex-direction: row;
+            justify-content: center;
+            form {
+                margin: 0;
+            }
+        }
+
+        #color {
+            label {
+                color: white;
+                margin: 1em;
+            }
+        }
+    }
+    
+    #channels {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        align-items: center;
+
+        .command {
+            border: 1px solid transparent;
+            background:transparent;
+            font-size: 20px;
+        }
+    
+        .selected {
+            border: 1px solid rgb(51, 122, 183);
+        }
+    }
+}
+
+#editing-area {
+    grid-row: 2;
+    grid-column: 1 / 6;
+    #canvas-ui {
+        width: 100%;
+        height: 100%;
+    }
+}
+
+button, select {
+    background: #222222;
+    border: 1px solid rgb(51, 122, 183);
+    margin: 5px 10px 5px 10px;
+    color:white;
+    padding: 4px 5px;
+    opacity: 0.9;
+    cursor: pointer;
+}
+
+button:hover, select:hover {
+    opacity: 1.0;
+}
+
+button:active {
+    background: #282828;
+}   
+
+button:focus, select:focus {
+    border: 1px solid rgb(51, 122, 183);
+    outline: 0px;
+} 
+
+input[type="text"] {
+    border: none;
+    padding: 0;
+    border-bottom: solid 1px rgb(51, 122, 183);
+    background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 96%, rgb(51, 122, 183) 4%);
+    background-position: -1000px 0;
+    background-size: 1000px 100%;
+    background-repeat: no-repeat;  
+    color:white;    
+}
+
+input[type="text"]::placeholder {
+    color: lightgray;
+}
+
+input[type="text"]:focus  {
+    box-shadow: none;
+    outline: none;
+    background-position: 0 0;
 }

+ 74 - 29
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx

@@ -1,30 +1,75 @@
-import * as React from 'react';
-import { GlobalState } from '../../../../../globalState';
-import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
-import { TextureCanvasManager } from './textureCanvasManager';
-
-require('./textureEditor.scss');
-
-interface TextureEditorComponentProps {
-    globalState: GlobalState;
-    texture: BaseTexture;
-}
-
-export class TextureEditorComponent extends React.Component<TextureEditorComponentProps> {
-    private _textureCanvasManager: TextureCanvasManager;
-    private reactCanvas = React.createRef<HTMLCanvasElement>();
-
-    componentDidMount() {
-        this._textureCanvasManager = new TextureCanvasManager(this.reactCanvas.current!, this.props.texture);
-    }
-
-    componentWillUnmount() {
-        this._textureCanvasManager.dispose();
-    }
-
-    render() {
-        return <div id='texture-editor'>
-            <canvas id="texture-canvas" ref={this.reactCanvas} tabIndex={1}></canvas>
-        </div>
-    }
+import * as React from 'react';
+import { GlobalState } from '../../../../../globalState';
+import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
+import { TextureCanvasManager } from './textureCanvasManager';
+import { TextureChannelToDisplay } from '../../../../../../textureHelper';
+
+require('./textureEditor.scss');
+
+interface TextureEditorComponentProps {
+    globalState: GlobalState;
+    texture: BaseTexture;
+}
+
+interface TextureEditorComponentState {
+    channel: TextureChannelToDisplay;
+}
+
+export class TextureEditorComponent extends React.Component<TextureEditorComponentProps, TextureEditorComponentState> {
+    private _textureCanvasManager: TextureCanvasManager;
+    private canvasUI = React.createRef<HTMLCanvasElement>();
+    private canvas2D = React.createRef<HTMLCanvasElement>();
+    private canvasDisplay = React.createRef<HTMLCanvasElement>();
+
+    private channels = [
+        {name: "RGBA", channel: TextureChannelToDisplay.All, className: "all"},
+        {name: "R", channel: TextureChannelToDisplay.R, className: "red"},
+        {name: "G", channel: TextureChannelToDisplay.G, className: "green"},
+        {name: "B", channel: TextureChannelToDisplay.B, className: "blue"},
+        {name: "A", channel: TextureChannelToDisplay.A, className: "alpha"},
+    ]
+
+    constructor(props : TextureEditorComponentProps) {
+        super(props);
+        this.state = {
+            channel: TextureChannelToDisplay.All,
+        }
+    }
+
+    componentDidMount() {
+        this._textureCanvasManager = new TextureCanvasManager(
+            this.props.texture,
+            this.canvasUI.current!,
+            this.canvas2D.current!,
+            this.canvasDisplay.current!
+        );
+    }
+
+    componentDidUpdate() {
+        this._textureCanvasManager.displayChannel = this.state.channel;
+    }
+
+    componentWillUnmount() {
+        this._textureCanvasManager.dispose();
+    }
+
+    render() {
+        return <div id="texture-editor">
+            <div id="controls">
+                <div id="channels">
+                    {this.channels.map(
+                        item => {
+                            const classNames = (item.channel === this.state.channel) ? "selected command " + item.className : "command " + item.className;
+                            return <button className={classNames} key={item.name} onClick={() => this.setState({channel: item.channel})}>{item.name}</button>
+                        }
+                    )}
+                </div>
+            </div>
+            <div id="editing-area">
+                <canvas id="canvas-ui" ref={this.canvasUI} tabIndex={1}></canvas>
+            </div>
+            <canvas id="canvas-display" ref={this.canvasDisplay} width={this.props.texture.getSize().width} height={this.props.texture.getSize().height} hidden={true}></canvas>
+            <canvas id="canvas-2D" ref={this.canvas2D} width={this.props.texture.getSize().width} height={this.props.texture.getSize().height} hidden={true}></canvas>
+        </div>
+    }
 }