Pārlūkot izejas kodu

Texture Inspector Design Refresh (#8561)

* ui refresh

* add environment texture support and reset

* disable tools, ui improvements

* clean up

* remove extra setting

* fix imports and add whats new.md

* ui fixes

* inspector fixes
DarraghBurkeMS 5 gadi atpakaļ
vecāks
revīzija
c5baf783c3
34 mainītis faili ar 847 papildinājumiem un 257 dzēšanām
  1. 1 1
      dist/preview release/what's new.md
  2. 34 26
      inspector/src/components/actionTabs/lines/textureLineComponent.tsx
  3. 28 15
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/texturePropertyGridComponent.tsx
  4. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/babylonLogo.svg
  5. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelA.svg
  6. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelB.svg
  7. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelD.svg
  8. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelG.svg
  9. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelR.svg
  10. 11 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/editIcon.svg
  11. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/eyeClosed.svg
  12. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/eyeOpen.svg
  13. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/mipDown.svg
  14. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/mipUp.svg
  15. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/negX.svg
  16. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/negY.svg
  17. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/negZ.svg
  18. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/posX.svg
  19. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/posY.svg
  20. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/posZ.svg
  21. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/reset.svg
  22. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/resizeTool.svg
  23. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/save.svg
  24. 1 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/upload.svg
  25. 13 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/bottomBar.tsx
  26. 52 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/channelsBar.tsx
  27. 95 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/propertiesBar.tsx
  28. 23 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasComponent.tsx
  29. 177 63
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts
  30. 157 84
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss
  31. 126 29
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditorComponent.tsx
  32. 53 0
      inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx
  33. 2 2
      inspector/src/components/actionTabs/tabs/propertyGrids/sprites/spritePropertyGridComponent.tsx
  34. 55 37
      inspector/src/textureHelper.ts

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

@@ -74,7 +74,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 and individual channels. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
+- View textures in pop out inspector with zoom & pan and individual channels, and save to local machine. ([DarraghBurkeMS](https://github.com/DarraghBurkeMS))
 
 ### Cameras
 

+ 34 - 26
inspector/src/components/actionTabs/lines/textureLineComponent.tsx

@@ -4,7 +4,7 @@ import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
 
 import { GlobalState } from "../../../components/globalState";
 import { ButtonLineComponent } from './buttonLineComponent';
-import { TextureHelper, TextureChannelToDisplay } from '../../../textureHelper';
+import { TextureHelper, TextureChannelsToDisplay } from '../../../textureHelper';
 
 interface ITextureLineComponentProps {
     texture: BaseTexture;
@@ -15,22 +15,30 @@ interface ITextureLineComponentProps {
 }
 
 
-export class TextureLineComponent extends React.Component<ITextureLineComponentProps, { channel: TextureChannelToDisplay, face: number }> {
+export class TextureLineComponent extends React.Component<ITextureLineComponentProps, { channels: TextureChannelsToDisplay, face: number }> {
     private canvasRef: React.RefObject<HTMLCanvasElement>;
 
+    private static 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}
+    }
+
     constructor(props: ITextureLineComponentProps) {
         super(props);
 
         this.state = {
-            channel: TextureChannelToDisplay.All,
+            channels: TextureLineComponent.TextureChannelStates.ALL,
             face: 0
         };
 
         this.canvasRef = React.createRef();
     }
 
-    shouldComponentUpdate(nextProps: ITextureLineComponentProps, nextState: { channel: TextureChannelToDisplay, face: number }): boolean {
-        return (nextProps.texture !== this.props.texture || nextState.channel !== this.state.channel || nextState.face !== this.state.face);
+    shouldComponentUpdate(nextProps: ITextureLineComponentProps, nextState: { channels: TextureChannelsToDisplay, face: number }): boolean {
+        return (nextProps.texture !== this.props.texture || nextState.channels !== this.state.channels || nextState.face !== this.state.face);
     }
 
     componentDidMount() {
@@ -41,28 +49,28 @@ export class TextureLineComponent extends React.Component<ITextureLineComponentP
         this.updatePreview();
     }
 
-    updatePreview() {
+   async updatePreview() {
+        const previewCanvas = this.canvasRef.current!;
         var texture = this.props.texture;
         var size = texture.getSize();
         var ratio = size.width / size.height;
         var width = this.props.width;
         var height = (width / ratio) | 1;            
 
-        TextureHelper.GetTextureDataAsync(texture, width, height, this.state.face, this.state.channel, this.props.globalState).then(data => {
-            const previewCanvas = this.canvasRef.current as HTMLCanvasElement;
-            previewCanvas.width = width;
-            previewCanvas.height = height;
-            var context = previewCanvas.getContext('2d');
-
-            if (context) {
-                // Copy the pixels to the preview canvas
-                var imageData = context.createImageData(width, height);
-                var castData = imageData.data;
-                castData.set(data);
-                context.putImageData(imageData, 0, 0);
-            }
-            previewCanvas.style.height = height + "px";
-        });
+        const data = await TextureHelper.GetTextureDataAsync(texture, width, height, this.state.face, this.state.channels, this.props.globalState);
+        
+        previewCanvas.width = width;
+        previewCanvas.height = height;
+        var context = previewCanvas.getContext('2d');
+
+        if (context) {
+            // Copy the pixels to the preview canvas
+            var imageData = context.createImageData(width, height);
+            var castData = imageData.data;
+            castData.set(data);
+            context.putImageData(imageData, 0, 0);
+        }
+        previewCanvas.style.height = height + "px";
     }
 
     render() {
@@ -85,11 +93,11 @@ export class TextureLineComponent extends React.Component<ITextureLineComponentP
                     {
                         !this.props.hideChannelSelect && !texture.isCube &&
                         <div className="control">
-                            <button className={this.state.channel === TextureChannelToDisplay.R ? "red command selected" : "red command"} onClick={() => this.setState({ channel: TextureChannelToDisplay.R })}>R</button>
-                            <button className={this.state.channel === TextureChannelToDisplay.G ? "green command selected" : "green command"} onClick={() => this.setState({ channel: TextureChannelToDisplay.G })}>G</button>
-                            <button className={this.state.channel === TextureChannelToDisplay.B ? "blue command selected" : "blue command"} onClick={() => this.setState({ channel: TextureChannelToDisplay.B })}>B</button>
-                            <button className={this.state.channel === TextureChannelToDisplay.A ? "alpha command selected" : "alpha command"} onClick={() => this.setState({ channel: TextureChannelToDisplay.A })}>A</button>
-                            <button className={this.state.channel === TextureChannelToDisplay.All ? "all command selected" : "all command"} onClick={() => this.setState({ channel: TextureChannelToDisplay.All })}>ALL</button>
+                            <button className={this.state.channels === TextureLineComponent.TextureChannelStates.R ? "red command selected" : "red command"} onClick={() => this.setState({ channels: TextureLineComponent.TextureChannelStates.R })}>R</button>
+                            <button className={this.state.channels === TextureLineComponent.TextureChannelStates.G ? "green command selected" : "green command"} onClick={() => this.setState({ channels: TextureLineComponent.TextureChannelStates.G })}>G</button>
+                            <button className={this.state.channels === TextureLineComponent.TextureChannelStates.B ? "blue command selected" : "blue command"} onClick={() => this.setState({ channels: TextureLineComponent.TextureChannelStates.B })}>B</button>
+                            <button className={this.state.channels === TextureLineComponent.TextureChannelStates.A ? "alpha command selected" : "alpha command"} onClick={() => this.setState({ channels: TextureLineComponent.TextureChannelStates.A })}>A</button>
+                            <button className={this.state.channels === TextureLineComponent.TextureChannelStates.ALL ? "all command selected" : "all command"} onClick={() => this.setState({ channels: TextureLineComponent.TextureChannelStates.ALL })}>ALL</button>
                         </div>
                     }
                     <canvas ref={this.canvasRef} className="preview" />

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

@@ -27,7 +27,6 @@ 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';
 
@@ -38,17 +37,24 @@ interface ITexturePropertyGridComponentProps {
     onPropertyChangedObservable?: Observable<PropertyChangedEvent>
 }
 
-export class TexturePropertyGridComponent extends React.Component<ITexturePropertyGridComponentProps> {
+interface ITexturePropertyGridComponentState {
+    isTextureEditorOpen : boolean,
+    textureEditing : Nullable<BaseTexture>
+}
+
+export class TexturePropertyGridComponent extends React.Component<ITexturePropertyGridComponentProps,ITexturePropertyGridComponentState> {
 
     private _adtInstrumentation: Nullable<AdvancedDynamicTextureInstrumentation>;
     private textureLineRef: React.RefObject<TextureLineComponent>;
 
-    private _isTextureEditorOpen = false;
-    
 
     constructor(props: ITexturePropertyGridComponentProps) {
         super(props);
 
+        this.state = {
+            isTextureEditorOpen: false,
+            textureEditing: null
+        }
         const texture = this.props.texture;
 
         this.textureLineRef = React.createRef();
@@ -99,11 +105,21 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
     }
 
     onOpenTextureEditor() {
-        this._isTextureEditorOpen = true;
+        if (this.state.isTextureEditorOpen && this.state.textureEditing !== this.props.texture) {
+            this.onCloseTextureEditor(null, () => this.onOpenTextureEditor());
+            return;
+        }
+        this.setState({
+            isTextureEditorOpen: true,
+            textureEditing: this.props.texture
+        });
     }
     
-    onCloseTextureEditor(window: Window | null) {
-        this._isTextureEditorOpen = false;
+    onCloseTextureEditor(window: Window | null, callback?: {() : void}) {
+        this.setState({
+            isTextureEditorOpen: false,
+            textureEditing: null
+        }, callback);
         if (window !== null) {
             window.close();
         }
@@ -149,25 +165,21 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
             }
         }
 
-        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()} />
-                    }
+                    <ButtonLineComponent label="Edit" onClick={() => this.onOpenTextureEditor()} />
                     <TextInputLineComponent label="URL" value={textureUrl} lockObject={this.props.lockObject} onChange={url => {
                         (texture as Texture).updateURL(url);
                         this.forceRefresh();
                     }} />
                 </LineContainerComponent>
-                {this._isTextureEditorOpen && (
+                {this.state.isTextureEditorOpen && (
                 <PopupComponent
                   id='texture-editor'
-                  title='Texture Editor'
+                  title='Texture Inspector'
                   size={{ width: 1024, height: 490 }}
                   onOpen={(window: Window) => {}}
                   onClose={(window: Window) =>
@@ -177,6 +189,7 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
                     <TextureEditorComponent
                         globalState={this.props.globalState}
                         texture={this.props.texture}
+                        url={textureUrl}
                     />
                 </PopupComponent>)}
                 <CustomPropertyGridComponent globalState={this.props.globalState} target={texture}
@@ -234,7 +247,7 @@ export class TexturePropertyGridComponent extends React.Component<ITextureProper
                     <AnimationGridComponent globalState={this.props.globalState} animatable={texture} scene={texture.getScene()!} lockObject={this.props.lockObject} />
                 }
                 {
-                    (texture as any).rootContainer &&
+                    (texture as any).rootContainer && this._adtInstrumentation &&
                     <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" />

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/babylonLogo.svg


+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelA.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M20,30.5A10.5,10.5,0,1,1,30.5,20,10.51,10.51,0,0,1,20,30.5Zm0-20A9.5,9.5,0,1,0,29.5,20,9.51,9.51,0,0,0,20,10.5Z" style="fill:#fff"/><path d="M18,22.53l-.79,2.38H15l3.51-10.33h2.81L25,24.91H22.64l-.86-2.38Zm3.3-1.83c-.73-2.19-1.2-3.59-1.45-4.52h0c-.25,1-.76,2.64-1.35,4.52Z" style="fill:#fff"/></svg>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelB.svg


+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelD.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M20,30.5A10.58,10.58,0,1,1,30.57,19.93,10.58,10.58,0,0,1,20,30.5Zm0-19.9a9.33,9.33,0,1,0,9.33,9.33A9.34,9.34,0,0,0,20,10.6Z" style="fill:#fff"/><path d="M16.32,14.54h4.09a4.68,4.68,0,0,1,4.95,5c0,3-1.91,5.27-5.06,5.27h-4ZM18.54,23h1.67c2,0,2.85-1.48,2.85-3.48,0-1.69-.82-3.22-2.83-3.22H18.54Z" style="fill:#fff"/></svg>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelG.svg


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/channelR.svg


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 11 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/editIcon.svg


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/eyeClosed.svg


+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/eyeOpen.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M20,18.27a3.09,3.09,0,1,1-3.08,3.08A3.09,3.09,0,0,1,20,18.27Zm0,1.15a1.93,1.93,0,1,0,1.93,1.93A1.94,1.94,0,0,0,20,19.42Zm0-3.86a7.71,7.71,0,0,1,7.48,5.84.58.58,0,1,1-1.12.28,6.56,6.56,0,0,0-12.72,0,.58.58,0,0,1-1.12-.28A7.72,7.72,0,0,1,20,15.56Z" style="fill:#fff"/></svg>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/mipDown.svg


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/mipUp.svg


+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/negX.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M17.75,18.72v1.45H14.33V18.72Z" style="fill:#fff"/><path d="M23.75,24.62,22,20.41h0l-1.77,4.21H18.44l2.61-5.57-2.52-5.29h1.93l1.63,3.8h0l1.65-3.8h1.78L23,18.87l2.64,5.75Z" style="fill:#fff"/></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/negY.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M17.7,18.72v1.45H14.28V18.72Z" style="fill:#fff"/><path d="M21.21,24.62V20.68a.63.63,0,0,0-.07-.32l-2.73-6.6h1.87c.66,1.76,1.5,4,1.82,5.13.4-1.24,1.25-3.46,1.84-5.13h1.78L23,20.37a.9.9,0,0,0-.05.34v3.91Z" style="fill:#fff"/></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/negZ.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M18,18.72v1.45H14.54V18.72Z" style="fill:#fff"/><path d="M19.09,23.43l4.35-8.18h-4V13.76h5.86v1.36l-4.22,8h4.4l-.23,1.48H19.09Z" style="fill:#fff"/></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/posX.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M16.89,16v3h2.82v1.27H16.89v3h-1.3v-3H12.74V19h2.85V16Z" style="fill:#fff"/><path d="M25.34,24.62l-1.75-4.21h0L21.8,24.62H20l2.61-5.57-2.51-5.29H22l1.63,3.8h0l1.65-3.8h1.77l-2.49,5.11,2.64,5.75Z" style="fill:#fff"/></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/posY.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M16.84,16v3h2.82v1.27H16.84v3h-1.3v-3H12.69V19h2.85V16Z" style="fill:#fff"/><path d="M22.79,24.62V20.68a.63.63,0,0,0-.06-.32L20,13.76h1.87c.66,1.76,1.51,4,1.83,5.13.4-1.24,1.25-3.46,1.84-5.13h1.78l-2.77,6.61a.73.73,0,0,0-.05.34v3.91Z" style="fill:#fff"/></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/posZ.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M17.1,16v3h2.82v1.27H17.1v3H15.81v-3H13V19h2.85V16Z" style="fill:#fff"/><path d="M20.67,23.43,25,15.25H21V13.76h5.86v1.36l-4.23,8H27l-.22,1.48H20.67Z" style="fill:#fff"/></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/reset.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M13.45,11.32a.78.78,0,0,0-.77.66v3a.77.77,0,0,0,.66.76h3a.78.78,0,0,0,.77-.77.77.77,0,0,0-.67-.76H15.36a7.43,7.43,0,1,1-2.64,4.38.77.77,0,1,0-1.51-.29,9,9,0,1,0,3-5.13V12.09A.76.76,0,0,0,13.45,11.32Z" style="fill:#fff"/></svg>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/resizeTool.svg


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/save.svg


+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/assets/upload.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><rect width="40" height="40" style="fill:none"/><path d="M13.5,28.5h13a.76.76,0,0,1,.75.75.75.75,0,0,1-.65.74H13.5a.75.75,0,0,1-.1-1.5Zm1-13.28,5-5a.73.73,0,0,1,1-.07l.09.07,5,5a.75.75,0,0,1,0,1.06.75.75,0,0,1-1,.07l-.09-.07-3.72-3.72V26.25a.75.75,0,0,1-.65.74H20a.75.75,0,0,1-.74-.65V12.56l-3.72,3.72a.76.76,0,0,1-1,.07l-.08-.07a.75.75,0,0,1-.07-1l.07-.08,5-5Z" style="fill:#fff"/></svg>

+ 13 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/bottomBar.tsx

@@ -0,0 +1,13 @@
+import * as React from 'react';
+
+interface BottomBarProps {
+    name: string;
+}
+
+export class BottomBar extends React.Component<BottomBarProps> {
+    render() {
+        return <div id='bottom-bar'>
+            <span id='file-url'>{this.props.name}</span>
+        </div>;
+    }
+}

+ 52 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/channelsBar.tsx

@@ -0,0 +1,52 @@
+import * as React from 'react';
+
+export interface Channel {
+    visible: boolean;
+    editable: boolean;
+    name: string;
+    id: 'R' | 'G' | 'B' | 'A';
+    icon: any;
+}
+
+interface ChannelsBarProps {
+    channels: Channel[];
+    setChannels(channelState : Channel[]) : void;
+}
+
+const eyeOpen = require('./assets/eyeOpen.svg');
+const eyeClosed = require('./assets/eyeClosed.svg');
+
+export class ChannelsBar extends React.Component<ChannelsBarProps> {
+    render() {
+        return <div id='channels-bar'>
+            {this.props.channels.map(
+                (channel,index) => {
+                    const visTip = channel.visible ? 'Hide' : 'Show';
+                    const editTip = channel.editable ? 'Lock' : 'Unlock'
+                    return <div key={channel.name} className={channel.editable ? 'channel' : 'channel uneditable'}>
+                        <img
+                            className={channel.visible ? 'icon channel-visibility visible' : 'icon channel-visibility'}
+                            onClick={() => {
+                                let newChannels = this.props.channels;
+                                newChannels[index].visible = !newChannels[index].visible;
+                                this.props.setChannels(newChannels);
+                            }}
+                            src={channel.visible ? eyeOpen : eyeClosed}
+                            title={`${visTip} ${channel.name}`}
+                        />
+                        <img
+                            className='icon channel-name'
+                            onClick={() => {
+                                let newChannels = this.props.channels;
+                                newChannels[index].editable = !newChannels[index].editable;
+                                this.props.setChannels(newChannels);
+                            }}
+                            src={channel.icon}
+                            title={`${editTip} ${channel.name}`}
+                        />
+                    </div>
+                }
+            )}
+        </div>;
+    }
+}

+ 95 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/propertiesBar.tsx

@@ -0,0 +1,95 @@
+import * as React from 'react';
+import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
+import { PixelData } from './textureCanvasManager';
+
+interface PropertiesBarProps {
+    texture: BaseTexture;
+    saveTexture(): void;
+    pixelData: PixelData;
+    face: number;
+    setFace(face : number): void;
+    resetTexture() : void;
+}
+
+const resetButton = require('./assets/reset.svg');
+const uploadButton = require('./assets/upload.svg');
+const saveButton = require('./assets/save.svg');
+const babylonLogo = require('./assets/babylonLogo.svg');
+
+const posX = require('./assets/posX.svg');
+const posY = require('./assets/posY.svg');
+const posZ = require('./assets/posZ.svg');
+const negX = require('./assets/negX.svg');
+const negY = require('./assets/negY.svg');
+const negZ = require('./assets/negZ.svg');
+
+const resizeButton = require('./assets/resizeTool.svg');
+
+const mipUp = require('./assets/mipUp.svg');
+const mipDown = require('./assets/mipDown.svg');
+
+const faces = [
+    posX,
+    negX,
+    posY,
+    negY,
+    posZ,
+    negZ
+]
+
+interface PixelDataProps {
+    name : string;
+    data?: number;
+}
+
+function PixelData(props : PixelDataProps) {
+    return <span className='pixel-data'>{props.name}: <span className='value'>{props.data || '-'}</span></span>
+}
+
+export class PropertiesBar extends React.Component<PropertiesBarProps> {
+    render() {
+        return <div id='properties'>
+                <div className='tab' id='logo-tab'>
+                    <img className='icon' src={babylonLogo}/>
+                </div>
+                <div className='tab' id='dimensions-tab'>
+                    <label className='dimensions'>W: <input type='text' readOnly contentEditable={false} value={this.props.texture.getSize().width}/></label>
+                    <label className='dimensions'>H: <input type='text' readOnly contentEditable={false} value={this.props.texture.getSize().height} /></label>
+                    <img id='resize' className='icon button' title='Resize' alt='Resize' src={resizeButton}/>
+                </div>
+                <div className='tab' id='pixel-coords-tab'>
+                    <PixelData name='X' data={this.props.pixelData.x}/>
+                    <PixelData name='Y' data={this.props.pixelData.y}/>
+                </div>
+                <div className='tab' id='pixel-color-tab'>
+                    <PixelData name='R' data={this.props.pixelData.r}/>
+                    <PixelData name='G' data={this.props.pixelData.g}/>
+                    <PixelData name='B' data={this.props.pixelData.b}/>
+                    <PixelData name='A' data={this.props.pixelData.a}/>
+                </div>
+                {this.props.texture.isCube &&
+                <>
+                    <div className='tab' id='face-tab'>
+                        {faces.map((face, index) =>
+                        <img
+                            key={index}
+                            className={this.props.face == index ? 'icon face button active' : 'icon face button'}
+                            src={face}
+                            onClick={() => this.props.setFace(index)}
+                        />)}
+                    </div>
+                    <div className='tab' id='mip-tab'>
+                        <img title='Mip Preview Up' className='icon button' src={mipUp} />
+                        <img title='Mip Preview Down' className='icon button' src={mipDown} />
+                    </div>
+                </>}
+                <div className='tab' id='right-tab'>
+                    <div className='content'>
+                        <img title='Reset' className='icon button' src={resetButton} onClick={() => this.props.resetTexture()}/>
+                        <img title='Upload' className='icon button' src={uploadButton}/>
+                        <img title='Save' className='icon button' src={saveButton} onClick={() => this.props.saveTexture()}/>
+                    </div>
+                </div>
+        </div>;
+    }
+}

+ 23 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasComponent.tsx

@@ -0,0 +1,23 @@
+import * as React from 'react';
+import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
+
+interface TextureCanvasComponentProps {
+    canvasUI : React.RefObject<HTMLCanvasElement>;
+    canvas2D : React.RefObject<HTMLCanvasElement>;
+    canvasDisplay : React.RefObject<HTMLCanvasElement>;
+    texture : BaseTexture;
+}
+
+export class TextureCanvasComponent extends React.Component<TextureCanvasComponentProps> {
+    shouldComponentUpdate(nextProps : TextureCanvasComponentProps) {
+        return (nextProps.texture !== this.props.texture);
+    }
+
+    render() {
+        return <div>
+            <canvas id="canvas-ui" ref={this.props.canvasUI} tabIndex={1}></canvas>
+            <canvas id="canvas-display" ref={this.props.canvasDisplay} width={this.props.texture.getSize().width} height={this.props.texture.getSize().height} hidden={true}></canvas>
+            <canvas id="canvas-2D" ref={this.props.canvas2D} width={this.props.texture.getSize().width} height={this.props.texture.getSize().height} hidden={true}></canvas>
+        </div>
+    }
+}

+ 177 - 63
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureCanvasManager.ts

@@ -1,6 +1,6 @@
 import { Engine } from 'babylonjs/Engines/engine';
 import { Scene } from 'babylonjs/scene';
-import { Vector3 } from 'babylonjs/Maths/math.vector';
+import { Vector3, Vector2 } from 'babylonjs/Maths/math.vector';
 import { Color4 } from 'babylonjs/Maths/math.color';
 import { FreeCamera } from 'babylonjs/Cameras/freeCamera';
 import { Nullable } from 'babylonjs/types'
@@ -12,13 +12,26 @@ 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 { PBRMaterial } from 'babylonjs/Materials/PBR/pbrMaterial';
+import { RawCubeTexture } from 'babylonjs/Materials/Textures/rawCubeTexture';
+import { TextureHelper, TextureChannelsToDisplay } from '../../../../../../textureHelper';
 import { ISize } from 'babylonjs/Maths/math.size';
 
-
-import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
+import { PointerEventTypes, PointerInfo } from 'babylonjs/Events/pointerEvents';
 import { KeyboardEventTypes } from 'babylonjs/Events/keyboardEvents';
 
+import { Tool } from './toolBar';
+import { Channel } from './channelsBar';
+
+export interface PixelData {
+    x? : number;
+    y? : number;
+    r? : number;
+    g? : number;
+    b? : number;
+    a? : number;
+}
+
 export class TextureCanvasManager {
     private _engine: Engine;
     private _scene: Scene;
@@ -33,28 +46,32 @@ export class TextureCanvasManager {
 
     private _size : ISize;
 
-    /* This is the canvas we paint onto using the canvas API */
+    /* 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 _channels : Channel[] = [];
+    private _face : number = 0;
+    /* The texture that we are actually displaying. It is created by sampling a combination of channels from _texture */
     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;
+    private _target : HtmlElementTexture | RawCubeTexture;
     /* The internal texture representation of the original texture */
     private _originalInternalTexture : Nullable<InternalTexture> = null;
+    /* Keeps track of whether we have modified the texture */
+    private _didEdit : boolean = false;
 
     private _plane : Mesh;
     private _planeMaterial : NodeMaterial;
+    private _planeFallbackMaterial : PBRMaterial;
 
     /* Tracks which keys are currently pressed */
-    private keyMap : any = {};
+    private _keyMap : any = {};
 
     private static ZOOM_MOUSE_SPEED : number = 0.0005;
     private static ZOOM_KEYBOARD_SPEED : number = 0.2;
@@ -62,46 +79,55 @@ export class TextureCanvasManager {
     private static ZOOM_OUT_KEY : string = '-';
 
     private static PAN_SPEED : number = 0.002;
-    private static PAN_MOUSE_BUTTON : number = 0; // RMB
+    private static PAN_MOUSE_BUTTON : number = 0; // LMB
     private static PAN_KEY : string = ' ';
 
     private static MIN_SCALE : number = 0.01;
     private static MAX_SCALE : number = 10;
 
+    private _tool : Nullable<Tool>;
+
+    private _setPixelData : (pixelData : PixelData) => void;
+
     public metadata : any = {
         color: '#ffffff',
         opacity: 1.0
     };
 
-    public constructor(texture: BaseTexture, canvasUI: HTMLCanvasElement, canvas2D: HTMLCanvasElement, canvasDisplay: HTMLCanvasElement) {
+    public constructor(
+        texture: BaseTexture,
+        canvasUI: HTMLCanvasElement,
+        canvas2D: HTMLCanvasElement,
+        canvasDisplay: HTMLCanvasElement,
+        setPixelData : (pixelData : PixelData) => void
+    ) {
         this._UICanvas = canvasUI;
         this._2DCanvas = canvas2D;
         this._displayCanvas = canvasDisplay;
+        this._setPixelData = setPixelData;
 
+        this._size = texture.getSize();
         this._originalTexture = texture;
-        this._size = this._originalTexture.getSize();
-
+        this._originalInternalTexture = this._originalTexture._texture;
         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._scene.clearColor = new Color4(0.11, 0.11, 0.11, 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);
+        this.grabOriginalTexture();
 
         const textureRatio = this._size.width / this._size.height;
         
         this._plane = PlaneBuilder.CreatePlane("plane", {width: textureRatio, height: 1}, this._scene);
+        this._planeFallbackMaterial = new PBRMaterial('fallback_material', this._scene);
+        this._planeFallbackMaterial.albedoTexture = this._displayTexture;
+        this._planeFallbackMaterial.disableLighting = true;
+        this._planeFallbackMaterial.unlit = true;
+        this._plane.material = this._planeFallbackMaterial;
         NodeMaterial.ParseFromSnippetAsync("#TPSEV2#4", this._scene)
             .then((material) => {
                 this._planeMaterial = material;
@@ -118,32 +144,32 @@ export class TextureCanvasManager {
             this._engine.resize();
             this._scene.render();
             let cursor = 'initial';
-            if (this.keyMap[TextureCanvasManager.PAN_KEY]) {
+            if (this._keyMap[TextureCanvasManager.PAN_KEY]) {
                 cursor = 'pointer';
             }
             this._UICanvas.parentElement!.style.cursor = cursor;
         });
 
-        this._scale = 1;
+        this._scale = 1.5;
         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._camera.orthoBottom = -1 / this._scale;
+            this._camera.orthoTop = 1 / this._scale;
+            this._camera.orthoLeft =  ratio / -this._scale;
+            this._camera.orthoRight = ratio / this._scale;
         })
 
         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 -= (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]) {
+                    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;
@@ -157,11 +183,19 @@ export class TextureCanvasManager {
                     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._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;
                     }
+                    if (pointerInfo.pickInfo?.hit) {
+                        const pos = this.getMouseCoordinates(pointerInfo);
+                        const ctx = this._2DCanvas.getContext('2d');
+                        const pixel = ctx?.getImageData(pos.x, pos.y, 1, 1).data!;
+                        this._setPixelData({x: pos.x, y: pos.y, r:pixel[0], g:pixel[1], b:pixel[2], a:pixel[3]});
+                    } else {
+                        this._setPixelData({});
+                    }
                     break;
             }
         })
@@ -169,16 +203,16 @@ export class TextureCanvasManager {
         this._scene.onKeyboardObservable.add((kbInfo) => {
             switch(kbInfo.type) {
                 case KeyboardEventTypes.KEYDOWN:
-                    this.keyMap[kbInfo.event.key] = true;
+                    this._keyMap[kbInfo.event.key] = true;
                     if (kbInfo.event.key === TextureCanvasManager.ZOOM_IN_KEY) {
-                        this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                        this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
                     }
                     if (kbInfo.event.key === TextureCanvasManager.ZOOM_OUT_KEY) {
-                        this._scale += TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
+                        this._scale -= TextureCanvasManager.ZOOM_KEYBOARD_SPEED * this._scale;
                     }
                     break;
                 case KeyboardEventTypes.KEYUP:
-                    this.keyMap[kbInfo.event.key] = false;
+                    this._keyMap[kbInfo.event.key] = false;
                     if (kbInfo.event.key == TextureCanvasManager.PAN_KEY) {
                         this._isPanning = false;
                     }
@@ -188,32 +222,70 @@ export class TextureCanvasManager {
 
     }
 
-    public updateTexture() {
+    public async 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._didEdit = true;
+        if (this._originalTexture.isCube) {
+            // TODO: fix cube map editing
+            let pixels : ArrayBufferView[] = [];
+            for (let face = 0; face < 6; face++) {
+                let textureToCopy = this._originalTexture;
+                if (face === this._face) {
+                    textureToCopy = this._texture;
+                }
+                pixels[face] = await TextureHelper.GetTextureDataAsync(textureToCopy, this._size.width, this._size.height, face, {R: true, G: true, B: true, A: true});
+            }
+            if (!this._target) {
+                this._target = new RawCubeTexture(this._originalTexture.getScene()!, pixels, this._size.width, this._originalTexture.textureFormat, Engine.TEXTURETYPE_UNSIGNED_INT, false);
+                this._target.getScene()?.removeTexture(this._target);
+            } else {
+                (this._target as RawCubeTexture).update(pixels, this._originalTexture.textureFormat, this._originalTexture.textureType, false);
+            }
+        } else {
+            if (!this._target) {
+                this._target = new HtmlElementTexture("editor", this._2DCanvas, {engine: this._originalTexture.getScene()?.getEngine()!, scene: null});
+            } else {
+                (this._target as HtmlElementTexture).update();
+            }
         }
-        this._targetTexture.update();
-        this._originalTexture._texture = this._targetTexture._texture;
+        this._originalTexture._texture = this._target._texture;
         this.copyTextureToDisplayTexture();
     }
 
     private copyTextureToDisplayTexture() {
-        TextureHelper.GetTextureDataAsync(this._texture, this._size.width, this._size.height, 0, this._displayChannel)
+        let channelsToDisplay : TextureChannelsToDisplay = {
+            R: true,
+            G: true,
+            B: true,
+            A: true
+        }
+        this._channels.forEach(channel => channelsToDisplay[channel.id] = channel.visible);
+        TextureHelper.GetTextureDataAsync(this._originalTexture, this._size.width, this._size.height, this._face, channelsToDisplay)
             .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 set channels(channels: Channel[]) {
+        // Determine if we need to re-render the texture. This is an expensive operation, so we should only do it if channel visibility has changed.
+        let needsRender = false;
+        if (channels.length !== this._channels.length) {
+            needsRender = true;
+        }
+        else {
+            channels.forEach(
+                (channel,index) => {
+                    if (channel.visible !== this._channels[index].visible) {
+                        needsRender = true;
+                    }
+                }
+            );
+        }
+        this._channels = channels;
+        if (needsRender) {
+            this.copyTextureToDisplayTexture();
+        }
     }
 
     public static paintPixelsOnCanvas(pixelData : Uint8Array, canvas: HTMLCanvasElement) {
@@ -221,19 +293,31 @@ export class TextureCanvasManager {
         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 grabOriginalTexture() {
+        // Grab image data from original texture and paint it onto the context of a DynamicTexture
+        TextureHelper.GetTextureDataAsync(
+            this._originalTexture,
+            this._size.width,
+            this._size.height,
+            this._face,
+            {R:true ,G:true ,B:true ,A:true}
+        ).then(data => {
+            TextureCanvasManager.paintPixelsOnCanvas(data, this._2DCanvas);
+            this._texture.update();
+            this.copyTextureToDisplayTexture();
+        })
+    }
+
+    public getMouseCoordinates(pointerInfo : PointerInfo) : Vector2 {
+        if (pointerInfo.pickInfo?.hit) {
+            const x = Math.floor(pointerInfo.pickInfo.getTextureCoordinates()!.x * this._size.width);
+            const y = Math.floor((1 - pointerInfo.pickInfo.getTextureCoordinates()!.y) * this._size.height);
+            return new Vector2(x,y);
+        } else {
+            return new Vector2();
+        }
     }
 
     public get scene() : Scene {
@@ -248,12 +332,42 @@ export class TextureCanvasManager {
         return this._size;
     }
 
+    public set tool(tool: Nullable<Tool>) {
+        if (this._tool) {
+            this._tool.instance.cleanup();
+        }
+        this._tool = tool;
+        if (this._tool) {
+            this._tool.instance.setup();
+        }
+    }
+
+    public get tool(): Nullable<Tool> {
+        return this._tool;
+    }
+
+    public set face(face: number) {
+        if (this._face !== face) {
+            this._face = face;
+            this.copyTextureToDisplayTexture();
+        }
+    }
+
+    public resetTexture() : void {
+        this._originalTexture._texture = this._originalInternalTexture;
+        this.grabOriginalTexture();
+        this._didEdit = false;
+    }
+
     public dispose() {
         if (this._planeMaterial) {
             this._planeMaterial.dispose();
         }
-        if (this._originalInternalTexture) {
-            this._originalInternalTexture.dispose();
+        if (this._didEdit) {
+            this._originalInternalTexture?.dispose();
+        }
+        if (this._tool) {
+            this._tool.instance.cleanup();
         }
         this._displayTexture.dispose();
         this._texture.dispose();

+ 157 - 84
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/textureEditor.scss

@@ -1,110 +1,183 @@
 #texture-editor {
-    display: grid;
     height: 100%;
     width: 100%;
-    grid-template-columns: auto auto auto auto auto;
-    grid-template-rows: 60px calc(100% - 60px);
-}
+    color: white;
+    background-color: #1e1e1e;
 
-#controls {
-    width: 100%;
-    grid-row: 1;
-    grid-column: 1 / 6;
-    background: #464646;
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-    justify-content: space-around;
+    .icon {
+        width: 40px;
+        height: 40px;
+        &.button {
+            background-color: #333333;
+            &:hover {
+                background-color: #4a4a4a;
+                cursor: pointer;
+            }
+            &.active {
+                background-color: #666666;
+            }
+            &:active {
+                background-color: #888888;
+            }
+        }
+    }
+    
+    .has-tooltip {
+        display: inline-block;
+        .tooltip {
+            visibility: hidden;
+            background-color: rgb(255, 255, 255);
+            z-index: 1;
+            position: absolute;
+            opacity: 0;
+            transition: opacity 0.5s;
+            line-height: normal;
+            font-size: 14px;
+            padding: 0px 5px;
+            color: black;
+        }
+    
+        &:hover .tooltip {
+            visibility: visible;
+            opacity: 1;
+        }
+    }
+    
+    #properties {
+        position: absolute;
+        width: 100%;
+        height: 40px;
+        
+        display: flex;
+        flex-flow: row nowrap;
+        align-items: center;
+        font-size: 12px;
+        color: white;
+        user-select: none;
+        background-color: #1e1e1e;
+    
+        .tab {
+            display: flex;
+            line-height: 40px;
+            height: 40px;
+            flex-shrink: 0;
+            flex-grow: 0;
+            margin-right: 2px;
+            background-color: #333333;
+        }
+        #dimensions-tab {
+            label {
+                margin-left: 15px;
+                color: #afafaf;
+                input {
+                    width: 40px;
+                    height: 24px;
+                    background-color: #000000;
+                    color: #ffffff;
+                    border: 0;
+                    padding-left: 4px;
+                    font-size: 12px;
+                }
+    
+                &:last-of-type {
+                    margin-right: 8px;
+                }
+            }
+        }
+        #right-tab {
+            flex-grow: 1;
+            flex-shrink: 1;
+            margin-right: 0;
+            .content {
+                position: absolute;
+                right: 0px;
+            }
+        }
+    
+        .pixel-data {
+            &:first-of-type {
+                margin-left: 15px;
+            }
+            width: 45px;
+            color: #afafaf;
+            display: flex;
+            justify-content: space-between;
+            .value {
+                display: inline-block;
+                width: 30px;
+                color: white;
+            }
+        }
+    }
     
     #toolbar {
+        position: absolute;
+        top: 60px;
+        left: 0;
+        width: 40px;
         display: flex;
         flex-direction: column;
         justify-content: left;
         
         #tools {
             display: flex;
-            flex-direction: row;
-            justify-content: center;
-            form {
-                margin: 0;
-            }
+            flex-direction: column;
+            
         }
-
+    
         #color {
-            label {
-                color: white;
-                margin: 1em;
+            margin-top: 8px;
+            #activeColor {
+                margin: 10px;
+                width: 20px;
+                height: 20px;
+                border-radius: 50%;
             }
         }
     }
     
-    #channels {
-        display: flex;
-        flex-direction: row;
-        justify-content: center;
-        align-items: center;
-
-        .command {
-            border: 1px solid transparent;
-            background:transparent;
-            font-size: 20px;
-        }
+    #channels-bar {
+        position: absolute;
+        top: 60px;
+        right: 0;
+        width: 80px;
+        background: #666666;
+        .channel {
+            color: white;
+            border-bottom: 2px solid #232323;
+            width: 80px;
+            height: 40px;
+            font-size: 16px;
+            display: flex;
+            align-items: center;
     
-        .selected {
-            border: 1px solid rgb(51, 122, 183);
+            &.uneditable {
+                background: #333333;
+            }
+
+            &:hover {
+                cursor: pointer;
+            }
         }
+        user-select: none;
     }
-}
-
-#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;
+    
+    #bottom-bar {
+        position: absolute;
+        bottom: 0;
+        height: 30px;
+        width: 100%;
+        background-color: #333333;
+        font-size: 14px;
+        user-select: none;
+        line-height: 30px;
+        #file-url {
+            margin-left: 30px;
+        }
+    }
 }

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

@@ -1,18 +1,39 @@
 import * as React from 'react';
 import { GlobalState } from '../../../../../globalState';
 import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
-import { TextureCanvasManager } from './textureCanvasManager';
-import { TextureChannelToDisplay } from '../../../../../../textureHelper';
+import { TextureCanvasManager, PixelData } from './textureCanvasManager';
+import { Tool, ToolBar } from './toolBar';
+import { PropertiesBar } from './propertiesBar';
+import { Channel, ChannelsBar } from './channelsBar';
+import { BottomBar } from './bottomBar';
+import { Tools } from "babylonjs/Misc/tools";
+import { TextureCanvasComponent } from './textureCanvasComponent';
 
 require('./textureEditor.scss');
 
 interface TextureEditorComponentProps {
     globalState: GlobalState;
     texture: BaseTexture;
+    url: string;
 }
 
 interface TextureEditorComponentState {
-    channel: TextureChannelToDisplay;
+    tools: Tool[];
+    activeToolIndex: number;
+    metadata: any;
+    channels: Channel[];
+    pixelData : PixelData;
+    face: number;
+}
+
+interface ToolData {
+    name: string;
+    type: any;
+    icon: string;
+}
+
+declare global {
+    var _TOOL_DATA_ : ToolData;
 }
 
 export class TextureEditorComponent extends React.Component<TextureEditorComponentProps, TextureEditorComponentState> {
@@ -21,19 +42,35 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
     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);
+        let channels : Channel[] = [
+            {name: 'Red', visible: true, editable: true, id: 'R', icon: require('./assets/channelR.svg')},
+            {name: 'Green', visible: true, editable: true, id: 'G', icon: require('./assets/channelG.svg')},
+            {name: 'Blue', visible: true, editable: true, id: 'B', icon: require('./assets/channelB.svg')},
+        ];
+        if (this.props.texture.isCube) {
+            channels.push({name: 'Display', visible: true, editable: true, id: 'A', icon: require('./assets/channelD.svg')});
+        } else {
+            channels.push({name: 'Alpha', visible: true, editable: true, id: 'A', icon: require('./assets/channelA.svg')});
+        }
         this.state = {
-            channel: TextureChannelToDisplay.All,
+            tools: [],
+            activeToolIndex: -1,
+            metadata: {
+                color: '#ffffff',
+                opacity: 1
+            },
+            channels,
+            pixelData: {},
+            face: 0
         }
+        this.loadTool = this.loadTool.bind(this);
+        this.changeTool = this.changeTool.bind(this);
+        this.setMetadata = this.setMetadata.bind(this);
+        this.saveTexture = this.saveTexture.bind(this);
+        this.setFace = this.setFace.bind(this);
+        this.resetTexture = this.resetTexture.bind(this);
     }
 
     componentDidMount() {
@@ -41,35 +78,95 @@ export class TextureEditorComponent extends React.Component<TextureEditorCompone
             this.props.texture,
             this.canvasUI.current!,
             this.canvas2D.current!,
-            this.canvasDisplay.current!
+            this.canvasDisplay.current!,
+            (data : PixelData) => {this.setState({pixelData: data})}
         );
     }
 
     componentDidUpdate() {
-        this._textureCanvasManager.displayChannel = this.state.channel;
+        let channelsClone : Channel[] = [];
+        this.state.channels.forEach(channel => channelsClone.push({...channel}));
+        this._textureCanvasManager.channels = channelsClone;
+        this._textureCanvasManager.metadata = {...this.state.metadata};
+        this._textureCanvasManager.face = this.state.face;
     }
 
     componentWillUnmount() {
         this._textureCanvasManager.dispose();
     }
 
+    // There is currently no UI for adding a tool, so this function does not get called
+    loadTool(url : string) {
+        Tools.LoadScript(url, () => {
+            const tool : Tool = {
+                ..._TOOL_DATA_,
+                instance: new _TOOL_DATA_.type({
+                    scene: this._textureCanvasManager.scene,
+                    canvas2D: this._textureCanvasManager.canvas2D,
+                    size: this._textureCanvasManager.size,
+                    updateTexture: () => this._textureCanvasManager.updateTexture(),
+                    getMetadata: () => this.state.metadata,
+                    setMetadata: (data : any) => this.setMetadata(data)
+                })
+            };
+            const newTools = this.state.tools.concat(tool);
+            this.setState({tools: newTools});
+                console.log(tool);
+        });
+    }
+
+    changeTool(index : number) {
+        if (index != -1) {
+            this._textureCanvasManager.tool = this.state.tools[index];
+        } else {
+            this._textureCanvasManager.tool = null;
+        }
+        this.setState({activeToolIndex: index});
+    }
+
+    setMetadata(newMetadata : any) {
+        const data = {
+            ...this.state.metadata,
+            ...newMetadata
+        }
+        this.setState({metadata: data});
+    }
+
+    setFace(face: number) {
+        this.setState({face});
+    }
+
+    saveTexture() {
+        Tools.ToBlob(this.canvas2D.current!, (blob) => {
+            Tools.Download(blob!, this.props.url);
+        });
+    }
+
+    resetTexture() {
+        this._textureCanvasManager.resetTexture();
+    }
+
     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>
+            <PropertiesBar
+                texture={this.props.texture}
+                saveTexture={this.saveTexture}
+                pixelData={this.state.pixelData}
+                face={this.state.face}
+                setFace={this.setFace}
+                resetTexture={this.resetTexture}
+            />
+            <ToolBar
+                tools={this.state.tools}
+                activeToolIndex={this.state.activeToolIndex}
+                addTool={this.loadTool}
+                changeTool={this.changeTool}
+                metadata={this.state.metadata}
+                setMetadata={this.setMetadata}
+            />
+            <ChannelsBar channels={this.state.channels} setChannels={(channels) => {this.setState({channels})}}/>
+            <TextureCanvasComponent canvas2D={this.canvas2D} canvasDisplay={this.canvasDisplay} canvasUI={this.canvasUI} texture={this.props.texture}/>
+            <BottomBar name={this.props.url}/>
         </div>
     }
 }

+ 53 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/materials/textures/toolBar.tsx

@@ -0,0 +1,53 @@
+import * as React from 'react';
+
+export interface Tool {
+    type: any,
+    name: string,
+    instance: any,
+    icon: string
+}
+
+interface ToolBarProps {
+    tools: Tool[];
+    addTool(url: string): void;
+    changeTool(toolIndex : number): void;
+    activeToolIndex : number;
+    metadata: any;
+    setMetadata(data : any): void;
+}
+
+interface ToolBarState {
+    toolURL : string;
+}
+
+export class ToolBar extends React.Component<ToolBarProps, ToolBarState> {
+    constructor(props : ToolBarProps) {
+        super(props);
+        this.state = {
+            toolURL: "",
+        };
+    }
+    render() {
+        // No need to display toolbar if there aren't any tools loaded
+        if (this.props.tools.length === 0) return <></>;
+        return <div id='toolbar'>
+            <div id='tools'>
+                {this.props.tools.map(
+                    (item, index) => {
+                        return <img
+                            src={`data:image/svg+xml;base64,${item.icon}`}
+                            className={index === this.props.activeToolIndex ? 'icon button active' : 'icon button'}
+                            alt={item.name}
+                            title={item.name}
+                            onClick={() => this.props.changeTool(index)}
+                            key={index}
+                        />
+                    }
+                )}
+            </div>
+            <div id='color' title='Color' className='icon button'>
+                <div id='activeColor' style={{backgroundColor: this.props.metadata.color}}></div>
+            </div>
+        </div>;
+    }
+}

+ 2 - 2
inspector/src/components/actionTabs/tabs/propertyGrids/sprites/spritePropertyGridComponent.tsx

@@ -15,7 +15,7 @@ import { Color4LineComponent } from '../../../lines/color4LineComponent';
 import { FloatLineComponent } from '../../../lines/floatLineComponent';
 import { SliderLineComponent } from '../../../lines/sliderLineComponent';
 import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { TextureHelper, TextureChannelToDisplay } from '../../../../../textureHelper';
+import { TextureHelper } from '../../../../../textureHelper';
 import { Nullable } from 'babylonjs/types';
 
 interface ISpritePropertyGridComponentProps {
@@ -94,7 +94,7 @@ export class SpritePropertyGridComponent extends React.Component<ISpriteProperty
         var size = texture.getSize();
 
         if (!this.imageData) {
-            TextureHelper.GetTextureDataAsync(texture, size.width, size.height, 0, TextureChannelToDisplay.All, this.props.globalState).then(data => {
+            TextureHelper.GetTextureDataAsync(texture, size.width, size.height, 0, {R: true, G:true, B:true, A:true}, this.props.globalState).then(data => {
                 this.imageData = data;
                 this.forceUpdate();
             });

+ 55 - 37
inspector/src/textureHelper.ts

@@ -7,17 +7,16 @@ import { RenderTargetTexture } from 'babylonjs/Materials/Textures/renderTargetTe
 import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
 import { Nullable } from 'babylonjs/types';
 
-export enum TextureChannelToDisplay {
-    R,
-    G,
-    B,
-    A,
-    All
+export interface TextureChannelsToDisplay {
+    R: boolean;
+    G: boolean;
+    B: boolean;
+    A: boolean;
 }
 
 export class TextureHelper {
 
-    private static _ProcessAsync(texture: BaseTexture, width: number, height: number, face: number, channel: TextureChannelToDisplay, globalState: Nullable<GlobalState>, resolve: (result: Uint8Array) => void, reject: () => void) {
+    private static _ProcessAsync(texture: BaseTexture, width: number, height: number, face: number, channels: TextureChannelsToDisplay, globalState: Nullable<GlobalState>, resolve: (result: Uint8Array) => void, reject: () => void) {
         var scene = texture.getScene()!;
         var engine = scene.getEngine();
 
@@ -37,7 +36,7 @@ export class TextureHelper {
             passPostProcess.dispose();
 
             setTimeout(() => {
-                this._ProcessAsync(texture, width, height, face, channel, globalState, resolve, reject);
+                this._ProcessAsync(texture, width, height, face, channels, globalState, resolve, reject);
             }, 250);
 
             return;
@@ -68,34 +67,53 @@ export class TextureHelper {
             //Reading datas from WebGL
             var data = engine.readPixels(0, 0, width, height);
 
-            if (!texture.isCube) {
-                if (channel != TextureChannelToDisplay.All) {
-                    for (var i = 0; i < width * height * 4; i += 4) {
-
-                        switch (channel) {
-                            case TextureChannelToDisplay.R:
-                                data[i + 1] = data[i];
-                                data[i + 2] = data[i];
-                                data[i + 3] = 255;
-                                break;
-                            case TextureChannelToDisplay.G:
-                                data[i] = data[i + 1];
-                                data[i + 2] = data[i];
-                                data[i + 3] = 255;
-                                break;
-                            case TextureChannelToDisplay.B:
-                                data[i] = data[i + 2];
-                                data[i + 1] = data[i + 2];
-                                data[i + 3] = 255;
-                                break;
-                            case TextureChannelToDisplay.A:
-                                data[i] = data[i + 3];
-                                data[i + 1] = data[i + 3];
-                                data[i + 2] = data[i + 3];
-                                data[i + 3] = 255;
-                                break;
+            if (!channels.R || !channels.G || !channels.B || !channels.A) {
+                for (var 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;
                 }
             }
 
@@ -130,16 +148,16 @@ export class TextureHelper {
         }
     }
 
-    public static GetTextureDataAsync(texture: BaseTexture, width: number, height: number, face: number, channel: TextureChannelToDisplay, globalState?: GlobalState): Promise<Uint8Array> {
+    public static GetTextureDataAsync(texture: BaseTexture, width: number, height: number, face: number, channels: TextureChannelsToDisplay, globalState?: GlobalState): Promise<Uint8Array> {
         return new Promise((resolve, reject) => {
             if (!texture.isReady() && texture._texture) {
                 texture._texture.onLoadedObservable.addOnce(() => {
-                    this._ProcessAsync(texture, width, height, face, channel, globalState || null, resolve, reject);
+                    this._ProcessAsync(texture, width, height, face, channels, globalState || null, resolve, reject);
                 });
                 return;
             }        
 
-            this._ProcessAsync(texture, width, height, face, channel, globalState || null, resolve, reject);
+            this._ProcessAsync(texture, width, height, face, channels, globalState || null, resolve, reject);
         });
     }
 }